PID 1, сигналы и restart policy: корректная остановка контейнера
Автор: Казачкин Даниил Михайлович · Обновлено
Когда контейнер получает команду остановки, Docker обращается к его главному процессу. Приложение должно успеть завершить запросы и освободить ресурсы; если главный процесс…
Когда контейнер получает команду остановки, Docker обращается к его главному процессу. Приложение должно успеть завершить запросы и освободить ресурсы; если главный процесс является лишним shell, сигнал может пройти не так, как ожидает автор Dockerfile. Здесь мы увидим доставку SIGTERM и отделим остановку от перезапуска.
Главный процесс задаёт время жизни
Контейнер работает, пока работает его основной процесс. Запуск сервера в фоне с немедленным выходом shell может сразу завершить контейнер, даже если команду запуска «успели выполнить». JSON-форма CMD ["python", "app.py"] не добавляет промежуточный shell. Если нужен shell-entrypoint, завершайте его exec основной программы, чтобы она заняла его место.
PID 1 имеет особое поведение сигналов и обязан корректно работать с дочерними процессами. Флаг --init добавляет небольшой init, который помогает пересылать сигналы и собирать завершившихся детей; он не добавляет обработчик graceful shutdown внутрь самого приложения. Назначение init.
Наблюдаемый обработчик
Сохраните worker.py:
import signal
import time
received = None
def request_stop(signum, frame):
global received
received = signum
signal.signal(signal.SIGTERM, request_stop)
print("ready", flush=True)
while received is None:
time.sleep(0.05)
print(f"received signal {received}", flush=True)
print("flushed and stopped", flush=True)Обработчик только присваивает простой флаг: печать и завершение выполняются в обычном потоке управления. Захват lock или threading.Event.set внутри signal handler может приводить к deadlock; ограничения Python signal нужно учитывать отдельно.
В Bash из той же папки:
docker run -d --name yk-signal-demo --mount "type=bind,src=$(pwd)/worker.py,dst=/worker.py,readonly" python:3.13-alpine python -u /worker.py
docker logs yk-signal-demo
docker stop --timeout 10 yk-signal-demo
docker logs yk-signal-demo
docker inspect --format '{{.State.ExitCode}}' yk-signal-demoПосле остановки ожидайте сообщения о SIGTERM (на Linux номер 15), завершающую строку и код 0. Дождитесь ready до stop: это отделяет проверку обработчика от гонки с инициализацией. docker stop сначала отправляет настроенный stop signal, обычно TERM, а после таймаута может применить KILL. Справка stop.
Выбрать политику восстановления
no не перезапускает процесс автоматически. on-failure реагирует на ненулевой exit code, always и unless-stopped отличаются поведением после ручной остановки и перезапуска daemon. Это политика процесса, не проверка успешности HTTP-запроса. Прочитайте условия применения restart policy перед использованием в сервисе.
В Compose для долгоживущего сервиса можно задать restart: unless-stopped и stop_grace_period: 20s. Значение времени должно соответствовать тому, сколько реально занимает завершение работы. Бесконечный рестарт из-за неверной конфигурации не исправляет конфигурацию; сначала посмотрите первые сообщения запуска и число повторов.
Почему unhealthy не равен exited
Healthcheck может проваливаться при работающем процессе. Обычный Docker не перезапускает контейнер только из-за метки unhealthy. Поэтому приложение с зависшим обработчиком может требовать действий внешнего supervisor или оператора; его healthcheck должен помогать диагностике, а не создавать ложную гарантию самовосстановления.
Для очереди задач graceful shutdown обычно означает прекратить брать новые задания, закончить или вернуть текущие и только затем выйти. Для HTTP — перестать принимать новые соединения, дождаться текущих в пределах бюджета и закрыть ресурсы. Это проектное поведение приложения: один только stop_grace_period не реализует его.
Задание
Измените worker так, чтобы перед выходом он делал контролируемую задержку длиннее выбранного timeout. Сравните логи и exit code, затем верните достаточный бюджет и повторите. Не используйте настоящую базу для опыта с KILL. После проверки удалите yk-signal-demo. Объясните, какая часть результата обеспечена Docker, а какая написана вами в Python: это и есть граница между инфраструктурой и корректной остановкой приложения.