ENTRYPOINT задаёт исполняемый файл, который контейнер запускает всегда. CMD отвечает за аргументы по умолчанию для этого файла или за саму команду, если ENTRYPOINT в образе не задан.
Аргументы после имени образа в docker run полностью заменяют CMD, а точку входа не трогают. Заменить ENTRYPOINT можно только явно, флагом --entrypoint. Форма записи весит больше значений: shell-форма запускает команду через /bin/sh -c, PID 1 достаётся оболочке, SIGTERM до приложения не доходит, docker stop ждёт таймаут и добивает процесс SIGKILL. В exec-форме приложение становится PID 1 напрямую и получает сигнал само.
Дальше: таблица переопределений, перевод shell-формы в exec, готовые связки ENTRYPOINT + CMD и команды, которыми проверяют, что запуск и остановка отрабатывают как ожидается.
CMD и ENTRYPOINT: в чём разница и как они работают вместе
За запуск в Dockerfile отвечают две инструкции. ENTRYPOINT задаёт точку входа, то есть исполняемый файл, который стартует всегда. CMD задаёт аргументы по умолчанию, которые передаются в ENTRYPOINT. Если ENTRYPOINT в образе нет, CMD становится командой контейнера целиком.
Правило совместной работы простое: CMD дописывается к ENTRYPOINT как список аргументов. Для собственного образа на базе nginx это выглядит так:
FROM nginx:1.27-alpine COPY nginx.conf /etc/nginx/nginx.conf ENTRYPOINT ["nginx"] CMD ["-g", "daemon off;"]
Контейнер всегда запускает nginx, а параметры запуска приходят из CMD. Передайте другой аргумент в docker run, и nginx получит его вместо значения по умолчанию, сама команда запуска останется прежней.
Действует только последняя инструкция каждого типа. Если CMD или ENTRYPOINT указаны в Dockerfile дважды, Docker использует вторую запись, первую молча игнорирует. Логика не зависит от версии Docker Engine: поля Entrypoint и Cmd зафиксированы в спецификации образа OCI, движок лишь читает их из конфига образа. Согласно спецификации, Cmd — это список аргументов, используемых как команда запуска контейнера, а Entrypoint — аргументы по умолчанию для точки входа; если Entrypoint не задан, первый элемент массива Cmd интерпретируется как исполняемый файл (спецификация OCI Image Format).
| Инструкция | Что задаёт | Чем переопределяется | Поведение при docker run image arg |
|---|---|---|---|
| CMD | Команду запуска или аргументы по умолчанию для ENTRYPOINT | Аргументами после имени образа | CMD полностью заменяется на arg |
| ENTRYPOINT | Исполняемый файл, точку входа | Только флагом --entrypoint | Остаётся прежним, arg уходит в него как аргумент |
| CMD без ENTRYPOINT | Единственную команду контейнера | Аргументами после имени образа | Запускается arg вместо CMD |
| ENTRYPOINT в shell-форме | Команду, переданную в /bin/sh -c | Флагом --entrypoint | CMD игнорируется |
Если базовый образ не содержит ни CMD, ни ENTRYPOINT (так бывает у scratch и у минимальных баз), контейнер без явной команды не стартует: docker run потребует передать её после имени образа.
Что переопределяет docker run: CMD или ENTRYPOINT
На практике встречаются три сценария переопределения.
- docker run mynginx -g "daemon off;" заменяет CMD целиком, точка входа сохраняется.
- docker run --entrypoint /bin/sh mynginx -c "nginx -v" заменяет ENTRYPOINT. Аргументы после имени образа уходят в новый entrypoint, а CMD из образа не используется.
- docker run mycurl -I https://admin-wiki.ru заменяет команду, если в образе задан только CMD.
Проверить, что реально прописано в образе, можно без запуска контейнера:
docker inspect --format '{{.Config.Entrypoint}}' mynginx
docker inspect --format '{{.Config.Cmd}}' mynginx
Пустой вывод первой команды означает, что точка входа не задана и работает только CMD. Отдельный случай, ENTRYPOINT в shell-форме: оболочка не принимает аргументы отдельным списком, поэтому CMD при такой записи игнорируется.
Остальные сценарии запуска с портами, томами и переменными окружения собраны в полном руководстве по команде docker run.
Когда использовать только CMD, только ENTRYPOINT или оба
Выбор зависит от того, меняет ли пользователь команду запуска.
Только CMD. Образы-утилиты, dev-контейнеры, базы для экспериментов: команду почти всегда подставляют снаружи.
FROM alpine:3.21 RUN apk add --no-cache curl CMD ["curl", "--help"]
docker run --rm mycurl -sI https://admin-wiki.ru запустит curl вместо справки.
Только ENTRYPOINT. Сервис с фиксированным запуском: контейнер всегда стартует один и тот же процесс, дополнительные аргументы приходят как флаги.
FROM alpine:3.21 COPY myservice /usr/local/bin/myservice ENTRYPOINT ["myservice"]
docker run --rm myservice --debug передаст флаг процессу, команду подменить не получится.
ENTRYPOINT + CMD. Параметризуемый контейнер: точка входа неизменна, а рабочие параметры имеют разумные значения по умолчанию.
FROM alpine:3.21 COPY entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh ENTRYPOINT ["/entrypoint.sh"] CMD ["--help"]
docker run --rm mytool --version подменит CMD и передаст в скрипт один аргумент, docker run --rm mytool оставит поведение по умолчанию.
Общую картину по установке, образам и жизненному циклу контейнеров держите под рукой: Docker в 2026: полное практическое руководство по установке, образам и контейнерам.
Shell и exec формы: почему exec критична для обработки сигналов
Обе инструкции записываются двумя способами. Shell-форма: CMD nginx -g 'daemon off;'. Exec-форма: CMD ["nginx", "-g", "daemon off;"] , то есть JSON-массив, где первый элемент это исполняемый файл, остальные аргументы.
Разница не косметическая. Shell-форма превращается в /bin/sh -c "команда", и PID 1 внутри контейнера достаётся оболочке, а приложение работает дочерним процессом. Когда docker stop отправляет SIGTERM, сигнал получает sh, и до приложения он часто не доходит. Docker stop сначала посылает главному процессу контейнера SIGTERM, а после grace-периода — SIGKILL; по умолчанию таймаут составляет 10 секунд для Linux-контейнеров и 30 секунд для Windows-контейнеров, если для контейнера не задан иной default (docker container stop). Число секунд ожидания после отправки сигнала задаёт флаг -t/--timeout; при значении 0 таймаут не применяется и демон ждёт завершения контейнера бесконечно. Процесс завершается жёстко, без graceful shutdown: соединения рвутся, буферы и незаписанные данные теряются.
В exec-форме оболочки нет. Процесс запускается напрямую и сам становится PID 1, поэтому SIGTERM приходит в него, а приложение успевает закрыть соединения и выйти штатно.
# shell-форма: PID 1 это /bin/sh CMD nginx -g 'daemon off;' # exec-форма: PID 1 это nginx CMD ["nginx", "-g", "daemon off;"]
Проверить, кто занимает PID 1, можно в запущенном контейнере:
docker exec web ps -o pid,ppid,comm,args docker exec web cat /proc/1/comm
В первом случае в выводе будет sh, во втором nginx. У shell-формы есть и второе следствие: лишний процесс в дереве, из-за которого логи приложения смешиваются с выводом оболочки, а коды возврата теряют однозначность.
Как переписать shell форму в exec без ошибок
Порядок действий такой:
- Разбейте строку на массив: команда и каждый аргумент отдельным элементом в двойных кавычках.
- Проверьте переменные окружения. Exec-форма не вызывает shell, поэтому $HOME и подобные подстановки в ней не раскрываются.
- Укажите абсолютный путь к бинарнику или убедитесь, что он лежит в PATH образа.
- Уберите конструкции, которым нужна оболочка: &&, |, >, ; и обратные подстановки. Если они необходимы, вызывайте sh -c осознанно.
# неверно: напечатает $HOME как есть CMD ["echo", "$HOME"] # верно: переменную раскроет оболочка CMD ["sh", "-c", "echo $HOME"]
Типичные ошибки при переводе: одинарные кавычки внутри JSON-массива, пропущенная запятая между элементами, лишняя запятая после последнего элемента. Такой Dockerfile либо не собирается, либо запускается не так, как задумано. Если в аргументе есть кавычки, экранируйте их обратным слешем.
CMD ["sh", "-c", "echo \"hello world\""]
Обратная ситуация тоже встречается: shell-форма выручает, когда нужно выполнить несколько команд подряд в простом образе. Тогда сигналы придётся обрабатывать вручную или ставить init перед оболочкой.
Типовая ошибка новичков: контейнер не останавливается по docker stop
Симптом: docker stop висит около 10 секунд, затем контейнер исчезает. Причина в связке из предыдущего раздела: сигнал не доходит до приложения, и Docker добивает процесс SIGKILL. Диагностика по шагам:
docker stop web
# засеките время: больше 1-2 секунд это признак проблемы
docker inspect --format '{{.State.ExitCode}}' web
docker inspect --format '{{.Config.StopSignal}}' web
docker logs --tail 20 web
Код 137 означает, что процесс был убит сигналом SIGKILL. Код 143 означает, что приложение получило SIGTERM и завершилось. В логах штатное завершение видно строкой вида shutting down gracefully.
Что делать по порядку:
- Переведите CMD и ENTRYPOINT в exec-форму.
- Добавьте в приложение обработчик SIGTERM, если его нет.
- Поставьте init-процесс перед приложением, если процесс не годится на роль PID 1.
- Проверьте STOPSIGNAL, если приложение ждёт не SIGTERM, а другой сигнал, например SIGQUIT.
ENTRYPOINT ["tini", "--"] CMD ["myapp", "--config", "/etc/myapp/config.yaml"]
Флаг --init у docker run запускает внутри контейнера init-процесс, который перенаправляет сигналы и собирает (reap) процессы; по умолчанию используется первый найденный в системном пути демона Docker исполняемый файл docker-init, и этот бинарник, включённый в стандартную установку, основан на tini (docker container run). Проект tini указывает, что начиная с Docker 1.13, включая все версии Docker CE, tini включён в Docker, и для его включения достаточно передать флаг --init в docker run (krallin/tini). Если нужен собственный init, ставьте tini или dumb-init из репозитория дистрибутива и указывайте его в ENTRYPOINT. Init перехватывает сигналы и передаёт их дочернему процессу, поэтому docker stop отрабатывает за доли секунды.
Готовые комбинации ENTRYPOINT + CMD для параметризуемых контейнеров
Четыре шаблона, которые копируются в Dockerfile почти без правок.
# 1. Веб-сервер: фиксированная точка входа, параметры в CMD FROM nginx:1.27-alpine COPY nginx.conf /etc/nginx/nginx.conf ENTRYPOINT ["nginx"] CMD ["-g", "daemon off;"]
# 2. Python-скрипт FROM python:3.13-slim WORKDIR /app COPY app.py . ENTRYPOINT ["python", "app.py"] CMD ["--port", "8080", "--host", "0.0.0.0"]
# 3. Node.js FROM node:lts-alpine WORKDIR /app COPY server.js . ENTRYPOINT ["node", "server.js"] CMD ["--host", "0.0.0.0"]
# 4. Универсальный скрипт-обёртка FROM alpine:3.21 COPY entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh COPY app /usr/local/bin/app ENTRYPOINT ["/entrypoint.sh"] CMD ["app", "--help"]
Теги базовых образов меняются со временем: перед сборкой сверяйтесь с официальными страницами образов на Docker Hub. Например, у официального образа nginx на Docker Hub присутствуют теги 1.27.0, mainline, 1, 1.27, latest, а также варианты на основе bookworm (nginx — Official Image).
docker run --rm myapp --port 9090 # CMD заменён на --port 9090 docker run --rm myapp --port 9090 --debug # оба аргумента уйдут в ENTRYPOINT docker run --rm --entrypoint /bin/sh myapp -c "app --help" # подмена точки входа на один запуск
Три проверки для любого из шаблонов: ENTRYPOINT записан в exec-форме, PID 1 в контейнере занят вашим приложением, SIGTERM обрабатывается приложением или init-процессом.
Шаблон entrypoint.sh с exec "$@"
#!/bin/sh set -e if [ -n "$RUN_MIGRATIONS" ]; then /app/manage.sh migrate fi mkdir -p /app/tmp chown -R app:app /app/tmp exec "$@"
Скрипт готовит окружение (миграции, права, каталоги) и последней строкой вызывает exec "$@". exec заменяет процесс оболочки на основной процесс, поэтому PID 1 остаётся за приложением, а сигналы приходят прямо в него. Без exec оболочка осталась бы родителем, и история с зависшим docker stop повторилась бы.
Условия, которые делают шаблон рабочим:
- shebang #!/bin/sh в первой строке файла;
- бит выполнения на скрипте (RUN chmod +x /entrypoint.sh);
- ENTRYPOINT в exec-форме: ENTRYPOINT ["/entrypoint.sh"], а CMD содержит аргументы: CMD ["myapp", "--flag"];
- set -e, чтобы падение подготовки останавливало контейнер, а не пряталось в логах.
Если написать ENTRYPOINT /entrypoint.sh в shell-форме, скрипт запустится через sh -c, и exec "$@" заменит промежуточную оболочку, а не PID 1. Сигналы снова не дойдут до приложения.
Проверка: как убедиться, что сигналы доходят
docker run -d --name test --init myapp
docker exec test ps -o pid,ppid,comm,args
time docker stop test
docker inspect --format '{{.State.ExitCode}}' test
docker events --filter container=test
Ориентиры: PID 1 занят приложением или tini с приложением в дочерних процессах, docker stop возвращает управление за 1-2 секунды, ExitCode равен 0 или 143, в логах есть сообщение о штатном завершении. Если остановка занимает около 10 секунд, а ExitCode равен 137, сигнал до приложения не дошёл. Для ручной проверки обработчика отправьте сигнал без остановки: docker kill -s TERM test. Контейнер должен завершиться сам, а в логах появиться запись о graceful shutdown.
Проверка и отладка команды запуска: команды и инструменты
docker inspect --format '{{.Config.Entrypoint}}' myapp
docker inspect --format '{{.Config.Cmd}}' myapp
docker inspect --format '{{.Path}} {{.Args}}' test
docker exec test ps aux | head
docker logs --tail 50 test
Первые две команды читают конфиг образа, то есть то, что записано в Dockerfile. Третья показывает запущенный контейнер: что реально стартовало с учётом флагов docker run. Если .Config.Entrypoint пуст, а .Config.Cmd заполнен, запускается CMD.
docker run --rm --entrypoint /bin/bash myapp docker run --rm --entrypoint "" myapp
Флаг --entrypoint подменяет точку входа на один запуск, пересобирать образ не нужно. Пустое значение снимает ENTRYPOINT, и тогда снова работает CMD образа.
Если контейнер падает сразу после старта, смотрите код возврата и логи: docker inspect --format '{{.State.ExitCode}}' test и docker logs test. Разбор кодов завершения и команд отладки собран в шпаргалке по управлению запущенными контейнерами Docker.
Чек-лист перед деплоем: как не допустить ошибок с CMD и ENTRYPOINT
- ENTRYPOINT записан в exec-форме, JSON-массивом.
- CMD тоже в exec-форме.
- Если ENTRYPOINT задан, CMD содержит аргументы, а не команду.
- PID 1 в запущенном контейнере занят вашим процессом (docker exec test ps -o pid,comm).
- docker stop завершает контейнер быстрее 2 секунд, ExitCode равен 0 или 143.
- Для процессов без обработки сигналов добавлен tini или dumb-init: ENTRYPOINT ["tini", "--"] либо флаг --init при запуске.
- В multi-stage сборке CMD и ENTRYPOINT заданы в финальной стадии: финальная стадия является целью сборки по умолчанию, и именно в ней задаётся runtime-конфигурация запуска приложения через CMD или ENTRYPOINT (Multi-stage builds).
docker build -t myapp . docker run --rm myapp
Слои, кэш и порядок инструкций разобраны в статье сборка Docker-образа пошагово: Dockerfile, кэширование и multi-stage build.
Пройдите по списку выше и запустите docker stop на тестовом контейнере. Если остановка занимает секунду и ExitCode равен 0, команда запуска настроена правильно.