Текст и данные · Инструкция
Docker ENTRYPOINT: exec vs shell и PID 1
Docker ENTRYPOINT: exec vs shell — это различие, которое становится заметно не при успешном старте, а при остановке.
Короткий ответ
Как выбрать exec- или shell-форму ENTRYPOINT, проверить PID 1 и SIGTERM и исправить контейнер, который долго останавливается.
Что меняется между exec- и shell-формой
Exec-форма записывается как JSON-массив, например `ENTRYPOINT ["myapp", "serve"]`. Docker запускает указанную программу напрямую, без промежуточного `/bin/sh -c`, поэтому аргументы передаются предсказуемо, а основной процесс может стать PID 1 контейнера. Shell-форма выглядит как `ENTRYPOINT myapp serve`; Docker запускает её через оболочку, и само приложение оказывается дочерним процессом shell. Docker документация предпочитает exec-форму для ENTRYPOINT именно из-за сигналов и поведения основного процесса. У shell-формы есть удобство: доступны подстановка переменных оболочки, пайпы и `&&`, но это не бесплатная магия — появляется дополнительный процесс и меняется доставка сигналов. Если shell нужен сознательно, часто лучше сделать отдельный entrypoint-скрипт, выполнить подготовительные команды и завершить его строкой `exec myapp "$@"`, чтобы shell заменился приложением. Тогда вы сохраняете возможности скрипта, но не оставляете оболочку посредником перед долгоживущим процессом.
Совет: Проверка проще теории: `docker exec <name> ps -o pid,ppid,args` сразу покажет, кто PID 1 и есть ли `/bin/sh -c` перед приложением.
Exec и shell: практическая разница
- Запуск. Программа запускается напрямую. Команда идёт через /bin/sh -c
- Сигналы. Основной процесс получает сигналы как PID 1. Приложение может не получить сигнал через shell
- Переменные оболочки. Нет автоматической подстановки shell. Есть $VAR, пайпы, && и другие возможности shell
- Аргументы docker run. Удобно добавляются к exec-ENTRYPOINT. Поведение ограничено; shell-ENTRYPOINT игнорирует CMD/run-аргументы как ожидаемый app-аргумент
- Типичный выбор. Сервис, сервер, worker. Короткая shell-логика или wrapper с осознанным exec
Совет: Если контейнер запускает сложную цепочку shell-команд, вынесите её в скрипт: это легче читать, тестировать и завершать `exec` основной программы.
Почему PID 1 в контейнере ведёт себя особым образом
В Linux процесс с PID 1 имеет специальные обязанности. Docker отдельно предупреждает, что процесс PID 1 может игнорировать сигналы, для которых действие по умолчанию — завершение, если программа сама не обрабатывает их. Это значит, что даже переход на exec-форму не гарантирует корректное graceful shutdown: приложение должно уметь обрабатывать `SIGTERM` или другой сигнал, который используется для остановки. Вторая обязанность — сбор завершившихся дочерних процессов. Если приложение порождает процессы и не reap-ит их, в контейнере могут накапливаться зомби. Для таких сценариев Docker предлагает `--init`: небольшой init-процесс становится PID 1, пересылает сигналы и собирает дочерние процессы. Это не заменяет обработчик graceful shutdown внутри приложения, но снимает часть типичных обязанностей init. Выбор между «приложение как PID 1» и `--init` зависит от архитектуры процесса, а не от универсального правила.
Как исправить контейнер, который плохо останавливается
- Посмотрите итоговую конфигурацию: `docker inspect <container>` и Dockerfile. Найдите `Entrypoint` и `Cmd`, затем определите, используется exec- или shell-форма.
- Проверьте процессы внутри контейнера. Если PID 1 — `/bin/sh -c ..`, а ваше приложение имеет другой PID, shell стоит убрать из пути сигналов либо корректно завершить через `exec`.
- Перепишите простой сервис на JSON-форму: `ENTRYPOINT ["/app/server"]`, а изменяемые аргументы вынесите в `CMD ["--port", "8080"]`.
- Если нужны подготовительные команды, используйте wrapper script и последней командой делайте `exec "$@"` или `exec /app/server ..`.
- Проверьте остановку: запустите контейнер, выполните `docker stop` и посмотрите, успевает ли приложение закрыть соединения и завершиться до таймаута. При необходимости добавьте корректный обработчик SIGTERM в приложение.
- Если программа создаёт дочерние процессы, которые она не собирает, протестируйте запуск с `--init` и проверьте дерево процессов.
Предупреждение: Не лечите проблему только увеличением stop timeout. Длинный таймаут может скрыть тот факт, что сигнал вообще не доходит до нужного процесса.
Как сочетаются ENTRYPOINT и CMD
Удобная модель — считать `ENTRYPOINT` стабильной исполняемой программой, а `CMD` её параметрами по умолчанию. Например, `ENTRYPOINT ["myapp"]` и `CMD ["serve", "--port=8080"]` позволяют пользователю заменить аргументы через `docker run image worker`, не меняя сам исполняемый файл. Docker документация рекомендует использовать exec-форму и для `ENTRYPOINT`, и для `CMD`, когда они работают вместе. Если `CMD` записан shell-формой рядом с exec-ENTRYPOINT, результатом может стать передача оболочки как аргумента вместо ожидаемого списка параметров. Отдельно стоит помнить про `docker run --entrypoint`: он полностью переопределяет ENTRYPOINT и полезен для диагностики, но это не способ постоянно исправлять неудобный Dockerfile. В продакшене лучше сделать образ с прозрачными предсказуемыми defaults, чем полагаться на длинный набор run-флагов.
Проверка перед публикацией образа
- Основной сервис запускается exec-формой либо wrapper-скрипт заканчивается `exec`.
- Приложение корректно реагирует на SIGTERM и успевает завершить работу до stop timeout.
- В контейнере понятно, кто PID 1 и кто собирает дочерние процессы.
- `CMD` используется как изменяемые аргументы, если это соответствует интерфейсу образа.
- Shell-форма оставлена только там, где реально нужны возможности оболочки.
- Тест `docker stop` выполняется в CI или хотя бы перед выпуском критичного образа.
Важно: Если контейнер работает в Kubernetes или другом оркестраторе, корректная обработка завершения особенно важна: платформа также полагается на сигналы и ограниченное время graceful shutdown.
Как протестировать graceful shutdown до продакшена
Сделайте проверку воспроизводимой: запустите контейнер с тестовой нагрузкой, установите активное соединение или задачу, затем отправьте обычный `docker stop` и засеките, что происходит. Приложение должно получить сигнал, перестать принимать новую работу, корректно закрыть текущие ресурсы и завершиться до истечения таймаута. Одновременно смотрите `docker logs` и дерево процессов, чтобы убедиться, что финальные сообщения действительно пишет основной процесс, а не промежуточная оболочка. После остановки проверьте exit code и отсутствие осиротевших данных или незавершённых файлов. Если поведение отличается между локальным Docker и оркестратором, сравните stop signal, grace period и команду запуска: один и тот же image может получать разные runtime-параметры. Такой тест полезно повторять после изменения base image, entrypoint-скрипта или способа запуска, потому что один небольшой refactor способен вернуть shell между Docker и приложением.
Что учитывать
В Linux процесс с PID 1 имеет специальные обязанности. Docker отдельно предупреждает, что процесс PID 1 может игнорировать сигналы, для которых действие по умолчанию — завершение, если программа сама не обрабатывает их. Это значит, что даже переход на exec-форму не гарантирует корректное graceful shutdown: приложение должно уметь обрабатывать `SIGTERM` или другой сигнал, который используется для остановки. Вторая обязанность — сбор завершившихся дочерних процессов. Если приложение порождает…
Источники и проверка
Фактическая часть сверена по первичным источникам (в т.ч. Dockerfile reference). Пример и формулировки — редакция N1RO на 2026-09-21.