Перейти к содержанию

4. Контейнеры как процессы

Цель темы: понимать жизненный цикл контейнера: как создавать, запускать, останавливать и удалять; различать CMD и ENTRYPOINT, exec и attach; понимать потоки STDIN/STDOUT, коды выхода, политики перезапуска и проблему PID 1 (сигналы, tini/dumb-init, зомби-процессы).


Определения терминов

Жизненный цикл контейнера

Контейнер может находиться в состояниях: created (создан, не запущен), running (работает), exited (остановлен, процесс завершён), paused (приостановлен). Переходы между ними выполняются командами run, start, stop, kill, pause, unpause, rm.

CMD и ENTRYPOINT (напоминание)

CMD — аргументы по умолчанию для процесса контейнера; можно переопределить в docker run. ENTRYPOINT — исполняемая команда/программа, которая запускается первой; аргументы CMD или docker run передаются ей. Если ENTRYPOINT не задан, то CMD определяет и команду, и аргументы. При одновременном использовании ENTRYPOINT — «что запускать», CMD — «с какими аргументами по умолчанию».

exec vs attach

attach — подключение к уже запущенному процессу контейнера: его stdin/stdout/stderr привязываются к вашему терминалу. То же, что интерактивный запуск без -d. exec — запуск нового процесса внутри того же контейнера (в том же namespace), с возможностью получить отдельный интерактивный shell (docker exec -it <container> sh). Для отладки и администрирования обычно используют exec; attach редко нужен и может «привязаться» к основному процессу, который не читает stdin.

Restart policy

Restart policy — правило перезапуска контейнера после завершения процесса. Варианты: no (по умолчанию), always, unless-stopped, on-failure (и опционально с лимитом попыток). Задаётся при создании контейнера (docker run --restart=...) и влияет на поведение после перезагрузки хоста и после падения процесса.

PID 1 problem

В PID namespace контейнера процесс, запущенный по ENTRYPOINT/CMD, получает PID 1. Ядро возлагает на него особую роль: перезапуск завершённых дочерних процессов (reap), чтобы они не становились зомби, и корректная обработка сигналов (в т.ч. SIGTERM при docker stop). Обычное приложение часто не делает wait() и не пересылает сигналы — отсюда «PID 1 problem»: зомби и неверная остановка контейнера.


run, start, stop, kill

docker run

Создаёт контейнер из образа и по умолчанию запускает его. Если указан -d (detached), контейнер работает в фоне; без -d — привязка к текущему терминалу (stdin/stdout процесса контейнера).

docker run nginx
docker run -d --name web -p 8080:80 nginx
docker run -it --rm alpine sh

После завершения процесса контейнер переходит в состояние exited. С флагом --rm контейнер удаляется после остановки.

docker start

Запускает уже созданный (остановленный) контейнер. Процесс стартует с теми же ENTRYPOINT/CMD и параметрами, с которыми контейнер был создан.

docker start web
docker start -a web   # привязать stdout/stderr к терминалу

docker stop

Отправляет процессу контейнера SIGTERM, ждёт завершения (по умолчанию 10 с), затем при необходимости отправляет SIGKILL. Таймаут можно задать: docker stop -t 30 <container>.

docker stop web

Graceful shutdown возможен только если процесс с PID 1 обрабатывает SIGTERM (сам или через init вроде tini).

docker kill

По умолчанию отправляет SIGKILL (немедленное завершение без обработки приложением). Можно указать другой сигнал: docker kill --signal=SIGTERM web.

docker kill web

Практика

Для нормального завершения сначала используйте docker stop; docker kill — когда контейнер не реагирует или нужна принудительная остановка.


exec и attach

exec — новый процесс в контейнере

Запуск отдельной команды (часто shell) внутри работающего контейнера. Не заменяет основной процесс; контейнер продолжает работать.

docker exec -it web sh
docker exec web nginx -t
docker exec web cat /etc/nginx/nginx.conf

-it — интерактивный TTY и stdin (для shell). Для отладки и проверки конфигурации предпочтительнее exec, а не attach.

attach — к stdin/stdout основного процесса

Подключение к уже запущенному контейнеру «как к его основному процессу». Ввод и вывод идут в основной процесс (PID 1). Если этот процесс не читает stdin (например, nginx), attach может выглядеть «зависшим» или не давать интерактива.

docker attach web

Чтобы отключиться без остановки контейнера: Ctrl+P, затем Ctrl+Q (detach). Ctrl+C отправит SIGINT основному процессу и может остановить контейнер.

Внимание

При attach вы разделяете один поток с основным процессом. Для интерактивной отладки и оболочки используйте docker exec -it <container> sh.


STDIN, STDOUT и интерактивный режим

  • Без -i контейнер не получает stdin; без -t не выделяется TTY. Для интерактивного shell нужны оба: docker run -it ... или docker exec -it ....
  • -d (detached) отключает привязку к текущему терминалу; контейнер работает в фоне, логи смотреть через docker logs.
  • docker logs -f — поток вывода stdout/stderr контейнера (то, что пишет основной процесс).

Exit codes

Код выхода процесса контейнера сохраняется в метаданных контейнера. Его можно увидеть в docker inspect или при docker run без -d — код выхода будет кодом выхода команды docker run.

