🐳 Раздел 14 · Вопрос #5

В чём разница между CMD и ENTRYPOINT

Обе инструкции определяют команду, которая выполняется при запуске контейнера (docker run), но у них принципиально разные роли в жизненном цикле:


🟢 Junior Level

30-секундный ответ

Обе инструкции определяют команду, которая выполняется при запуске контейнера (docker run), но у них принципиально разные роли в жизненном цикле:

  • ENTRYPOINT — задаёт основную исполняемую программу контейнера (неизменяемую точку входа). Она превращает контейнер в самостоятельный исполняемый бинарник (например, java).
  • CMD — задаёт аргументы по умолчанию для программы из ENTRYPOINT. Их можно мгновенно переопределить при вызове docker run.

Наглядная аналогия из программирования

Представьте вызов функции в Java:

java   -jar app.jar   --server.port=8080
▲      ▲              ▲
│      │              │
[  ENTRYPOINT  ]      [     CMD        ]
(Имя функции)         (Дефолтные аргументы)

Как они работают в связке (Best Practice)

# Фиксированная команда:
ENTRYPOINT ["java", "-jar", "app.jar"]

# Параметры по умолчанию:
CMD ["--server.port=8080"]
  1. Запуск по умолчанию:
    docker run my-app
    # Итог: java -jar app.jar --server.port=8080
    
  2. Переопределение порта при запуске:
    docker run my-app --server.port=9090
    # Итог: java -jar app.jar --server.port=9090 (CMD полностью заменён!)
    

🟡 Middle Level

Две формы записи: Exec Form vs Shell Form

Критерий Exec Form (Строго рекомендуется) Shell Form (Антипаттерн)
Синтаксис ENTRYPOINT ["java", "-jar", "app.jar"] ENTRYPOINT java -jar app.jar
Как запускается Напрямую через системный вызов execve() Через командную оболочку /bin/sh -c "..."
Номер процесса Процесс приложения получает PID 1 Процесс оболочки sh получает PID 1, приложение — PID 2
Сигналы ОС SIGTERM перехватывается Java-приложением Оболочка игнорирует SIGTERM, приложение убивается по SIGKILL
Graceful Shutdown 🟢 Работает корректно ❌ Сломан (задержка 10–30 с и краш)

Таблица взаимодействия ENTRYPOINT и CMD

В Dockerfile объявлено Вызов docker run Итоговая команда в Linux
CMD ["java", "-jar", "app.jar"] docker run my-app java -jar app.jar
CMD ["java", "-jar", "app.jar"] docker run my-app bash bash (CMD полностью заменён)
ENTRYPOINT ["java", "-jar", "app.jar"] docker run my-app java -jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"] docker run my-app --debug java -jar app.jar --debug
ENTRYPOINT ["java"] + CMD ["-version"] docker run my-app java -version
ENTRYPOINT ["java"] + CMD ["-version"] docker run my-app -jar x.jar java -jar x.jar

Маппинг на спецификацию Kubernetes Pod

В Kubernetes названия директив Docker были переименованы, что часто сбивает с толку разработчиков:

В манифесте Kubernetes Pod Соответствующая инструкция в Dockerfile
command ENTRYPOINT
args CMD
apiVersion: v1
kind: Pod
metadata:
  name: payment-service
spec:
  containers:
  - name: app
    image: payment-service:1.0
    command: ["java"]             # Полностью переопределяет ENTRYPOINT образа!
    args: ["-jar", "/app.jar", "--spring.profiles.active=k8s"] # Переопределяет CMD!

[!WARNING] Если в Kubernetes указано поле command, оно полностью аннулирует и ENTRYPOINT, и CMD из оригинального Dockerfile (если не указано поле args).


🔴 Senior Level

Проблема PID 1 и обработка сигналов ядра Linux

В операционной системе Linux процесс с идентификатором PID 1 наделён уникальными свойствами:

  1. Иммунитет к сигналам: ядро Linux не применяет стандартные действия сигналов по умолчанию (default actions) к процессу PID 1. Если процесс явно не зарегистрировал собственный обработчик (Signal Handler) для SIGTERM или SIGINT, ядро просто отбрасывает этот сигнал.
  2. Почему Shell Form ломает graceful shutdown:
    • При ENTRYPOINT java -jar app.jar процессом PID 1 становится /bin/sh.
    • При остановке пода в Kubernetes (kubectl delete pod) kubelet посылает контейнеру сигнал SIGTERM.
    • Оболочка /bin/sh получает сигнал, но не пробрасывает его дочернему процессу Java (PID 2).
    • JVM продолжает работать, не зная об остановке. Spring Boot контекст не закрывается, транзакции зависают.
    • По истечении terminationGracePeriodSeconds (по умолчанию 30 секунд) Kubernetes посылает принудительный сигнал SIGKILL (код 137), обрывая активные соединения и повреждая данные.

Паттерн «Entrypoint Wrapper Script» и системный вызов exec

В корпоративных приложениях перед запуском JVM часто требуется выполнить предпусковые действия (скачать секреты из HashiCorp Vault, применить миграции БД Flyway/Liquibase). Для этого используют shell-скрипт:

COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
CMD ["java", "-jar", "app.jar"]
#!/bin/sh
# entrypoint.sh

echo "Подготовка окружения и скачивание сертификатов..."

# ⚠️ КРИТИЧЕСКИ ВАЖНО: запуск через команду exec!
exec "$@"

Почему системный вызов exec "$@" обязателен?

Команда POSIX exec выполняет системный вызов execve(2):

  • Она заменяет текущий процесс shell-скрипта целевой программой (java), сохраняя тот же самый PID 1!
  • Если написать просто "$@", скрипт останется висеть как PID 1, породив Java как дочерний процесс, что вернёт проблему потери сигналов SIGTERM.

