Learning Platform
Глоссарий Troubleshooting
Урок 08.03 · 22 мин
Начальный
dockerdockerfilecmdentrypointshell-formexec-form

CMD vs ENTRYPOINT: что и как выполняется

В каждом Dockerfile должна быть инструкция, которая говорит «что запускается, когда контейнер стартует». Их две: CMD и ENTRYPOINT. Различить их сложно с первого раза — оба «запускают команду», оба могут быть в exec form или shell form, оба можно переопределить через docker run. Но за этим стоит важная семантика: ENTRYPOINT это «как» (всегда выполняется), CMD это «что» (default аргументы, легко переопределить).

В этом уроке: shell form vs exec form, отличия CMD от ENTRYPOINT, и комбо для гибкого CLI-like интерфейса контейнера.


Signals — kill, SIGTERM/SIGKILL и async-signal-safety

Shell form vs exec form

Обе инструкции (RUN тоже) поддерживают две формы:

Shell form — команда как строка:

CMD python app.py --port 8080

При запуске контейнера Docker оборачивает это в /bin/sh -c "python app.py --port 8080". Под капотом:

  • Spawns /bin/sh
  • Sh парсит команду, делает variable substitution ($VAR), wildcard expansion (*)
  • Sh форкает и exec’ит python

Exec form — команда как JSON array:

CMD ["python", "app.py", "--port", "8080"]

Тут НЕТ shell. Docker exec’ит первый элемент массива напрямую как PID 1:

  • Spawns python напрямую
  • Аргументы передаются как argv[1..n]
  • Никаких variable substitution, wildcard expansion

Какую форму выбрать:

Exec form предпочтительнее по двум причинам:

  1. Сигналы. В shell form контейнер запускает sh как PID 1, и sh запускает твой процесс как child. Когда docker stop посылает SIGTERM, он идёт PID 1 (sh) — а sh по дефолту не пересылает сигналы детям. Твой Python-процесс не получит SIGTERM и не сможет gracefully shutdown. В exec form Python запускается как PID 1 напрямую — сигналы идут ему.

  2. Производительность. На один процесс меньше (без sh -c).

# Плохо: shell form
CMD python app.py
# Внутри контейнера: PID 1 = /bin/sh, PID 2 = python
# docker stop посылает SIGTERM PID 1 -- sh не передаёт детям

# Хорошо: exec form
CMD ["python", "app.py"]
# Внутри контейнера: PID 1 = python
# docker stop посылает SIGTERM PID 1 = python -- graceful shutdown работает

Shell form имеет одно преимущество: variable substitution и shell-конструкции:

ENV PORT=8080
# Shell form -- работает, sh раскрывает $PORT
CMD python app.py --port $PORT

# Exec form -- НЕ РАБОТАЕТ, $PORT передаётся буквально
CMD ["python", "app.py", "--port", "$PORT"]   # python получит литерал "$PORT"

# Workaround: явно вызвать sh
CMD ["sh", "-c", "python app.py --port $PORT"]

В большинстве DE-сценариев лучше переменные читать в самом приложении (os.environ['PORT']), а в Dockerfile использовать exec form.

WARNING

Hadolint правило DL3025 — “Use arguments JSON notation for CMD and ENTRYPOINT”. Это автоматически блокирует shell form в CI, потому что shell form ломает SIGTERM-handling.


CMD: что выполнить по умолчанию

CMD задаёт команду, которая будет выполнена при docker run image БЕЗ дополнительных аргументов. Её легко переопределить:

FROM python:3.13-slim
WORKDIR /app
COPY app.py .
CMD ["python", "app.py"]
# Запускается app.py (CMD)
docker run myimage

# CMD переопределён аргументами docker run
docker run myimage python other.py   # запустится python other.py
docker run myimage bash                # запустится bash (для отладки)
docker run myimage ls /app             # запустится ls

В Dockerfile может быть только одна CMD. Если их несколько — действует последняя.

CMD удобна, когда контейнер по умолчанию делает одну вещь, но иногда нужно переопределить. Типичный пример — Postgres:

