Поведение среды исполнения (Runtime Behavior)

TL;DR: Контейнер = процесс с PID 1. docker stop → SIGTERM → 10 сек → SIGKILL. Exit code 137 = OOM Kill. Без ротации логов Docker заполнит весь диск.

Запуск контейнера — это лишь начало. Для обеспечения стабильной работы приложений в продакшене необходимо понимать, как Docker управляет жизненным циклом процесса, как обрабатывает сигналы остановки, куда направляет логи и что делает при сбоях.

1. Жизненный цикл процесса и Сигналы

В мире Linux управление процессами осуществляется через сигналы (Signals). Docker транслирует команды CLI в системные сигналы для процесса с PID 1 внутри контейнера.

Graceful Shutdown (Изящное завершение)

Когда вы выполняете docker stop, происходит следующая последовательность:

  1. SIGTERM (Signal 15): Docker отправляет этот сигнал главному процессу. Это вежливая просьба: “Пожалуйста, заверши работу”.
    • Ожидание приложения: Приложение должно перехватить этот сигнал, перестать принимать новые запросы, дописать данные на диск и закрыть соединения.
  2. Timeout: По умолчанию Docker ждет 10 секунд.
  3. SIGKILL (Signal 9): Если процесс не завершился за отведенное время, ядро убивает его принудительно. Это “выдергивание шнура из розетки” — возможна потеря данных или повреждение файлов.

Проблема PID 1: Если ваше приложение запускается через shell-скрипт (CMD ["/bin/sh", "-c", "npm start"]), то PID 1 получает sh. Shell обычно не пересылает сигналы дочерним процессам. В итоге npm start не получает SIGTERM, и Docker всегда убивает его через SIGKILL после 10 секунд ожидания. Решение: Используйте exec в entrypoint-скриптах или запускайте приложение напрямую (CMD ["npm", "start"]).

2. Коды выхода (Exit Codes)

Когда контейнер останавливается, он возвращает код выхода. Понимание этих кодов критично для отладки.

  • 0: Успешное завершение (Intentional stop).
  • 1: Ошибка приложения (Application error).
  • 137 (128 + 9): Процесс убит сигналом SIGKILL. Чаще всего это OOM Killer (нехватка памяти) или результат docker kill.
  • 139 (128 + 11): Segmentation Fault (ошибка доступа к памяти). Обычно указывает на баг в низкоуровневом коде (C/C++ библиотеки).
  • 143 (128 + 15): Процесс корректно завершился после получения SIGTERM.

3. Политики перезапуска (Restart Policies)

Docker имеет встроенный механизм “самоисцеления” (self-healing) для одиночных контейнеров.

  • no: Не перезапускать никогда (по умолчанию).
  • on-failure: Перезапускать, только если exit code != 0. Полезно для скриптов, которые должны упасть при ошибке, но остановиться при успехе.
  • always: Всегда перезапускать (даже если вы остановили его вручную, он встанет после рестарта демона).
  • unless-stopped: То же, что always, но если вы явно сказали docker stop, контейнер не запустится сам после перезагрузки демона.

4. Логирование (Logging Drivers)

По умолчанию Docker перехватывает потоки STDOUT и STDERR контейнера и пишет их в файлы на хосте (JSON-File driver).

Проблема ротации логов

Стандартная настройка Docker не ограничивает размер логов. Если ваше приложение пишет много логов, файл /var/lib/docker/containers/.../*.log может забить всё дисковое пространство хоста.

Решение:

  1. Настройка ротации в daemon.json:
    "log-driver": "json-file",
    "log-opts": {
      "max-size": "10m",
      "max-file": "3"
    }
  2. Использование других драйверов: syslog, journald, gelf (для Graylog), awslogs, fluentd. При использовании этих драйверов команда docker logs может перестать работать, так как логи уходят сразу во внешнюю систему.

5. Управление ресурсами (OOM Killer)

Что происходит, когда контейнеру не хватает памяти?

  1. Если лимиты не заданы: Контейнер может съесть всю RAM хоста, что приведет к зависанию всей системы или срабатыванию системного OOM Killer, который может убить даже dockerd или sshd.
  2. Если лимит задан (--memory="512m"):
    • При попытке выделить память сверх лимита, ядро Linux убивает процесс внутри контейнера.
    • Контейнер падает с кодом 137.
    • В docker inspect поле OOMKilled будет иметь значение true.

Best Practice: Всегда устанавливайте лимиты памяти (Memory Limit) для контейнеров в продакшене. Java и Node.js приложениям также нужно явно указывать их внутренние лимиты Heap Size, чтобы они соответствовали лимитам контейнера.

Подводные камни

ЗаблуждениеРеальность
«docker stop мгновенно останавливает»Сначала SIGTERM, потом 10 сек ожидания, потом SIGKILL. Shell как PID 1 не пробрасывает сигналы
«Логи Docker безопасны»Без ротации (max-size/max-file) логи заполнят диск. Нет лимита по умолчанию
«always restart — хорошая идея»Контейнер перезапустится даже после docker stop + рестарт демона. Используй unless-stopped
«Контейнер упал — что-то сломалось»Exit code 0 = нормальное завершение, 137 = OOM или kill, 143 = graceful shutdown