Утилизация зомби-процессов (Zombie Reaping) и Tini

Если Java-приложение порождает внешние системные процессы (через ProcessBuilder или вызовы shell):

  • При завершении дочерний процесс превращается в процесс-зомби (Zombie / Defunct Process), ожидая, пока родитель прочитает его код возврата через waitpid().
  • Если родитель завершился раньше, осиротевший зомби переподчиняется процессу с PID 1.
  • Если приложение само является PID 1, но не содержит логики вызова wait(), зомби-процессы накапливаются в таблице процессов ядра, приводя к исчерпанию лимита PID (PID Exhaustion).
  • Решение в контейнерах: использование легковесного init-демона Tini:
    ENTRYPOINT ["/usr/bin/tini", "--", "java", "-jar", "app.jar"]
    # Или запуск контейнера с флагом: docker run --init
    

4 Tricky Questions

1. Если в манифесте Kubernetes указать только command: ["java"], но не указывать поле args, что произойдёт со значениями CMD из исходного Dockerfile?

Ответ: Значения CMD из Dockerfile будут полностью проигнорированы! В спецификации контейнеров Kubernetes действует строгое правило: если вы объявляете поле command, оно заменяет Docker ENTRYPOINT, а массив аргументов args по умолчанию становится пустым ([]), полностью стирая любые дефолтные параметры CMD, прописанные создателем образа. Контейнер попытается выполнить команду java без аргументов, выведет справку по ключам запуска и мгновенно завершит работу с кодом 1 или 0 (статус CrashLoopBackOff).


2. Что произойдёт, если ENTRYPOINT написан в Shell Form, а CMD — в Exec Form? Передадутся ли аргументы из CMD в программу?

Ответ: Нет, аргументы из CMD будут полностью проигнорированы. Если ENTRYPOINT объявлен в Shell Form:

ENTRYPOINT java -jar app.jar
CMD ["--server.port=8080"]

Docker компилирует эту строчку как /bin/sh -c "java -jar app.jar". Спецификация POSIX shell такова, что аргументы, переданные после команды -c "...", интерпретируются как параметры самого шелла ($0, $1), а не команды внутри кавычек. Поэтому любые параметры из CMD или аргументы командной строки docker run будут проигнорированы, и приложение запустится без аргументов.


3. Почему в entrypoint-скрипте на Bash конструкция exec "$@" пишется именно в двойных кавычках с собачкой, а не exec $*?

Ответ: Разница заключается в обработке пробелов в аргументах:

  • $* (без кавычек): разбивает все аргументы по пробелам (Word Splitting). Если вы передадите параметр с пробелом (например, JVM-опцию -Dapp.name="Payment Service"), она разорвётся на два отдельных некорректных аргумента -Dapp.name="Payment и Service".
  • "$@" (в кавычках): специальная конструкция языка Bash, которая сохраняет каждый аргумент точно в том виде, в котором он был передан, гарантируя целостность аргументов с пробелами и спецсимволами. Команда exec подменяет текущий процесс шелла целевым процессом, сохраняя PID 1.

4. Можно ли передать флаги JVM (например, -Xmx1g) через CMD, если ENTRYPOINT равен ["java", "-jar", "app.jar"]?

Ответ: Нет, это приведёт к ошибке запуска! Синтаксис команды java строго регламентирован: флаги JVM (такие как -Xmx, -Dprop=val) должны располагаться ДО флага -jar: java [JVM-flags] -jar app.jar [app-args]. Если ENTRYPOINT зафиксирован как ["java", "-jar", "app.jar"], то любые значения из CMD добавятся в самый конец: java -jar app.jar -Xmx1g. В этом случае JVM интерпретирует -Xmx1g не как настройку памяти, а как обычный строковый аргумент метода main(String[] args) Spring Boot приложения! Правильное решение: выносить флаги JVM в переменную окружения JAVA_TOOL_OPTIONS или параметризовать ENTRYPOINT ["java"] с CMD ["-jar", "app.jar"].


🎯 Шпаргалка для интервью

Различия между CMD и ENTRYPOINT

| Характеристика | ENTRYPOINT | CMD | | :— | :— | :— | | Основная роль | Неизменяемая точка входа (бинарник) | Дефолтные параметры/аргументы | | Переопределение в CLI | Сложно (--entrypoint /bin/sh) | Легко (docker run my-app arg1) | | Маппинг в Kubernetes | command | args | | Рекомендуемая форма | Exec Form ["binary"] | Exec Form ["param1", "param2"] | | Поведение сигналов | Приложение получает PID 1 и SIGTERM | Зависит от формы записи |

Золотые правила для Production

  1. Всегда используйте Exec Form: только ["java", "-jar", "app.jar"], никаких строковых shell-форм.
  2. Используйте связку ENTRYPOINT + CMD: ENTRYPOINT фиксирует команду запуска, CMD даёт дефолтные флаги, которые можно переопределить.
  3. В wrapper-скриптах всегда используйте exec "$@": для сохранения PID 1 и доставки сигналов остановки.
  4. Не передавайте JVM-флаги памяти в CMD: используйте стандартную переменную окружения JAVA_TOOL_OPTIONS.

Красные флаги на собеседовании (Чего говорить нельзя)

  • ❌ «ENTRYPOINT и CMD — это синонимы, можно использовать любой» — у них принципиально разная семантика объединения и переопределения.
  • ❌ «Shell Form безопасна и ничем не отличается от Exec Form» — shell-форма ломает graceful shutdown, контейнер всегда завершается аварийно по timeout и SIGKILL.
  • ❌ «В Kubernetes поле command дополняет ENTRYPOINT из Dockerfile» — поле command в K8s полностью стирает и замещает обе инструкции Dockerfile.

Связанные вопросы