5.4. CMD и ENTRYPOINT
Цели
После этого материала вы сможете:
- предсказать результирующую команду для любой комбинации
CMDиENTRYPOINT; - объяснить различие exec form и shell form и его последствия для сигналов;
- выбрать между
CMD,ENTRYPOINTи их комбинацией под конкретную задачу; - написать entrypoint-скрипт с корректным
exec "$@"; - объяснить, что переопределяется при
docker runи что нет; - находить в чужих образах ошибки, из-за которых аргументы не доходят до приложения.
Предварительные знания
- 5.2. Базовые инструкции;
- 4.4. Сигналы и graceful shutdown — почему PID 1 важен;
- 4.2. Режимы запуска — стандартные потоки.
Ключевые термины
| Термин | Объяснение |
|---|---|
exec form | Запись массивом JSON: ["executable", "arg"] |
shell form | Запись строкой: executable arg |
результирующая команда | То, что фактически выполняется при запуске container |
entrypoint-скрипт | Скрипт-обёртка, выполняющий подготовку перед запуском приложения |
переопределение | Замена значения из образа аргументами docker run |
Теория
Две инструкции, одна задача
Обе задают, что выполнится при запуске container, но с разным смыслом:
ENTRYPOINT | CMD | |
|---|---|---|
| Смысл | Что это за программа | С какими аргументами по умолчанию |
Переопределяется аргументами docker run | нет | да |
| Переопределяется флагом | --entrypoint | не требуется |
| Типичное применение | Фиксированная точка входа | Аргументы, которые захотят менять |
Ключевое различие в одной строке: аргументы docker run заменяют CMD, но добавляются к ENTRYPOINT.
Таблица результирующих команд
Это главная таблица урока. Она полностью описывает поведение.
ENTRYPOINT | CMD | docker run без аргументов | docker run img arg1 |
|---|---|---|---|
| — | — | ошибка: нет команды | arg1 |
| — | ["echo", "hi"] | echo hi | arg1 |
| — | echo hi (shell) | /bin/sh -c "echo hi" | arg1 |
["echo"] | — | echo | echo arg1 |
["echo"] | ["hi"] | echo hi | echo arg1 |
["echo"] | hi (shell) | echo /bin/sh -c hi | echo 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 form | shell 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 вообще нужен:
CMD ["python", "-m", "$MODULE"] # запустит модуль с именем "$MODULE"
CMD python -m $MODULE # подставит значение переменной
Когда нужна подстановка, используйте shell form с явным exec:
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-скрипта
Классическая структура:
#!/bin/sh
set -e
# 1. Подготовка: ожидание зависимостей, миграции, генерация конфигурации
wait_for_database
apply_migrations
# 2. Передача управления приложению
exec "$@"
Три обязательных элемента:
set -e — прервать скрипт при ошибке любой команды. Без него подготовка может провалиться, а приложение всё равно запустится.
"$@" в кавычках — передать все аргументы, сохранив их границы. Без кавычек аргумент с пробелами разобьётся на несколько.
exec — заменить процесс скрипта процессом приложения. Без него оболочка останется PID 1, и сигналы не дойдут до приложения — та же проблема, что и при shell form.
Что переопределяется при запуске
docker run <image> [аргументы] # заменяют CMD
docker run --entrypoint <cmd> <image> # заменяет ENTRYPOINT
Тонкость: --entrypoint заменяет точку входа, но не сбрасывает CMD. Поэтому
docker run --entrypoint ls myimage
выполнит ls с аргументами из CMD, что часто даёт неожиданный результат. Чтобы этого избежать, задавайте аргументы явно:
docker run --entrypoint ls myimage -la /app
Внутренний механизм
Где хранятся значения
Обе инструкции записываются в конфигурацию образа (урок 3.1):
{
"config": {
"Entrypoint": ["/entrypoint.sh"],
"Cmd": ["python", "app.py"]
}
}
При запуске Docker формирует итоговую команду конкатенацией: Entrypoint + Cmd, где Cmd замещается аргументами docker run, если они переданы.
Shell form превращается в exec form ещё на этапе сборки:
CMD python app.py
↓
"Cmd": ["/bin/sh", "-c", "python app.py"]
Это видно в docker image inspect и объясняет, почему sh оказывается PID 1: он там записан явно.
Наследование от базового образа
Если стадия не задаёт ENTRYPOINT или CMD, они наследуются от базового образа. Отсюда неочевидное поведение:
FROM python:3.13-slim
# CMD не задан — наследуется ["python3"] из базового образа
Такой образ при запуске откроет интерпретатор Python, а не ваше приложение.
Важное правило: инструкция ENTRYPOINT сбрасывает унаследованный CMD. Если вы задаёте ENTRYPOINT, но не задаёте CMD, унаследованный CMD не будет использован — он обнуляется.
Команды и примеры
Подготовка
mkdir -p /tmp/cmd-entry && cd /tmp/cmd-entry
Проверка таблицы результирующих команд
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"]
'
=== Только 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
echo "=== ENTRYPOINT в shell form ==="
build_and_run ent-shell 'FROM alpine:3.21
ENTRYPOINT echo "ENTRYPOINT-shell:"
CMD ["значение-из-CMD"]
'
=== ENTRYPOINT в shell form ===
без аргументов: ENTRYPOINT-shell:
с аргументом: ENTRYPOINT-shell:
CMD проигнорирован. Аргумент проигнорирован. Приложение не получило ничего.
Посмотрим конфигурацию:
docker image inspect ce:ent-shell --format 'Entrypoint: {{json .Config.Entrypoint}}'
docker image inspect ce:ent-shell --format 'Cmd: {{json .Config.Cmd}}'
Entrypoint: ["/bin/sh","-c","echo \"ENTRYPOINT-shell:\""]
Cmd: ["значение-из-CMD"]
CMD в конфигурации есть, но /bin/sh -c принимает только первый аргумент как команду, а остальные становятся позиционными параметрами $0, $1 — и просто не используются.
Это молчаливая ошибка: сборка проходит, container запускается, а параметры теряются.
exec form против shell form: дерево процессов
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
exec PID 1: sleep 300
shell PID 1: /bin/sh -c sleep 300
Последствия для остановки:
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
exec время stop: 0.31 c, код: 0
shell время stop: 10.28 c, код: 137
Десять секунд разницы из-за формы записи одной строки.
Подстановка переменных
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
exec -> $GREETING
shell -> привет из ENV
fixed -> привет из ENV
Exec form не выполняет подстановку — строка передана буквально. Shell form подставляет. Вариант fixed подставляет и оставляет приложение в PID 1.
Проверим последнее:
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
sleep 300
Оболочки нет: exec её заменил. Мы получили и подстановку, и корректную доставку сигналов.
Наследование от базового образа
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}}'
Cmd: ["python3"]
база Cmd: ["python3"]
Образ унаследовал CMD от базового. Запуск откроет интерпретатор, а не приложение — распространённая причина «container сразу завершается» (урок 4.1).
ENTRYPOINT сбрасывает унаследованный CMD:
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}}'
Entrypoint: ["python","-c"] Cmd: null
Унаследованный ["python3"] обнулён. Это правильное поведение — иначе результирующая команда была бы python -c python3.
Entrypoint-скрипт
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
=== запуск с 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 — скрипта в памяти нет.
Проверим сигналы:
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
время stop: 0.38 c, код: 0
[app] получен SIGTERM, завершаюсь
Переопределение CMD при запуске:
docker run --rm -e APP_MODE=production ce:entry \
python -u /app.py --custom-arg --verbose 2>&1 | tail -2
[entrypoint] передаю управление: python -u /app.py --custom-arg --verbose
[app] запущен, PID=1, аргументы=['--custom-arg', '--verbose']
Подготовка выполнилась, аргументы заменились. Это и есть смысл разделения: ENTRYPOINT фиксирует процедуру, CMD задаёт изменяемую часть.
Что будет без exec
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
--- дерево процессов ---
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
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
=== обычный запуск ===
обычный
=== --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-arg — ls попытался вывести файл с именем python.
Правило: при использовании --entrypoint задавайте аргументы явно.
Для отладки образа с ENTRYPOINT:
docker run --rm -it --entrypoint sh ce:entry -c 'echo "внутри образа"; ls /'
внутри образа
app.py bin boot dev entrypoint.sh etc home ...
Уборка
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) выводит:
- Что выполняется при
docker runбез аргументов. - Что выполняется при
docker run <image> ARG. - Кто является PID 1.
Сверьте результат с таблицей из теоретической части и объясните две строки, где CMD игнорируется.
Подсказки
Подсказка 1
Чтобы увидеть результирующую команду, используйте образ, который печатает свои аргументы: CMD ["echo", ...] или скрипт с echo "$@".
Подсказка 2
PID 1 виден так:
docker run --rm <image> ps -o args= -p 1
но это сработает только если CMD можно переопределить. Надёжнее — запустить в detached режиме и посмотреть через docker exec.
Подсказка 3
Фактические значения из конфигурации образа:
docker image inspect <img> --format '{{json .Config.Entrypoint}} {{json .Config.Cmd}}'
Решение
Сначала выполните задание самостоятельно.
Показать решение
#!/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
Ожидаемый вывод:
КОНФИГУРАЦИЯ БЕЗ АРГУМЕНТОВ С АРГУМЕНТОМ 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, получая:
/bin/sh -c "/probe.sh ep" "cmd-arg"
Оболочка, вызванная с -c, интерпретирует первый аргумент как команду, а остальные становятся позиционными параметрами: $0 = cmd-arg. Поскольку команда /probe.sh ep их не использует, они просто теряются.
То же происходит с аргументами docker run: они попадают в ту же позицию и так же игнорируются.
Практическое следствие. Образ с ENTRYPOINT в shell form нельзя параметризовать: ни CMD, ни аргументы командной строки до приложения не доходят. Ошибка молчаливая — container запускается и работает, просто игнорирует конфигурацию.
Проверка чужого образа на эту проблему:
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.
Проверка результата
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 |
Контрольные вопросы
На понимание:
- Чем
CMDотличается отENTRYPOINTпо реакции на аргументыdocker run? - Почему при
ENTRYPOINTв shell form инструкцияCMDигнорируется? - Почему exec form не подставляет переменные окружения?
- Зачем в entrypoint-скрипте нужен
execв последней строке? - Что произойдёт, если задать
ENTRYPOINT, но не задатьCMD, при наличииCMDв базовом образе?
На применение:
- Как сохранить подстановку переменных и при этом оставить приложение в PID 1?
- Как запустить оболочку в образе, где задан
ENTRYPOINT? - Как проверить, что чужой образ не потеряет переданные ему аргументы?
На диагностику:
- Аргументы, переданные в
docker run, никак не влияют на поведение container. Что проверить в первую очередь? - Приложение имеет обработчик
SIGTERM, ноdocker stopзанимает 10 секунд. Entrypoint-скрипт присутствует. Где искать причину?
Краткое резюме
- Аргументы
docker runзаменяютCMDи добавляются кENTRYPOINT. ENTRYPOINTзадаёт «что это за программа»,CMD— «с какими аргументами по умолчанию».- Exec form запускает приложение напрямую; shell form оборачивает в
/bin/sh -c. - При shell form PID 1 — это
sh, иSIGTERMдо приложения не доходит. - При
ENTRYPOINTв shell formCMDи аргументы командной строки игнорируются полностью. - Exec form не подставляет переменные окружения; для подстановки —
CMD exec cmd $VAR. - Entrypoint-скрипт обязан заканчиваться
exec "$@"и начинаться сset -e. - При наличии
ENTRYPOINTзаписывайтеCMDтолько в exec form. ENTRYPOINTсбрасываетCMD, унаследованный от базового образа.--entrypointзаменяет точку входа, но не сбрасываетCMD— задавайте аргументы явно.
Официальные источники
| Источник | Ссылка | Что подтверждает |
|---|---|---|
| Dockerfile reference: CMD | https://docs.docker.com/reference/dockerfile/#cmd | Exec form и shell form, переопределение аргументами docker run |
| Dockerfile reference: ENTRYPOINT | https://docs.docker.com/reference/dockerfile/#entrypoint | Таблица взаимодействия с CMD, поведение shell form |
| Dockerfile reference: shell and exec form | https://docs.docker.com/reference/dockerfile/#shell-and-exec-form | Преобразование shell form в /bin/sh -c, отсутствие подстановки в exec form |
| docker run reference | https://docs.docker.com/reference/cli/docker/container/run/ | Флаг --entrypoint, переопределение команды |
| Building best practices | https://docs.docker.com/build/building/best-practices/ | Рекомендации по CMD и ENTRYPOINT, паттерн entrypoint-скрипта |
| docker image inspect | https://docs.docker.com/reference/cli/docker/image/inspect/ | Поля Config.Entrypoint и Config.Cmd |
sh(1) man page | https://man7.org/linux/man-pages/man1/sh.1p.html | Поведение -c: первый аргумент как команда, остальные как позиционные параметры |
Навигация
← Предыдущий материал
Вернуться к разделу
Следующий материал → Build cache
Главное оглавление