В чём разница между 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.