Главная/Dockerfile/Урок

5.4. CMD и ENTRYPOINT

Цели

После этого материала вы сможете:

  • предсказать результирующую команду для любой комбинации CMD и ENTRYPOINT;
  • объяснить различие exec form и shell form и его последствия для сигналов;
  • выбрать между CMD, ENTRYPOINT и их комбинацией под конкретную задачу;
  • написать entrypoint-скрипт с корректным exec "$@";
  • объяснить, что переопределяется при docker run и что нет;
  • находить в чужих образах ошибки, из-за которых аргументы не доходят до приложения.

Предварительные знания

Ключевые термины

ТерминОбъяснение
exec formЗапись массивом JSON: ["executable", "arg"]
shell formЗапись строкой: executable arg
результирующая командаТо, что фактически выполняется при запуске container
entrypoint-скриптСкрипт-обёртка, выполняющий подготовку перед запуском приложения
переопределениеЗамена значения из образа аргументами docker run

Теория

Две инструкции, одна задача

Обе задают, что выполнится при запуске container, но с разным смыслом:

ENTRYPOINTCMD
СмыслЧто это за программаС какими аргументами по умолчанию
Переопределяется аргументами docker runнетда
Переопределяется флагом--entrypointне требуется
Типичное применениеФиксированная точка входаАргументы, которые захотят менять

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

Таблица результирующих команд

Это главная таблица урока. Она полностью описывает поведение.

ENTRYPOINTCMDdocker run без аргументовdocker run img arg1
ошибка: нет командыarg1
["echo", "hi"]echo hiarg1
echo hi (shell)/bin/sh -c "echo hi"arg1
["echo"]echoecho arg1
["echo"]["hi"]echo hiecho arg1
["echo"]hi (shell)echo /bin/sh -c hiecho arg1
echo (shell)["hi"]/bin/sh -c echo/bin/sh -c echo
echo (shell)hi (shell)/bin/sh -c echo/bin/sh -c echo

Две последние строки требуют пояснения: если ENTRYPOINT записан в shell form, CMD игнорируется полностью, и аргументы docker run тоже. Это одна из самых коварных ошибок — приложение молча игнорирует всё, что ему передают.

exec form против shell form

exec formshell form
ЗаписьCMD ["python", "app.py"]CMD python app.py
Что запускаетсяpython напрямую/bin/sh -c "python app.py"
PID 1приложение/bin/sh
Доставка SIGTERMприложениюоболочке
Подстановка переменныхнетда
Конвейеры, &&, перенаправлениянетда
Требует наличия оболочки в образенетда

Строка про PID 1 — причина, по которой exec form является правилом. Подробно разбиралось в уроке 4.4: при shell form docker stop занимает 10 секунд и завершается кодом 137, а код завершения приложения не выполняется.

Строка про подстановку переменных объясняет, зачем shell form вообще нужен:

dockerfile
CMD ["python", "-m", "$MODULE"]     # запустит модуль с именем "$MODULE"
CMD python -m $MODULE               # подставит значение переменной

Когда нужна подстановка, используйте shell form с явным exec:

dockerfile
CMD exec python -m $MODULE

Встроенная команда exec заменяет процесс оболочки процессом приложения — sh не остаётся в памяти, приложение становится PID 1.

Когда что применять

ЗадачаРешение
Образ запускает одно приложение, аргументы могут менятьсяENTRYPOINT — программа, CMD — аргументы по умолчанию
Образ-инструмент (как curl, jq)ENTRYPOINT — сам инструмент, CMD — пустой или подсказка
Универсальный образ для разных командТолько CMD; ENTRYPOINT мешал бы
Нужна подготовка перед стартомENTRYPOINT — скрипт с exec "$@", CMD — команда приложения
Образ для интерактивной отладкиТолько CMD, чтобы легко подменить на sh

Практическое соображение: ENTRYPOINT делает образ удобнее для одного сценария и неудобнее для всех остальных. Чтобы запустить в таком образе sh, придётся писать --entrypoint sh, что помнят не все.

Паттерн entrypoint-скрипта

Классическая структура:

bash
#!/bin/sh
set -e

