Перейти к содержимому
RootPoligon

Процесс, контейнер, образ и хост

Чтение и практика ~15 мин

Ситуация

Коллега говорит: «Запусти приложение в Docker — оно в контейнере, значит, изолировано и безопасно, пусть пользуется кто угодно». Похоже на виртуальную машину: свои файлы, свой root, свои процессы. Так ли это?

Главная мысль

Контейнер — это обычный процесс на хосте, которому ядро показывает ограниченный мир: свои файлы (из образа), свой список процессов, своё имя машины, свою сеть. Ядро у всех контейнеров общее — ядро хоста. Поэтому изоляция контейнера — это настройки одного ядра, а не отдельная машина.

Новые слова

  • Хост — машина (настоящая или виртуальная), на которой работает Docker и его ядро Linux.
  • Образ (image) — неизменяемый набор файлов и настроек запуска: «что лежит на диске и какую команду выполнить». Пример: python:3.12-slim — минимальный Linux с Python 3.12.
  • Контейнер (container) — запущенный (или остановленный) экземпляр образа: процесс плюс собственный слой для изменений файлов. Из одного образа можно запустить много контейнеров.
  • Docker Engine — служба на хосте (dockerd), которая скачивает образы и запускает контейнеры; команда docker — её пульт управления.
  • Реестр (registry) — хранилище образов, например Docker Hub.

Сравнение с курсом Git: образ похож на коммит — снимок, который не меняется; контейнер — на рабочую папку, развёрнутую из снимка, где можно что-то менять.

Разбор: смотрим на контейнер с двух сторон

Команды выполняются на учебной машине с Docker от root (или пользователя в группе docker). Папка не важна.

Одна команда в контейнере

docker run --rm python:3.12-slim python3 -c "print('Привет из контейнера')"
Привет из контейнера

Docker взял образ python:3.12-slim (если его нет локально — сначала скачает), создал контейнер, выполнил в нём python3 -c … и, благодаря --rm, удалил контейнер после завершения. Python на хосте при этом не нужен.

При первом запуске перед выводом появятся строки загрузки образа (Unable to find image … locally, Pull complete).

Долгий процесс — изнутри и снаружи

Запустим контейнер, который просто спит пять минут. -d — в фоне, --name — понятное имя с учебным префиксом:

docker run -d --name rpd-sleep python:3.12-slim sleep 300

Вывод — длинный идентификатор контейнера. Спросим изнутри, какая программа работает под номером 1:

docker exec rpd-sleep cat /proc/1/comm
sleep

docker exec выполняет ещё одну команду в работающем контейнере. Внутри sleep — процесс номер 1: первый и главный процесс контейнера. А теперь посмотрим с хоста:

docker top rpd-sleep
UID     PID     PPID    C    STIME   TTY   TIME       CMD
root    5740    5716    14   14:50   ?     00:00:00   sleep 300

Тот же процесс, но с номером 5740: хост видит его как обычный процесс среди остальных, и ps -ef на хосте тоже его покажет. Ядро одно — просто внутри контейнера список процессов «обрезан». Номера у вас будут другими; столбцы в выводе docker top для телефона сжаты.

Имя машины внутри — свой, по умолчанию начало идентификатора контейнера:

docker exec rpd-sleep cat /etc/hostname
77708b97ea32

Убираем учебный контейнер:

docker rm -f rpd-sleep

-f останавливает и удаляет контейнер. Удаляем только свой, по имени.

Образы на машине

docker images python

Строка с python и тегом 3.12-slim — образ остался после удаления контейнеров. Образ не меняется от того, что с ним делали контейнеры.

Почему root внутри — не безопасность

id внутри покажет uid=0(root). Это тот же номер пользователя 0, что у root хоста, и процесс работает на том же ядре. От остального хоста его отделяют настройки ядра — отдельные пространства имён (namespaces), ограничения ресурсов (cgroups) и урезанные права (capabilities). Они хорошо работают для обычных программ, но:

  • достаточно дать контейнеру лишнее — флаг --privileged, доступ к сокету Docker /var/run/docker.sock или папку хоста через -v, — и root внутри получает root снаружи;
  • уязвимость в ядре затрагивает все контейнеры сразу: ядро у них общее;
  • право запускать docker равно праву root на машине: кто может выполнить docker run, может смонтировать в контейнер любую папку хоста.

