🐳 Розділ 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.

Пов’язані питання