# 1. Подготовка: ожидание зависимостей, миграции, генерация конфигурации
wait_for_database
apply_migrations

# 2. Передача управления приложению
exec "$@"

Три обязательных элемента:

set -e — прервать скрипт при ошибке любой команды. Без него подготовка может провалиться, а приложение всё равно запустится.

"$@" в кавычках — передать все аргументы, сохранив их границы. Без кавычек аргумент с пробелами разобьётся на несколько.

exec — заменить процесс скрипта процессом приложения. Без него оболочка останется PID 1, и сигналы не дойдут до приложения — та же проблема, что и при shell form.

Что переопределяется при запуске

bash
docker run <image> [аргументы]        # заменяют CMD
docker run --entrypoint <cmd> <image> # заменяет ENTRYPOINT

Тонкость: --entrypoint заменяет точку входа, но не сбрасывает CMD. Поэтому

bash
docker run --entrypoint ls myimage

выполнит ls с аргументами из CMD, что часто даёт неожиданный результат. Чтобы этого избежать, задавайте аргументы явно:

bash
docker run --entrypoint ls myimage -la /app

Внутренний механизм

Где хранятся значения

Обе инструкции записываются в конфигурацию образа (урок 3.1):

json
{
  "config": {
    "Entrypoint": ["/entrypoint.sh"],
    "Cmd": ["python", "app.py"]
  }
}

При запуске Docker формирует итоговую команду конкатенацией: Entrypoint + Cmd, где Cmd замещается аргументами docker run, если они переданы.

Shell form превращается в exec form ещё на этапе сборки:

text
CMD python app.py
      ↓
"Cmd": ["/bin/sh", "-c", "python app.py"]

Это видно в docker image inspect и объясняет, почему sh оказывается PID 1: он там записан явно.

Наследование от базового образа

Если стадия не задаёт ENTRYPOINT или CMD, они наследуются от базового образа. Отсюда неочевидное поведение:

dockerfile
FROM python:3.13-slim
# CMD не задан — наследуется ["python3"] из базового образа

Такой образ при запуске откроет интерпретатор Python, а не ваше приложение.

Важное правило: инструкция ENTRYPOINT сбрасывает унаследованный CMD. Если вы задаёте ENTRYPOINT, но не задаёте CMD, унаследованный CMD не будет использован — он обнуляется.


Команды и примеры

Подготовка

bash
mkdir -p /tmp/cmd-entry && cd /tmp/cmd-entry

Проверка таблицы результирующих команд

bash
build_and_run() {
    local name="$1" dockerfile="$2" shift 2
    printf '%s' "$dockerfile" > "Dockerfile.$name"
    docker build -q -f "Dockerfile.$name" -t "ce:$name" . > /dev/null 2>&1
    printf '  без аргументов: '
    docker run --rm "ce:$name" 2>&1 | head -1
    printf '  с аргументом:   '
    docker run --rm "ce:$name" ARG1 2>&1 | head -1
}

echo "=== Только CMD (exec form) ==="
build_and_run cmd-exec 'FROM alpine:3.21
CMD ["echo", "значение-из-CMD"]
'

echo
echo "=== Только ENTRYPOINT (exec form) ==="
build_and_run ent-exec 'FROM alpine:3.21
ENTRYPOINT ["echo", "ENTRYPOINT:"]
'

echo
echo "=== ENTRYPOINT + CMD (оба exec form) ==="
build_and_run both 'FROM alpine:3.21
ENTRYPOINT ["echo", "ENTRYPOINT:"]
CMD ["значение-из-CMD"]
'
text
=== Только CMD (exec form) ===
  без аргументов: значение-из-CMD
  с аргументом:   ARG1
=== Только ENTRYPOINT (exec form) ===
  без аргументов: ENTRYPOINT:
  с аргументом:   ENTRYPOINT: ARG1
=== ENTRYPOINT + CMD (оба exec form) ===
  без аргументов: ENTRYPOINT: значение-из-CMD
  с аргументом:   ENTRYPOINT: ARG1

Разница видна отчётливо:

  • аргумент заменил CMD;
  • аргумент добавился к ENTRYPOINT;
  • в комбинации аргумент заменил CMD, оставив ENTRYPOINT.

