В чому різниця між 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"]
- Запуск за замовчуванням:
docker run my-app # Підсумок: java -jar app.jar --server.port=8080 - Перевизначення порту при запуску:
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 наділений унікальними властивостями:
- Імунітет до сигналів: ядро Linux не застосовує стандартні дії сигналів за замовчуванням (
default actions) до процесу PID 1. Якщо процес явно не зареєстрував власний обробник (Signal Handler) дляSIGTERMабоSIGINT, ядро просто відкидає цей сигнал. - Чому 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
- Завжди використовуйте Exec Form: тільки
["java", "-jar", "app.jar"], жодних рядкових shell-форм. - Використовуйте зв’язку ENTRYPOINT + CMD: ENTRYPOINT фіксує команду запуску, CMD дає дефолтні прапорці, які можна перевизначити.
- У wrapper-скриптах завжди використовуйте
exec "$@": для збереження PID 1 та доставки сигналів зупинки. - Не передавайте JVM-прапорці пам’яті в
CMD: використовуйте стандартну змінну оточенняJAVA_TOOL_OPTIONS.
Червоні прапорці на співбесіді (Чого говорити не можна)
- ❌ «ENTRYPOINT та CMD — це синоніми, можна використовувати будь-який» — у них принципово різна семантика об’єднання та перевизначення.
- ❌ «Shell Form безпечна і нічим не відрізняється від Exec Form» — shell-форма ламає graceful shutdown, контейнер завжди завершується аварійно за timeout та SIGKILL.
- ❌ «У Kubernetes поле command доповнює ENTRYPOINT із Dockerfile» — поле
commandв K8s повністю стирає та заміщує обидві інструкції Dockerfile.