Поэтому контейнер с root нельзя считать песочницей для посторонних. Учебные песочницы RootPoligon устроены сложнее (отдельная среда выполнения, лимиты, отдельная машина), и даже их платформа открывает только после отдельной проверки. В курсе Docker работает на вашей учебной машине, и доступ к ней вы ни с кем не делите.

Практика: два контейнера из одного образа

  1. Предскажите: если запустить два контейнера rpd-a и rpd-b из python:3.12-slim и в rpd-a создать файл /tmp/hello.txt, увидит ли его rpd-b?
  2. Проверьте. Создать файл: docker exec rpd-a sh -c 'echo привет > /tmp/hello.txt'; посмотреть: docker exec rpd-b ls /tmp.
  3. Уберите оба контейнера.

Признак успеха: вы видите ответ на вопрос 1 в выводе и можете объяснить его словами «образ» и «контейнер»; docker ps -a --filter name=rpd- в конце пуст.

Подсказка 1

Контейнеры нужно запустить так, чтобы они жили, пока вы с ними работаете. Какая команда в разборе держала контейнер живым пять минут?

Подсказка 2

docker run -d --name rpd-a python:3.12-slim sleep 300 и так же для rpd-b.

Подсказка 3

Удалить: docker rm -f rpd-a rpd-b. Проверить, что учебных контейнеров не осталось: docker ps -a --filter name=rpd-.

Решение — в answers/01-containers-and-images.md.

Если результат другой

  • permission denied while trying to connect to the Docker daemon socket. Ваш пользователь не в группе docker. На учебной машине работайте от root. Добавлять в группу docker посторонних нельзя — см. раздел выше.
  • Cannot connect to the Docker daemon … Is the docker daemon running?. Служба Docker не запущена: systemctl status docker.
  • Conflict. The container name "/rpd-a" is already in use. Контейнер с таким именем остался с прошлой попытки. Удалите его: docker rm -f rpd-a.

Проверка понимания

  1. Чем образ отличается от контейнера? Ответьте одной фразой со словом «снимок».
  2. Почему фраза «в контейнере root, но это же контейнер — пусть пользуется кто угодно» опасна?

Ответы — в answers/01-containers-and-images.md.

Дальше: 2. Жизненный цикл контейнера.

Проверка автора и источники

  • Воспроизведено: все команды разбора и практики — Docker Engine 29.8.2, Ubuntu 24.04. Вместо python:3.12-slim использовался локальный образ-заменитель с Python 3.12 (Docker Hub из окружения проверки недоступен), поэтому строки загрузки образа и размер образа в docker images не проверены. В разборе намеренно используются только cat, id и sh; процессы смотрим через docker top с хоста. Codex на настоящем python:3.12-slim подтвердил: sh, cat, id, ls есть, ps и curl нет — GIT_DOCKER_REVIEW.md.
  • Источники: Docker overview, docker run, Docker Engine security (атакуемая поверхность демона, группа docker), namespaces(7).
Разбор и ответы — откройте после своей попытки

Практика

docker run -d --name rpd-a python:3.12-slim sleep 300
docker run -d --name rpd-b python:3.12-slim sleep 300
docker exec rpd-a sh -c 'echo привет > /tmp/hello.txt'
docker exec rpd-a ls /tmp
hello.txt
docker exec rpd-b ls /tmp

Вывод пустой: в rpd-b файла нет. Каждый контейнер получает свой слой для изменений поверх одного и того же неизменяемого образа. Запись в rpd-a попала только в его слой; образ и rpd-b её не видят.

docker rm -f rpd-a rpd-b
docker ps -a --filter name=rpd-
rpd-a
rpd-b
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES

Таблица пустая — учебных контейнеров не осталось. Файл hello.txt исчез вместе с rpd-a: слой контейнера удаляется с контейнером (подробнее — глава 5).

Воспроизведено: Docker Engine 29.8.2, образ-заменитель python:3.12-slim (см. проверку автора в главе).

Проверка понимания

  1. Образ — неизменяемый снимок файлов и настроек запуска; контейнер — процесс, запущенный из этого снимка, со своим слоем для изменений.
  2. Процесс в контейнере работает на общем ядре хоста под тем же uid 0. Одна лишняя настройка (--privileged, папка хоста, сокет Docker) или уязвимость ядра — и посторонний получает root на машине. А тот, кому дали запускать docker, уже фактически root.

Прочитали и разобрались? Отметьте урок — прогресс сохранится в этом браузере.