Опасный случай: ENTRYPOINT в shell form

bash
echo "=== ENTRYPOINT в shell form ==="
build_and_run ent-shell 'FROM alpine:3.21
ENTRYPOINT echo "ENTRYPOINT-shell:"
CMD ["значение-из-CMD"]
'
text
=== ENTRYPOINT в shell form ===
  без аргументов: ENTRYPOINT-shell:
  с аргументом:   ENTRYPOINT-shell:

CMD проигнорирован. Аргумент проигнорирован. Приложение не получило ничего.

Посмотрим конфигурацию:

bash
docker image inspect ce:ent-shell --format 'Entrypoint: {{json .Config.Entrypoint}}'
docker image inspect ce:ent-shell --format 'Cmd:        {{json .Config.Cmd}}'
text
Entrypoint: ["/bin/sh","-c","echo \"ENTRYPOINT-shell:\""]
Cmd:        ["значение-из-CMD"]

CMD в конфигурации есть, но /bin/sh -c принимает только первый аргумент как команду, а остальные становятся позиционными параметрами $0, $1 — и просто не используются.

Это молчаливая ошибка: сборка проходит, container запускается, а параметры теряются.

exec form против shell form: дерево процессов

bash
cat > Dockerfile.pid-exec <<'EOF'
FROM alpine:3.21
CMD ["sleep", "300"]
EOF

cat > Dockerfile.pid-shell <<'EOF'
FROM alpine:3.21
CMD sleep 300
EOF

docker build -q -f Dockerfile.pid-exec  -t ce:pid-exec  . > /dev/null
docker build -q -f Dockerfile.pid-shell -t ce:pid-shell . > /dev/null

for v in exec shell; do
    docker run -d --name "pid-$v" "ce:pid-$v" > /dev/null
    sleep 1
    printf '%-6s PID 1: %s\n' "$v" "$(docker exec "pid-$v" ps -o args= -p 1)"
done
text
exec   PID 1: sleep 300
shell  PID 1: /bin/sh -c sleep 300

Последствия для остановки:

bash
for v in exec shell; do
    s="$(date +%s.%N)"
    docker stop "pid-$v" > /dev/null
    e="$(date +%s.%N)"
    printf '%-6s время stop: %.2f c, код: %s\n' "$v" \
        "$(awk -v a="$s" -v b="$e" 'BEGIN{printf "%.2f", b-a}')" \
        "$(docker inspect "pid-$v" --format '{{.State.ExitCode}}')"
    docker rm "pid-$v" > /dev/null
done
text
exec   время stop: 0.31 c, код: 0
shell  время stop: 10.28 c, код: 137

Десять секунд разницы из-за формы записи одной строки.

Подстановка переменных

bash
cat > Dockerfile.var-exec <<'EOF'
FROM alpine:3.21
ENV GREETING="привет из ENV"
CMD ["echo", "$GREETING"]
EOF

cat > Dockerfile.var-shell <<'EOF'
FROM alpine:3.21
ENV GREETING="привет из ENV"
CMD echo $GREETING
EOF

cat > Dockerfile.var-fixed <<'EOF'
FROM alpine:3.21
ENV GREETING="привет из ENV"
CMD exec echo $GREETING
EOF

for v in exec shell fixed; do
    docker build -q -f "Dockerfile.var-$v" -t "ce:var-$v" . > /dev/null
    printf '%-6s -> %s\n' "$v" "$(docker run --rm "ce:var-$v")"
done
text
exec   -> $GREETING
shell  -> привет из ENV
fixed  -> привет из ENV

Exec form не выполняет подстановку — строка передана буквально. Shell form подставляет. Вариант fixed подставляет и оставляет приложение в PID 1.

Проверим последнее:

bash
cat > Dockerfile.var-pid <<'EOF'
FROM alpine:3.21
ENV SLEEP_TIME=300
CMD exec sleep $SLEEP_TIME
EOF