# Внутри postgres:16 Dockerfile (упрощённо)
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["postgres"]
# По умолчанию запускается postgres
docker run postgres:16

# Можно переопределить CMD для отладки:
docker run postgres:16 psql -V    # запустит psql -V вместо postgres
# (но ENTRYPOINT docker-entrypoint.sh всё равно выполнится)

ENTRYPOINT: обёртка, которая всегда выполняется

ENTRYPOINT задаёт команду, которая выполняется всегда, и аргументы docker run или CMD передаются ей как параметры.

FROM python:3.13-slim
COPY etl.py /usr/local/bin/etl
RUN chmod +x /usr/local/bin/etl
ENTRYPOINT ["python", "/usr/local/bin/etl"]
# Запустится: python /usr/local/bin/etl
docker run my-etl

# Запустится: python /usr/local/bin/etl --verbose --date 2026-01-15
docker run my-etl --verbose --date 2026-01-15

# Аргументы добавляются к ENTRYPOINT

Это превращает контейнер в executable, который ведёт себя как обычная CLI-утилита: ты передаёшь ему флаги, он работает.

В Dockerfile может быть только один ENTRYPOINT. Если их несколько — действует последний.

ENTRYPOINT shell form тоже существует, но имеет тот же недостаток с сигналами:

# Плохо: shell form ENTRYPOINT
ENTRYPOINT python app.py
# Все CMD / docker run args ИГНОРИРУЮТСЯ -- sh -c принимает только одну строку

# Хорошо: exec form
ENTRYPOINT ["python", "app.py"]

Комбо ENTRYPOINT + CMD: default args

Магия начинается когда оба используются вместе. ENTRYPOINT задаёт «как» (что всегда выполняется), CMD задаёт «что по умолчанию передать».

FROM python:3.13-slim
COPY etl.py /app/etl.py
WORKDIR /app

ENTRYPOINT ["python", "etl.py"]
CMD ["--mode", "full", "--verbose"]
# Запустится: python etl.py --mode full --verbose
docker run my-etl

# CMD переопределена -- запустится: python etl.py --mode incremental
docker run my-etl --mode incremental

# ENTRYPOINT всегда python etl.py, CMD заменяется аргументами docker run

Это и есть «контейнер как CLI-утилита с default args». Postgres использует тот же паттерн:

docker run postgres:16              # запускает postgres
docker run postgres:16 postgres -D /data --max-connections=200  # postgres с custom args
docker run postgres:16 psql         # переопределяет CMD на psql, ENTRYPOINT всё ещё выполняется
docker run -it postgres:16 bash     # эквивалентно: docker-entrypoint.sh bash
ENTRYPOINT + CMD: гибкая комбинация
ENTRYPOINT['python','etl.py']ENTRYPOINT: всегда выполняется первым. Это 'как' запустить контейнер. Без --entrypoint флага не переопределяется.
+
CMD['--mode','full']CMD: default аргументы для ENTRYPOINT. Заменяется аргументами docker run.
run: defaultsdocker run без args: python etl.py --mode full
cmd defaultedpython etl.py --mode full
run: custom argsdocker run my-etl --mode incremental: CMD заменяется
cmd overriddenpython etl.py --mode incremental

Переопределение ENTRYPOINT

Иногда нужно полностью переопределить ENTRYPOINT — например, для отладки. Флаг --entrypoint:

# Заменить ENTRYPOINT на bash для отладки
docker run -it --entrypoint /bin/bash my-etl
# Внутри bash, ENTRYPOINT python etl.py НЕ запускается

# Заменить на python без etl.py
docker run -it --entrypoint python my-etl -c "import etl; etl.dry_run()"

--entrypoint принимает только executable; аргументы идут после имени образа (как CMD).

Это полезно для troubleshooting: «образ собрался, но запускается с ошибкой — давай попаду внутрь и посмотрю».


Когда что использовать

Только CMD:

Когда контейнер — простой скрипт, и пользователю может понадобиться его переопределить (для отладки, для запуска тестов внутри образа).

FROM python:3.13-slim
COPY . /app
WORKDIR /app
CMD ["python", "main.py"]

docker run image bash для отладки сработает (заменяет CMD).

