Представь типичную картину. Ночью прилетел всплеск трафика, приложение в контейнере словило OOM и умерло. Дежурный спит, мониторинг шлёт письмо в пустоту, а docker контейнер просто стоит в состоянии exited. Утром выясняется, что поднять его надо было одной командой - но никто этого не сделал, потому что это надо было сделать руками. Вторая классика - перезагрузили хост ради обновления ядра, а после ребута половина сервисов не встала, потому что про автозапуск никто не подумал.
Весь этот класс проблем закрывается пониманием двух вещей: как именно устроен docker жизненный цикл контейнера (через какие состояния он проходит и что переводит его из одного в другое) и как работает docker restart policy - политика, которая говорит демону, что делать, когда процесс внутри умер. Это не магия и не настройки "на удачу". Это конечный автомат с понятными правилами, и если ты их знаешь, ты строишь самовосстанавливающиеся сервисы, а не дежуришь у телефона. Разберём механику до винтика: состояния, сигналы, коды выхода, политики, связь с systemd и сбор мусора.

Состояния контейнера: конечный автомат под капотом
Контейнер - это не одно "запущен/не запущен", а полноценный конечный автомат. Демон dockerd хранит состояние в своей БД, а реально процессом управляет containerd через runtime runc. Полный набор состояний такой:
- created - контейнер создан командой create, для него выделены неймспейсы, cgroups, файловая система собрана из слоёв, но главный процесс (PID 1 внутри) ещё не запущен. Это как накрытый стол, за который никто не сел.
- running - главный процесс работает. Это основное рабочее состояние.
- paused - все процессы внутри заморожены через cgroup freezer (механизм ядра, который замораживает задачи на уровне планировщика). Процессы не получают процессорное время, но память не освобождается, сокеты держатся. Это пауза, а не остановка.
- restarting - демон сейчас перезапускает контейнер согласно restart policy. Состояние обычно мелькает быстро, но если контейнер падает в крэш-петле, ты увидишь именно его.
- exited - главный процесс завершился (сам, по сигналу или с ошибкой). Контейнер сохранён, у него есть код выхода, его можно стартануть заново.
- dead - аварийное состояние. Демон не смог корректно удалить или остановить контейнер (часто из-за залипшего mount, проблем с драйвером хранилища или подвисшего runc). Из dead его нельзя стартовать, только удалить, и то иногда с боем.
Код: Выделить всё
docker inspect -f '{{.State.Status}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}} pid={{.State.Pid}}' web
Код: Выделить всё
exited exit=137 oom=true pid=0
Команды управления и что происходит на самом деле
Теперь пройдём по командам, которые двигают контейнер между состояниями, и разберём, что реально делает каждая.
create - готовит контейнер (состояние created), но не запускает. start - запускает главный процесс. Связка create + start - это и есть привычный docker run, просто run делает оба шага сразу и обычно ещё и тянет образ.
Самое интересное - docker stop. Это не "прибить", а "вежливо попросить уйти". Механика двухфазная:
- Демон шлёт главному процессу SIGTERM (сигнал номер 15). Это приглашение завершиться красиво: дописать буферы, закрыть соединения, сбросить данные на диск.
- Дальше демон ждёт. По умолчанию - 10 секунд. Управляется флагом --time (или -t) у docker stop, либо настройкой --stop-timeout на самом контейнере.
- Если за это время процесс не вышел, прилетает SIGKILL (сигнал 9). Этот сигнал нельзя перехватить или проигнорировать - ядро убивает процесс мгновенно, без шанса на cleanup.
Код: Выделить всё
docker stop -t 30 web
Сигнал, который шлётся первым, можно поменять. В Dockerfile это инструкция STOPSIGNAL, на запуске - флаг --stop-signal. Например, nginx по своей логике хочет SIGQUIT для graceful, а не SIGTERM:
Код: Выделить всё
docker run --stop-signal=SIGQUIT --stop-timeout=25 nginx
- docker kill - сразу шлёт сигнал (по умолчанию SIGKILL), без фазы ожидания. docker kill -s SIGHUP позволяет послать произвольный сигнал, чем часто пользуются для reload конфигов на лету.
- docker restart - это stop, затем start. Внимание: docker restart выполняет ту же двухфазную остановку с тем же таймаутом, так что docker restart -t 30 тоже ждёт graceful. Команда docker restart удобна, чтобы перечитать конфиг или сбросить подвисшее состояние, но это полный цикл остановки и запуска, а не "мягкая перезагрузка процесса".
- docker pause / docker unpause - заморозить и разморозить через cgroup freezer. Полезно, чтобы временно отдать CPU другому процессу или снять консистентный снапшот, не убивая контейнер.
- docker rm - удалить контейнер. Работает только для не-running (или с флагом --force, который сначала прибьёт). Удаляет метаданные и слой записи, но не именованные тома.
Коды выхода: язык, на котором контейнер говорит, как он умер
Когда главный процесс завершается, остаётся exit code. Это не просто число - это диагноз. Базовое правило Unix: код 0 - успех, всё остальное - ошибка. Но есть важная конвенция: смерть по сигналу даёт код 128 + номер сигнала.
- 0 - процесс завершился штатно. Для долгоживущего сервиса это часто подозрительно: почему демон вдруг решил, что закончил работу?
- 1 - generic-ошибка приложения. Чаще всего исключение или ошибка в коде.
- 125 - ошибка самого демона docker (например, неверный флаг в команде run). Контейнер даже не стартанул.
- 126 - команда найдена, но не исполняемая (нет бита +x на entrypoint).
- 127 - команда не найдена (опечатка в пути, нет бинарника в образе).
- 137 - 128+9, убит SIGKILL. Если при этом OOMKilled=true - ядро прибило за память. Если false - это твой docker stop, у которого истёк таймаут.
- 143 - 128+15, корректно завершился по SIGTERM. Нормальный результат docker stop, если приложение умеет gracefully останавливаться.
Docker restart policy: автоматическое восстановление и автозапуск после ребута
Теперь главное про устойчивость. Политика перезапуска - это правило, которое демон применяет, когда главный процесс контейнера завершился. Задаётся флагом --restart у docker run или полем restart в compose. Четыре варианта:
- no - дефолт. Демон ничего не делает, контейнер просто уходит в exited. Подходит для разовых задач и батчей.
- on-failure - перезапуск только при ненулевом коде выхода. Можно ограничить число попыток: --restart on-failure:5 даст максимум пять попыток, после чего демон сдаётся. Идеально для задач, которые должны доработать, но не крутиться вечно.
- always - перезапускать всегда, независимо от кода выхода. Даже если ты остановил контейнер руками через docker stop, после рестарта демона (или ребута хоста) always снова его поднимет. Это и плюс, и грабли.
- unless-stopped - как always, но с одним отличием: если ты остановил контейнер вручную через docker stop, после перезагрузки демона или хоста он останется лежать. Демон запоминает, что ты явно его остановил, и уважает это решение.
Как это связано с ребутом хоста. Сами по себе политики работают, только пока жив демон dockerd. Чтобы контейнеры встали после перезагрузки сервера, нужны два условия одновременно:
- Демон docker должен стартовать при загрузке: systemctl enable docker (а лучше enable --now). Без этого после ребута никакой docker вообще не запустится, и никакая политика не сработает.
- У контейнера должна стоять политика always или unless-stopped. Политика no и on-failure автозапуск после ребута не дают.
Когда же писать собственный systemd-юнит на контейнер? Когда нужен жёсткий порядок старта (сначала база, потом приложение), зависимости между сервисами через After=/Requires=, или интеграция с остальной инфраструктурой хоста. Тогда контейнер запускают через docker run внутри ExecStart юнита (политику docker при этом ставят в no, чтобы рулил один хозяин - systemd, а не оба сразу).
В compose это выглядит чисто:
Код: Выделить всё
services:
api:
image: registry.company.ru/api:1.4.2
restart: unless-stopped
stop_grace_period: 30s
stop_signal: SIGTERM
Docker events и наблюдение за жизненным циклом
Чтобы видеть переходы состояний в реальном времени, есть docker events - поток событий от демона. Это бесценно при отладке крэш-петель: ты видишь точную последовательность die -> restart -> start и таймстампы между ними.
Код: Выделить всё
docker events --filter event=die --filter event=restart --since 10m
Код: Выделить всё
2026-06-15T11:02:14 container die 9f3a... (exitCode=1, image=api:1.4.2, name=api)
2026-06-15T11:02:14 container restart 9f3a... (name=api)
2026-06-15T11:02:15 container start 9f3a... (name=api)
2026-06-15T11:02:16 container die 9f3a... (exitCode=1, name=api)
Сбор мусора: prune и грабли с залипшими контейнерами
Остановленные контейнеры не исчезают сами - они копятся, занимая место под слои записи и метаданные. Со временем docker ps -a превращается в свалку. Чистят это командой prune:
Код: Выделить всё
docker container prune --filter "until=24h"
Грабли с dead. Самая неприятная ситуация - контейнер залип в состоянии dead и не удаляется ни prune, ни обычным rm. Обычно причина в том, что у контейнера остался незакрытый mount (overlayfs не размонтировался) или подвис runc-процесс. Алгоритм лечения: сначала docker rm --force. Если не помогло - смотри docker logs самого демона (journalctl -u docker) на предмет ошибок размонтирования, проверяй mount | grep docker на залипшие точки монтирования в /var/lib/docker/overlay2, при необходимости размонтируй их вручную. В крайнем случае помогает рестарт самого демона systemctl restart docker - он переинициализирует состояние и часто отпускает залипший контейнер. К dead почти всегда ведёт что-то на уровне хоста (диск, драйвер хранилища, ядро), а не само приложение.
Антипаттерны и грабли из практики
- restart: always на отладочном контейнере. Сервис падает на старте, always тут же его поднимает, он опять падает - и ты получаешь бесконечный цикл, который ещё и греет CPU. На время отладки ставь no и смотри логи спокойно.
- Игнор SIGTERM в приложении. Каждый docker stop превращается в 10 секунд ожидания и грубый SIGKILL. Данные могут не сброситься на диск. Всегда вешай обработчик SIGTERM на graceful shutdown.
- Тяжёлая работа в shutdown без запаса по таймауту. Если корректное завершение требует 40 секунд, а stop_grace_period стоит дефолтные 10 - SIGKILL прилетит посреди процесса. Подгоняй таймаут под реальное время остановки.
- Расчёт, что политика спасёт после ребута без enable docker. Классика: restart: always стоит, а демон не включён в автозагрузку. После перезагрузки ничего не встаёт. Проверь systemctl is-enabled docker.
- Накопление exited-контейнеров. Без регулярного prune диск под /var/lib/docker забивается мёртвыми контейнерами и осиротевшими слоями.
Повтори всё сам, чтобы увидеть переходы своими глазами:
Код: Выделить всё
# 1. Контейнер, который держит SIGTERM и сам не уходит
docker run -d --name life --restart unless-stopped alpine sh -c 'trap "" TERM; sleep 600'
# 2. Посмотри состояние и политику
docker inspect -f '{{.State.Status}} policy={{.HostConfig.RestartPolicy.Name}}' life
# 3. Останови с коротким таймаутом и засеки время - увидишь паузу и SIGKILL
docker stop -t 5 life
# 4. Проверь код выхода: ждём 137 (убит SIGKILL по таймауту, без OOM)
docker inspect -f 'exit={{.State.ExitCode}} oom={{.State.OOMKilled}}' life
# 5. В соседнем терминале запусти поток событий, потом стартуй и убей контейнер
docker events --since 5m --filter container=life
# 6. Уборка
docker rm life
Контрольные вопросы
- Какую последовательность сигналов шлёт docker stop и сколько ждёт по умолчанию между ними? Что произойдёт, если PID 1 игнорирует первый сигнал?
- В чём практическая разница между restart policy always и unless-stopped при перезагрузке хоста, если контейнер до этого был остановлен вручную?
- Контейнер в состоянии exited с кодом 137 и OOMKilled=false - что это значит и где искать причину? А если OOMKilled=true?
- Какие два условия должны выполниться одновременно, чтобы контейнер сам поднялся после ребута сервера?
Docker жизненный цикл контейнера - это конечный автомат: created, running, paused, restarting, exited, dead, а команды create/start/stop/restart/pause/kill/rm двигают его между состояниями. docker stop - вежливая двухфазная остановка SIGTERM -> ожидание -> SIGKILL, и коды выхода (137, 143, OOMKilled) точно говорят, как умер процесс. Docker restart policy (no, on-failure, always, unless-stopped) автоматизирует восстановление, а в связке с systemctl enable docker - и автозапуск после ребута. Знай этот автомат - и твои сервисы чинят себя сами, а ты спишь по ночам.