docker build -q -f Dockerfile.var-pid -t ce:var-pid . > /dev/null
docker run -d --name var-pid ce:var-pid > /dev/null
sleep 1
# busybox ps не знает -p, а в python:*-slim ps нет вовсе — читаем /proc
docker exec var-pid cat /proc/1/cmdline | tr '\0' ' '; echo
docker rm -f var-pid > /dev/null
text
sleep 300

Оболочки нет: exec её заменил. Мы получили и подстановку, и корректную доставку сигналов.

Наследование от базового образа

bash
cat > Dockerfile.inherit <<'EOF'
FROM python:3.13-slim
# ни CMD, ни ENTRYPOINT не заданы
EOF

docker build -q -f Dockerfile.inherit -t ce:inherit . > /dev/null
docker image inspect ce:inherit --format 'Cmd: {{json .Config.Cmd}}'
docker image inspect python:3.13-slim --format 'база Cmd: {{json .Config.Cmd}}'
text
Cmd: ["python3"]
база Cmd: ["python3"]

Образ унаследовал CMD от базового. Запуск откроет интерпретатор, а не приложение — распространённая причина «container сразу завершается» (урок 4.1).

ENTRYPOINT сбрасывает унаследованный CMD:

bash
cat > Dockerfile.reset <<'EOF'
FROM python:3.13-slim
ENTRYPOINT ["python", "-c"]
EOF

docker build -q -f Dockerfile.reset -t ce:reset . > /dev/null
docker image inspect ce:reset --format 'Entrypoint: {{json .Config.Entrypoint}}  Cmd: {{json .Config.Cmd}}'
text
Entrypoint: ["python","-c"]  Cmd: null

Унаследованный ["python3"] обнулён. Это правильное поведение — иначе результирующая команда была бы python -c python3.

Entrypoint-скрипт

bash
cat > entrypoint.sh <<'SH'
#!/bin/sh
set -e

echo "[entrypoint] подготовка окружения" >&2

# Пример подготовки: проверка обязательной переменной
if [ -z "${APP_MODE:-}" ]; then
    echo "[entrypoint] APP_MODE не задан, использую default" >&2
    export APP_MODE=default
fi

echo "[entrypoint] режим: $APP_MODE" >&2
echo "[entrypoint] передаю управление: $*" >&2

# exec обязателен: заменяет процесс скрипта процессом приложения
exec "$@"
SH
chmod +x entrypoint.sh

cat > app.py <<'PY'
import os
import signal
import sys
import time

def handle(signum, frame):
    print("[app] получен SIGTERM, завершаюсь", flush=True)
    sys.exit(0)

signal.signal(signal.SIGTERM, handle)
print(f"[app] запущен, PID={os.getpid()}, аргументы={sys.argv[1:]}", flush=True)
while True:
    time.sleep(0.2)
PY

cat > Dockerfile.entry <<'EOF'
FROM python:3.13-slim
COPY --chmod=755 entrypoint.sh /entrypoint.sh
COPY app.py /app.py
ENTRYPOINT ["/entrypoint.sh"]
CMD ["python", "-u", "/app.py", "--default-arg"]
EOF

docker build -q -f Dockerfile.entry -t ce:entry . > /dev/null

echo "=== запуск с CMD по умолчанию ==="
docker run -d --name entry-1 ce:entry > /dev/null
sleep 1
docker logs entry-1
echo "--- PID 1 ---"
docker exec entry-1 ps -o args= -p 1
text
=== запуск с CMD по умолчанию ===
[entrypoint] подготовка окружения
[entrypoint] APP_MODE не задан, использую default
[entrypoint] режим: default
[entrypoint] передаю управление: python -u /app.py --default-arg
[app] запущен, PID=1, аргументы=['--default-arg']
--- PID 1 ---
python -u /app.py --default-arg

Скрипт выполнил подготовку и передал управление. Благодаря exec приложение стало PID 1 — скрипта в памяти нет.

Проверим сигналы:

bash
s="$(date +%s.%N)"; docker stop entry-1 > /dev/null; e="$(date +%s.%N)"
printf 'время stop: %.2f c, код: %s\n' \
    "$(awk -v a="$s" -v b="$e" 'BEGIN{printf "%.2f", b-a}')" \
    "$(docker inspect entry-1 --format '{{.State.ExitCode}}')"