Только ENTRYPOINT:

Когда контейнер — это утилита-обёртка, которая ВСЕГДА должна делать одно и то же. Аргументы передаются ей.

FROM python:3.13-slim
COPY etl.py /usr/local/bin/etl
RUN chmod +x /usr/local/bin/etl
ENTRYPOINT ["/usr/local/bin/etl"]

docker run image --date 2026-01-15 запустит etl с этими args.

ENTRYPOINT + CMD:

Когда нужна обёртка с default-аргументами, которые можно перекрыть.

FROM python:3.13-slim
COPY etl.py /usr/local/bin/etl
RUN chmod +x /usr/local/bin/etl
ENTRYPOINT ["/usr/local/bin/etl"]
CMD ["--mode", "full"]

docker run image запустит etl с --mode full. docker run image --mode incremental заменит args.

Wrapper-скрипт как ENTRYPOINT:

Часто в production используется bash-скрипт entrypoint.sh, который делает pre-startup logic (миграции, проверки), а потом exec’ит основную команду:

COPY entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/entrypoint.sh
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
CMD ["python", "app.py"]
# entrypoint.sh
#!/bin/bash
set -e

# Pre-startup logic
if [ -n "$WAIT_FOR_DB" ]; then
    until pg_isready -h "$DB_HOST"; do sleep 1; done
fi

# Run migrations
if [ "$RUN_MIGRATIONS" = "true" ]; then
    python manage.py migrate
fi

# Execute the main command (replaces $0 with $@)
exec "$@"

exec "$@" — критично: заменяет shell-процесс самим Python (PID 1 становится python), сигналы работают правильно.


Попробуй сам

Поэкспериментируй с CMD и ENTRYPOINT:

mkdir cmd-entrypoint && cd cmd-entrypoint

# Вариант 1: только CMD
cat > Dockerfile <<'EOF'
FROM python:3.13-slim
CMD ["python", "-c", "print('hello from CMD')"]
EOF
docker build -t test:cmd .
docker run --rm test:cmd
# hello from CMD
docker run --rm test:cmd python -c "print('overridden')"
# overridden  -- CMD переопределена

# Вариант 2: только ENTRYPOINT
cat > Dockerfile <<'EOF'
FROM python:3.13-slim
ENTRYPOINT ["python", "-c"]
EOF
docker build -t test:ep .
docker run --rm test:ep "print('from entrypoint')"
# from entrypoint  -- арг передан в ENTRYPOINT

# Переопределить ENTRYPOINT можно только через --entrypoint
docker run --rm --entrypoint echo test:ep "different command"
# different command

# Вариант 3: ENTRYPOINT + CMD
cat > Dockerfile <<'EOF'
FROM python:3.13-slim
ENTRYPOINT ["python", "-c"]
CMD ["print('default code')"]
EOF
docker build -t test:both .
docker run --rm test:both
# default code
docker run --rm test:both "print('custom code')"
# custom code  -- CMD заменена аргументом

cd .. && rm -rf cmd-entrypoint
docker rmi test:cmd test:ep test:both

Проверка знанийKnowledge check
В Dockerfile: CMD python app.py (shell form). Контейнер запущен в production. При docker stop приложение не успевает gracefully shutdown -- закрыть DB-connections, дописать current batch. В чём причина и как исправить?
ОтветAnswer
Shell form (без [] кавычек) выполняется через /bin/sh -c, поэтому внутри контейнера PID 1 это /bin/sh, а python это child. Когда docker stop посылает SIGTERM, он идёт PID 1 = sh. По дефолту sh не передаёт сигналы children, так что python не получает SIGTERM и не запускает graceful shutdown. По timeout (10 секунд) docker посылает SIGKILL -- python убивается жёстко, connections не закрываются, batch теряется. Исправление: использовать exec form CMD ['python', 'app.py'] -- тогда python запускается напрямую как PID 1 и сигналы идут ему.

Проверьте понимание

Результат: 0 из 0
Аналитический
Вопрос 1 из 4. В Dockerfile CMD python app.py (shell form). При docker stop приложение не успевает gracefully shutdown. В чём причина?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5