docker run alpine false
echo $?   # 1
docker inspect --format '{{.State.ExitCode}}' <container>

В оркестраторах (Kubernetes) и скриптах по exit code определяют успех/неуспех задачи (0 — успех, не 0 — ошибка).


Restart policies

Политика Описание
no Не перезапускать (по умолчанию).
always Всегда перезапускать при остановке; при перезагрузке хоста контейнер тоже стартует.
unless-stopped Перезапускать, пока контейнер не остановили вручную; после ручной остановки не стартовать при перезагрузке хоста.
on-failure Перезапускать только при ненулевом коде выхода; опционально лимит попыток: on-failure:5.
docker run -d --restart=unless-stopped --name web nginx

Production

Для сервисов, которые должны подниматься после перезагрузки ноды, часто используют unless-stopped. Для одноразовых задач (миграции, джобы) — no или явная остановка после выполнения.


PID 1: сигналы и зомби

Почему контейнер не реагирует на SIGTERM?

При docker stop демон отправляет SIGTERM процессу с PID 1 в контейнере. Если этот процесс:

  • не обрабатывает SIGTERM (не пересылает дочерним процессам или не завершает работу),
  • или это оболочка (/bin/sh), которая не пересылает сигнал приложению,

то контейнер не завершится gracefully и через таймаут получит SIGKILL. Решение: сделать PID 1 init-процесс (tini, dumb-init), который запускает ваше приложение как дочерний процесс и пересылает ему сигналы и перезапускает завершённых детей.

Что будет, если процесс PID 1 упал?

Если процесс с PID 1 завершается (по любой причине), весь PID namespace контейнера завершается: ядро уничтожает все процессы в этом namespace. Контейнер переходит в состояние exited. Демон может перезапустить контейнер по политике restart (например, on-failure или always).

Зомби-процессы

Дочерние процессы, завершившиеся, но не «подобранные» родителем через wait(), остаются в состоянии zombie. В контейнере родитель — PID 1. Если приложение не делает wait(), зомби накапливаются. Init (tini, dumb-init) делает wait() за всеми дочерними процессами, поэтому зомби не копятся.

Использование tini / dumb-init

В образе: установить tini или dumb-init и сделать их ENTRYPOINT, а приложение — аргументом.

ENTRYPOINT ["/tini", "--"]
CMD ["node", "index.js"]

При запуске: использовать встроенный в Docker init:

docker run --init -d myimage

--init подключает tini как PID 1; ваш CMD/ENTRYPOINT запускается как дочерний процесс tini.

Практика

Если образ не содержит init и приложение не предназначено быть PID 1, в production предпочтительно запускать с --init или пересобрать образ с tini/dumb-init.


Паттерны использования

Паттерн Описание
Отладка через exec Использовать docker exec -it <container> sh для shell и проверки, не attach.
Restart policy для сервисов unless-stopped или always для долгоживущих сервисов; on-failure для джобов при необходимости.
Init для PID 1 Всегда использовать --init или tini/dumb-init в образе, если основной процесс не умеет быть PID 1.
Graceful shutdown Обеспечить обработку SIGTERM в приложении или через init; задать достаточный таймаут в docker stop -t и в оркестраторе.

Антипаттерны

Антипаттерн Почему плохо Что делать
Приложение как PID 1 без init Зомби, неверная реакция на SIGTERM, контейнер не останавливается корректно. Добавить tini/dumb-init или docker run --init.
Использовать attach вместо exec Привязка к одному процессу, риск случайно остановить контейнер (Ctrl+C). Для отладки и shell — только exec.
Игнорировать exit code В CI и оркестраторах нельзя понять, упала задача или нет. Проверять exit code в скриптах и настраивать restart policy по смыслу.
Не задавать restart policy для сервисов После перезагрузки хоста контейнер не поднимется. Для сервисов указывать --restart=unless-stopped или аналог в оркестраторе.

Вопросы на собеседовании

Почему контейнер не реагирует на SIGTERM?
Процесс с PID 1 не обрабатывает SIGTERM (не пересылает приложению или не завершает работу). Часто это оболочка или приложение, не рассчитанное на роль PID 1. Решение: использовать init (tini, dumb-init) как PID 1 или запуск с --init.

Что будет, если процесс PID 1 упал?
Весь контейнер (PID namespace) завершается; контейнер переходит в exited. Демон может перезапустить его по политике restart (always, on-failure и т.д.).


Примеры из production

Контейнер «висит» при docker stop

Таймаут 10 с истекает, затем SIGKILL. Причина — PID 1 не обрабатывает SIGTERM. В логах приложения можно не увидеть попытки завершения. Решение: добавить --init или пересобрать образ с tini; в приложении обрабатывать SIGTERM и закрывать соединения.

Накопление зомби в долгоживущем контейнере

В контейнере много процессов в состоянии Z (zombie). PID 1 не делает wait(). Решение: init (tini/dumb-init) как PID 1; при необходимости переписать приложение, чтобы перезапускать дочерние процессы или использовать отдельный reaper.

Restart policy и перезагрузка ноды

После перезагрузки сервера контейнеры с no не стартуют; с always или unless-stopped — стартуют демоном при загрузке. В Kubernetes эту роль выполняет kubelet, а не Docker restart policy.


Дополнительные материалы