docker logs entry-1 2>&1 | tail -1
docker rm entry-1 > /dev/null
text
время stop: 0.38 c, код: 0
[app] получен SIGTERM, завершаюсь

Переопределение CMD при запуске:

bash
docker run --rm -e APP_MODE=production ce:entry \
    python -u /app.py --custom-arg --verbose 2>&1 | tail -2
text
[entrypoint] передаю управление: python -u /app.py --custom-arg --verbose
[app] запущен, PID=1, аргументы=['--custom-arg', '--verbose']

Подготовка выполнилась, аргументы заменились. Это и есть смысл разделения: ENTRYPOINT фиксирует процедуру, CMD задаёт изменяемую часть.

Что будет без exec

bash
cat > entrypoint-bad.sh <<'SH'
#!/bin/sh
set -e
echo "[entrypoint] подготовка" >&2
# ОШИБКА: нет exec
"$@"
SH
chmod +x entrypoint-bad.sh

cat > Dockerfile.entry-bad <<'EOF'
FROM python:3.13-slim
COPY --chmod=755 entrypoint-bad.sh /entrypoint.sh
COPY app.py /app.py
ENTRYPOINT ["/entrypoint.sh"]
CMD ["python", "-u", "/app.py"]
EOF

docker build -q -f Dockerfile.entry-bad -t ce:entry-bad . > /dev/null
docker run -d --name entry-bad ce:entry-bad > /dev/null
sleep 1

echo "--- дерево процессов ---"
docker exec entry-bad ps -o pid,args

s="$(date +%s.%N)"; docker stop entry-bad > /dev/null; e="$(date +%s.%N)"
printf 'время stop: %.2f c, код: %s\n' \
    "$(awk -v a="$s" -v b="$e" 'BEGIN{printf "%.2f", b-a}')" \
    "$(docker inspect entry-bad --format '{{.State.ExitCode}}')"
echo "--- обработчик вызывался? ---"
docker logs entry-bad 2>&1 | grep -c 'получен SIGTERM' || echo "0 — не вызывался"
docker rm entry-bad > /dev/null
text
--- дерево процессов ---
PID   COMMAND
    1 /bin/sh /entrypoint.sh python -u /app.py
    7 python -u /app.py
   14 ps -o pid,args
время stop: 10.31 c, код: 137
--- обработчик вызывался? ---
0 — не вызывался

Без exec скрипт остался PID 1, приложение получило PID 7 и не увидело SIGTERM. Обработчик, который есть в коде, не сработал.

Отличие от корректного варианта — одно слово в последней строке скрипта.

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

bash
echo "=== обычный запуск ==="
docker run --rm ce:entry python -u -c "print('обычный')" 2>&1 | tail -1

echo "=== --entrypoint без явных аргументов ==="
docker run --rm --entrypoint ls ce:entry 2>&1 | head -3

echo "=== --entrypoint с явными аргументами ==="
docker run --rm --entrypoint ls ce:entry -la /app.py
text
=== обычный запуск ===
обычный
=== --entrypoint без явных аргументов ===
ls: python: No such file or directory
ls: /app.py: ... 
=== --entrypoint с явными аргументами ===
-rw-r--r-- 1 root root 331 Jul 30 11:40 /app.py

Второй случай показывает ловушку: --entrypoint заменил точку входа, но CMD остался. Получилось ls python -u /app.py --default-argls попытался вывести файл с именем python.

Правило: при использовании --entrypoint задавайте аргументы явно.

Для отладки образа с ENTRYPOINT:

bash
docker run --rm -it --entrypoint sh ce:entry -c 'echo "внутри образа"; ls /'
text
внутри образа
app.py  bin  boot  dev  entrypoint.sh  etc  home ...

Уборка

bash
cd /tmp
docker rmi -f $(docker images -q --filter 'reference=ce:*') 2>/dev/null || true
rm -rf /tmp/cmd-entry

Практическое упражнение

Задание. Постройте таблицу результирующих команд экспериментально и объясните каждую строку.

Напишите скрипт cmd-entrypoint-matrix.sh, который для восьми комбинаций ENTRYPOINT и CMD (наличие/отсутствие × exec/shell form) выводит:

  1. Что выполняется при docker run без аргументов.
  2. Что выполняется при docker run <image> ARG.
  3. Кто является PID 1.

Сверьте результат с таблицей из теоретической части и объясните две строки, где CMD игнорируется.

Подсказки

Подсказка 1

Чтобы увидеть результирующую команду, используйте образ, который печатает свои аргументы: CMD ["echo", ...] или скрипт с echo "$@".

Подсказка 2

PID 1 виден так:

bash
docker run --rm <image> ps -o args= -p 1

но это сработает только если CMD можно переопределить. Надёжнее — запустить в detached режиме и посмотреть через docker exec.

Подсказка 3

Фактические значения из конфигурации образа:

bash
docker image inspect <img> --format '{{json .Config.Entrypoint}} {{json .Config.Cmd}}'

Решение

Сначала выполните задание самостоятельно.

Показать решение
bash
#!/usr/bin/env bash
# cmd-entrypoint-matrix.sh — экспериментальная таблица CMD и ENTRYPOINT.
set -uo pipefail

WORK="$(mktemp -d)"
trap 'docker rmi -f $(docker images -q --filter "reference=matrix:*") >/dev/null 2>&1 || true;
      rm -rf "$WORK"' EXIT
cd "$WORK"

# Скрипт-зонд: печатает всё, что получил
cat > probe.sh <<'SH'
#!/bin/sh
echo "ВЫПОЛНЕНО: $0 $*"
SH
chmod +x probe.sh

build() {
    local tag="$1" entry="$2" cmd="$3"
    {
        echo "FROM alpine:3.21"
        echo "COPY --chmod=755 probe.sh /probe.sh"
        [ -n "$entry" ] && echo "$entry"
        [ -n "$cmd" ] && echo "$cmd"
    } > "Dockerfile.$tag"
    docker build -q -f "Dockerfile.$tag" -t "matrix:$tag" . > /dev/null 2>&1
}

run_case() {
    local tag="$1" label="$2"
    local no_arg with_arg
    no_arg="$(timeout 10 docker run --rm "matrix:$tag" 2>&1 | head -1)"
    with_arg="$(timeout 10 docker run --rm "matrix:$tag" ARG1 2>&1 | head -1)"
    printf '%-34s %-38s %s\n' "$label" "${no_arg:0:36}" "${with_arg:0:36}"
}

printf '%-34s %-38s %s\n' 'КОНФИГУРАЦИЯ' 'БЕЗ АРГУМЕНТОВ' 'С АРГУМЕНТОМ ARG1'
printf '%s\n' "---------------------------------------------------------------------------------------------------"

build c1 ''                              'CMD ["/probe.sh", "cmd-arg"]'
run_case c1 'CMD exec'

build c2 ''                              'CMD /probe.sh cmd-arg'
run_case c2 'CMD shell'

build c3 'ENTRYPOINT ["/probe.sh", "ep"]' ''
run_case c3 'ENTRYPOINT exec'

build c4 'ENTRYPOINT /probe.sh ep'        ''
run_case c4 'ENTRYPOINT shell'

build c5 'ENTRYPOINT ["/probe.sh", "ep"]' 'CMD ["cmd-arg"]'
run_case c5 'ENTRYPOINT exec + CMD exec'

build c6 'ENTRYPOINT ["/probe.sh", "ep"]' 'CMD cmd-arg'
run_case c6 'ENTRYPOINT exec + CMD shell'

build c7 'ENTRYPOINT /probe.sh ep'        'CMD ["cmd-arg"]'
run_case c7 'ENTRYPOINT shell + CMD exec'

build c8 'ENTRYPOINT /probe.sh ep'        'CMD /probe.sh cmd-arg'
run_case c8 'ENTRYPOINT shell + CMD shell'

echo
echo "═══ Конфигурация в образах ═══"
for t in c5 c7; do
    printf '%-4s Entrypoint=%s  Cmd=%s\n' "$t" \
        "$(docker image inspect "matrix:$t" --format '{{json .Config.Entrypoint}}')" \
        "$(docker image inspect "matrix:$t" --format '{{json .Config.Cmd}}')"
