CMD и ENTRYPOINT в Dockerfile: как правильно задать команду запуска в 2026 | AdminWiki

CMD и ENTRYPOINT в Dockerfile: как правильно задать команду запуска в 2026

26 сентября 2026 11 мин. чтения

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Флагом --entrypointCMD игнорируется

Если базовый образ не содержит ни CMD, ни ENTRYPOINT (так бывает у scratch и у минимальных баз), контейнер без явной команды не стартует: docker run потребует передать её после имени образа.

Что переопределяет docker run: CMD или ENTRYPOINT

На практике встречаются три сценария переопределения.

  1. docker run mynginx -g "daemon off;" заменяет CMD целиком, точка входа сохраняется.
  2. docker run --entrypoint /bin/sh mynginx -c "nginx -v" заменяет ENTRYPOINT. Аргументы после имени образа уходят в новый entrypoint, а CMD из образа не используется.
  3. 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 без ошибок

Порядок действий такой:

  1. Разбейте строку на массив: команда и каждый аргумент отдельным элементом в двойных кавычках.
  2. Проверьте переменные окружения. Exec-форма не вызывает shell, поэтому $HOME и подобные подстановки в ней не раскрываются.
  3. Укажите абсолютный путь к бинарнику или убедитесь, что он лежит в PATH образа.
  4. Уберите конструкции, которым нужна оболочка: &&, |, >, ; и обратные подстановки. Если они необходимы, вызывайте 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.

Что делать по порядку:

  1. Переведите CMD и ENTRYPOINT в exec-форму.
  2. Добавьте в приложение обработчик SIGTERM, если его нет.
  3. Поставьте init-процесс перед приложением, если процесс не годится на роль PID 1.
  4. Проверьте 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

  1. ENTRYPOINT записан в exec-форме, JSON-массивом.
  2. CMD тоже в exec-форме.
  3. Если ENTRYPOINT задан, CMD содержит аргументы, а не команду.
  4. PID 1 в запущенном контейнере занят вашим процессом (docker exec test ps -o pid,comm).
  5. docker stop завершает контейнер быстрее 2 секунд, ExitCode равен 0 или 143.
  6. Для процессов без обработки сигналов добавлен tini или dumb-init: ENTRYPOINT ["tini", "--"] либо флаг --init при запуске.
  7. В 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, команда запуска настроена правильно.

Поделиться: