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.