Docker остановить контейнер: stop, kill и graceful shutdown

Контейнер, который висит ровно десять секунд после команды docker stop, — это не каприз Docker, а точный диагноз. Ровно столько длится grace-период по умолчанию, после которого демон теряет терпение и добивает процесс сигналом SIGKILL. Если ваше приложение регулярно уходит именно за десять секунд, значит, оно либо не получило SIGTERM, либо проигнорировало его. В обоих случаях данные — буферы, пул соединений, незакоммиченные транзакции — уходят в никуда.
Механика здесь простая, но именно на ней спотыкаются чаще всего. SIGTERM получает процесс с PID 1 внутри контейнера — тот, что прописан в ENTRYPOINT или CMD. И если в Dockerfile использована shell-форма записи, PID 1 становится оболочкой, а не самим приложением. Bash и sh не транслируют сигналы дочерним процессам: SIGTERM уходит в пустоту, таймер досчитывает до нуля, Docker бьёт по всему семейству процессов. Приложение не успевает ни дописать логи, ни корректно закрыть соединения с базой.
Лечится это тремя способами, и все они элементарны. Первый — exec-форма в Dockerfile: CMD ["node", "server.js"] вместо CMD npm start, тогда приложение само становится PID 1 и получает сигнал напрямую. Второй — флаг --init при запуске: Docker подставит крошечный tini в роли первого процесса. Третий — явный ENTRYPOINT с dumb-init или tini внутри образа. Проверить, кто именно занял PID 1, можно командой docker exec my-app ps -p 1 -o pid,comm,args. Если в выводе sh или bash — вот ваши потерянные секунды и данные.
Docker предлагает четыре инструмента выключения, и разница между ними не косметическая. docker stop посылает SIGTERM и ждёт десять секунд, после чего прилетает SIGKILL. docker kill бьёт сразу, без предупреждений, хотя флаг --signal позволяет отправить другой сигнал, например SIGHUP для перечитывания конфига. docker pause замораживает процессы через cgroup freezer: контейнер остаётся в памяти, порты заняты, соединения висят, но CPU не потребляется. docker rm -f убивает процесс и удаляет контейнер вместе с writable-слоем, тома при этом остаются на месте. Вывод очевиден: если приложение умеет реагировать на сигналы, используйте stop. Всё остальное — аварийный инструментарий.
Таймаут по умолчанию — десять секунд, и для приложений с долгими операциями этого мало. Миграции схемы, выгрузка отчётов, коммит offset в Kafka — всё это требует запаса. Флаг -t 60 даёт приложению минуту на завершение. Обратная сторона: при деплое через CI/CD эти секунды умножаются на число реплик, и роллаут растягивается. Здесь важен баланс между аккуратностью и скоростью выкатки.
Отдельная головная боль — политика перезапуска. Останавливаете контейнер, а он через секунду снова в статусе Up. Причина почти всегда в параметрах always или unless-stopped, которые заставляют демон поднимать процесс при любом завершении, кроме явного docker stop. Посмотреть текущую политику можно через docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' my-app, убрать авторестарт у работающего контейнера — командой docker update --restart=no my-app. В Kubernetes аналогом служит restartPolicy в манифесте пода, но для Deployment доступен только Always, так что локальный docker stop там не поможет: под пересоздастся вместе с контейнером.
Бывают ситуации, когда контейнер зависает в статусе Stopping или Dead. Типичные причины — недоступное NFS или SMB-монтирование, процесс в непрерываемом сне из-за проблем с диском, healthcheck, который не успевает завершиться. Если процесс находится в состоянии D, его не убьёт даже SIGKILL: сигнал доставится, но ядро не сможет выгрузить задачу, пока не ответит подсистема ввода-вывода. Единственный надёжный выход — починить хранилище или перезагрузить хост. Проверить состояние можно через docker inspect my-app --format '{{.State.Status}} {{.State.ExitCode}} {{.State.Error}}', а непрерываемые и зомби-процессы видны на хосте командой ps -eo pid,stat,comm | grep -E ' D| Z'.
Перед остановкой продакшена стоит пройтись по короткому чек-листу. Убедиться, что нет активных миграций и фоновых задач в очереди. Снять контейнер с балансировщика и дождаться, пока разойдутся живые соединения. Проверить, что тома смонтированы с хоста, а не лежат в writable-слое. Задать таймаут с запасом на завершение транзакций. Прочитать логи после остановки через docker logs --tail 50 my-app. И убедиться, что restart policy не поднимет контейнер обратно.
Осторожного обращения требует команда docker system prune -a --volumes. Она сносит все остановленные контейнеры, неиспользуемые образы и тома. На боевой машине это почти всегда означает потерю данных, которые никто не бэкапил. Для быстрой уборки в dev-окружении это удобно, для продакшена — потенциальная катастрофа.
В повседневной работе выручают алиасы: dstop для аккуратной остановки с таймаутом 45 секунд, dkill для аварий, dstate для вывода статусов всех контейнеров одной строкой. Однажды автор этого разбора полдня ловил баг в staging: PostgreSQL-контейнер стабильно висел десять секунд и уходил в SIGKILL, оставляя WAL в странном виде. Оказалось, в Dockerfile кто-то дописал обёртку bash -c, и она сломала обработку сигналов. С тех пор проверка PID 1 в каждом новом образе перед выкаткой стала обязательным ритуалом. Дешевле, чем разбирать битый кластер.
Для штатной остановки хватает docker stop с адекватным таймаутом, но сначала убедитесь, что приложение действительно получает SIGTERM. Exec-форма в Dockerfile или флаг --init решают девять из десяти проблем. Kill и rm -f держите для аварий, а перед выключением продакшена проверяйте restart policy и наличие томов на хосте. Данные в томе после остановки не пропадут — они исчезнут только при явном удалении тома или флаге --volumes у docker compose down.