done

Ожидаемый вывод:

text
КОНФИГУРАЦИЯ                       БЕЗ АРГУМЕНТОВ                         С АРГУМЕНТОМ ARG1
---------------------------------------------------------------------------------------------------
CMD exec                           ВЫПОЛНЕНО: /probe.sh cmd-arg           ВЫПОЛНЕНО: ARG1
CMD shell                          ВЫПОЛНЕНО: /probe.sh cmd-arg           ВЫПОЛНЕНО: ARG1
ENTRYPOINT exec                    ВЫПОЛНЕНО: /probe.sh ep                ВЫПОЛНЕНО: /probe.sh ep ARG1
ENTRYPOINT shell                   ВЫПОЛНЕНО: /probe.sh ep                ВЫПОЛНЕНО: /probe.sh ep
ENTRYPOINT exec + CMD exec         ВЫПОЛНЕНО: /probe.sh ep cmd-arg        ВЫПОЛНЕНО: /probe.sh ep ARG1
ENTRYPOINT exec + CMD shell        ВЫПОЛНЕНО: /probe.sh ep /bin/sh -c ..  ВЫПОЛНЕНО: /probe.sh ep ARG1
ENTRYPOINT shell + CMD exec        ВЫПОЛНЕНО: /probe.sh ep                ВЫПОЛНЕНО: /probe.sh ep
ENTRYPOINT shell + CMD shell       ВЫПОЛНЕНО: /probe.sh ep                ВЫПОЛНЕНО: /probe.sh ep

═══ Конфигурация в образах ═══
c5   Entrypoint=["/probe.sh","ep"]  Cmd=["cmd-arg"]
c7   Entrypoint=["/bin/sh","-c","/probe.sh ep"]  Cmd=["cmd-arg"]

Разбор двух строк, где CMD игнорируется (c7 и c8).

Посмотрите на конфигурацию c7: Entrypoint превратился в ["/bin/sh", "-c", "/probe.sh ep"]. Docker формирует итоговую команду конкатенацией Entrypoint + Cmd, получая:

text
/bin/sh -c "/probe.sh ep" "cmd-arg"

Оболочка, вызванная с -c, интерпретирует первый аргумент как команду, а остальные становятся позиционными параметрами: $0 = cmd-arg. Поскольку команда /probe.sh ep их не использует, они просто теряются.

То же происходит с аргументами docker run: они попадают в ту же позицию и так же игнорируются.

Практическое следствие. Образ с ENTRYPOINT в shell form нельзя параметризовать: ни CMD, ни аргументы командной строки до приложения не доходят. Ошибка молчаливая — container запускается и работает, просто игнорирует конфигурацию.

Проверка чужого образа на эту проблему:

bash
docker image inspect <image> --format '{{json .Config.Entrypoint}}'

Значение вида ["/bin/sh","-c",...] означает shell form и потерю параметров.

Строка ENTRYPOINT exec + CMD shell (c6) тоже поучительна: CMD в shell form превратился в ["/bin/sh","-c","cmd-arg"], и эти три элемента дописались к ENTRYPOINT как обычные аргументы. Результат — бессмысленная команда. Правило: при наличии ENTRYPOINT записывайте CMD только в exec form.

Проверка результата

bash
mkdir -p /tmp/vc && cd /tmp/vc
printf 'FROM alpine:3.21\nENTRYPOINT ["echo", "EP:"]\nCMD ["default"]\n' > Dockerfile
docker build -q -t vc:1 . > /dev/null
echo -n "без аргументов: "; docker run --rm vc:1
echo -n "с аргументом:   "; docker run --rm vc:1 custom
docker rmi -f vc:1 > /dev/null; cd /tmp && rm -rf /tmp/vc

Ожидается EP: default и EP: custom. Объясните, почему EP: остался в обоих случаях.

Типичные ошибки

