Docker долго останавливает приложение: shell-форма CMD и доставка SIGTERM
При обновлении приложения контейнер каждый раз останавливается с заметной задержкой. В журнале нет сообщения о штатном завершении, хотя в программе написан обработчик сигнала. Увеличение времени ожидания почти ничего не меняет. Одна из причин находится ещё до бизнес-кода: основным процессом контейнера стала оболочка, а приложение запущено её дочерним процессом.
При обычной остановке Docker посылает настроенный сигнал основному процессу контейнера. По умолчанию это SIGTERM. Если процесс не завершился за отведённое время, следует принудительное завершение. Поэтому прежде чем увеличивать ожидание, нужно ответить на два вопроса: кто получает сигнал и что этот процесс делает после получения.
Проверяем форму команды
В Linux-образе запись CMD python app.py запускается через оболочку. Передача сигналов дочерней программе при этом зависит от поведения оболочки и конкретного запуска. Надёжнее сделать приложение непосредственным основным процессом через exec-форму: CMD ["python", "-u", "app.py"]. Ключ -u позволяет сразу увидеть сообщения Python в журнале.
Если образ наследует ENTRYPOINT, проверьте и его: изменение одного CMD может лишь поменять аргументы чужого сценария запуска. Сценарий-обёртка должен завершаться командой exec, когда подготовка закончена и управление передаётся приложению. Она заменяет текущую оболочку процессом программы. Запуск в фоне через амперсанд требует уже отдельного управления дочерними процессами и их ожидания.
Небольшой пример с завершением
Создайте пустой учебный каталог и сохраните два файла. Первый — app.py. Он устанавливает обработчики SIGTERM и SIGINT, печатает сообщение о готовности и ждёт остановки. Обработчик только устанавливает флаг. Основной цикл замечает его и переходит в блок очистки; там в реальном приложении закрывают соединения и заканчивают уже начатую работу.
import signal
import time
stopping = False
def request_stop(signum, frame):
global stopping
stopping = True
signal.signal(signal.SIGTERM, request_stop)
signal.signal(signal.SIGINT, request_stop)
print("ready", flush=True)
try:
while not stopping:
time.sleep(0.2)
finally:
print("cleanup complete", flush=True)
Второй файл — Dockerfile. Здесь нет дополнительной оболочки или фонового процесса: Python получает роль основного процесса контейнера напрямую. Это пример для Linux-контейнера; поведение сигналов Windows-контейнеров следует разбирать отдельно.
FROM python:3.12-slim
WORKDIR /app
COPY app.py .
CMD ["python", "-u", "app.py"]
Соберите отдельный образ командой docker build -t signal-demo ., затем запустите его: docker run -d --name signal-demo signal-demo. После появления ready в docker logs signal-demo выполните docker stop --timeout 5 signal-demo. В журнале должна появиться строка cleanup complete, а состояние контейнера должно стать завершённым. Имя используйте только для этого учебного примера, чтобы не остановить другой контейнер.
Отличаем доставку сигнала от долгой очистки
Сначала убедитесь, что приложение начало завершение. Для настоящего сервиса добавьте сообщение непосредственно после выхода из основного цикла и после закрытия каждого долгого ресурса. Если первая отметка есть, а последней нет, сигнал дошёл: дальше ищите зависшее соединение, ожидание потока или бесконечный запрос. Если нет даже первой, вернитесь к процессу запуска и регистрации обработчика.
Для процесса с несколькими потоками или дочерними программами прямого запуска может быть недостаточно: нужно согласованно остановить всех участников. При большом числе дочерних процессов помогает init-процесс, но он не создаёт за приложение правильную логику завершения. Порядок остановки по-прежнему должен быть определён: перестать принимать новую работу, ограниченно дождаться текущей, освободить ресурсы и выйти.
Время ожидания выбирайте по этой процедуре. Если известная корректная очистка требует нескольких секунд, дайте ей запас. Если обработчик вообще не вызывается, большой таймаут только удлинит каждый выпуск. Формы команды описаны в справочнике Dockerfile, а сигнал и время ожидания — в параметрах docker stop.
Полезно оставить проверку остановки рядом с проверкой запуска в инструкции проекта. Коллега должен видеть ожидаемые сообщения и понимать, какой шаг не завершился. При обновлении базового образа повторите её: унаследованная точка входа или новая обёртка могут изменить дерево процессов даже при прежнем коде приложения. Так задержка остановки обнаружится до очередного рабочего выпуска.
Завершение стоит проверять и при отсутствии текущих запросов, и во время короткой тестовой операции. В первом случае видна доставка сигнала, во втором — порядок остановки и сохранение уже принятой работы.
В платном курсе «Docker и Docker Compose: контейнеризация приложений» — посмотрите программу команды запуска связываются с образом, окружением и диагностикой контейнера. Затем вы перейдёте к приложению с PostgreSQL, обновлению и откату, передаче проекта и восстановлению базы. Это позволяет рассматривать остановку как часть воспроизводимого выпуска: важно не только поднять процесс, но и завершить его перед заменой версии.
Продолжить на практике
- Docker и Docker Compose: контейнеризация приложений ↗
Упакуйте приложение в Docker и запустите его с PostgreSQL через Compose. Образы, сети, тома, диагностика поломок, обновление версий и восстановление данных — с практикой и итоговым проектом.