ОшибкаПричинаИсправление
CMD в shell formКороче писатьPID 1 становится sh, SIGTERM не доходит. Использовать exec form
ENTRYPOINT в shell formТо жеCMD и аргументы игнорируются полностью
Нет exec в entrypoint-скриптеНе очевидноСкрипт остаётся PID 1; та же проблема с сигналами
"$@" без кавычекРаботает в простых случаяхАргумент с пробелами разобьётся на несколько
Нет set -e в entrypoint-скриптеЗабываютОшибка подготовки не остановит запуск приложения
Ожидание подстановки переменных в exec formВыглядит одинаковоExec form не вызывает оболочку; использовать CMD exec cmd $VAR
CMD в shell form при заданном ENTRYPOINTСмешение формCMD превращается в три аргумента /bin/sh -c ...
--entrypoint без явных аргументовКажется, что CMD сбрасываетсяCMD остаётся и дописывается; задавать аргументы явно
Не задан CMD, унаследован от базового образаНеявное поведениеЗадавать явно; проверять docker image inspect
ENTRYPOINT в образе общего назначенияКажется правильнымУсложняет отладку: нужен --entrypoint sh

Контрольные вопросы

На понимание:

  1. Чем CMD отличается от ENTRYPOINT по реакции на аргументы docker run?
  2. Почему при ENTRYPOINT в shell form инструкция CMD игнорируется?
  3. Почему exec form не подставляет переменные окружения?
  4. Зачем в entrypoint-скрипте нужен exec в последней строке?
  5. Что произойдёт, если задать ENTRYPOINT, но не задать CMD, при наличии CMD в базовом образе?

На применение:

  1. Как сохранить подстановку переменных и при этом оставить приложение в PID 1?
  2. Как запустить оболочку в образе, где задан ENTRYPOINT?
  3. Как проверить, что чужой образ не потеряет переданные ему аргументы?

На диагностику:

  1. Аргументы, переданные в docker run, никак не влияют на поведение container. Что проверить в первую очередь?
  2. Приложение имеет обработчик SIGTERM, но docker stop занимает 10 секунд. Entrypoint-скрипт присутствует. Где искать причину?

Краткое резюме

  1. Аргументы docker run заменяют CMD и добавляются к ENTRYPOINT.
  2. ENTRYPOINT задаёт «что это за программа», CMD — «с какими аргументами по умолчанию».
  3. Exec form запускает приложение напрямую; shell form оборачивает в /bin/sh -c.
  4. При shell form PID 1 — это sh, и SIGTERM до приложения не доходит.
  5. При ENTRYPOINT в shell form CMD и аргументы командной строки игнорируются полностью.
  6. Exec form не подставляет переменные окружения; для подстановки — CMD exec cmd $VAR.
  7. Entrypoint-скрипт обязан заканчиваться exec "$@" и начинаться с set -e.
  8. При наличии ENTRYPOINT записывайте CMD только в exec form.
  9. ENTRYPOINT сбрасывает CMD, унаследованный от базового образа.
  10. --entrypoint заменяет точку входа, но не сбрасывает CMD — задавайте аргументы явно.

Официальные источники

ИсточникСсылкаЧто подтверждает
Dockerfile reference: CMDhttps://docs.docker.com/reference/dockerfile/#cmdExec form и shell form, переопределение аргументами docker run
Dockerfile reference: ENTRYPOINThttps://docs.docker.com/reference/dockerfile/#entrypointТаблица взаимодействия с CMD, поведение shell form
Dockerfile reference: shell and exec formhttps://docs.docker.com/reference/dockerfile/#shell-and-exec-formПреобразование shell form в /bin/sh -c, отсутствие подстановки в exec form
docker run referencehttps://docs.docker.com/reference/cli/docker/container/run/Флаг --entrypoint, переопределение команды
Building best practiceshttps://docs.docker.com/build/building/best-practices/Рекомендации по CMD и ENTRYPOINT, паттерн entrypoint-скрипта
docker image inspecthttps://docs.docker.com/reference/cli/docker/image/inspect/Поля Config.Entrypoint и Config.Cmd
sh(1) man pagehttps://man7.org/linux/man-pages/man1/sh.1p.htmlПоведение -c: первый аргумент как команда, остальные как позиционные параметры

Навигация

← Предыдущий материал
Вернуться к разделу
Следующий материал → Build cache
Главное оглавление

Markdown на GitHub ↗