ENTRYPOINT і CMD

Docker for Spring
Рівень 4 , Лекція 1
Відкрита

1. Контейнер = процес: команда й параметри

Частина Dockerfile, повʼязана зі збиранням, уже на місці: база, робочий каталог, jar усередині образу, базові змінні середовища й очікуваний порт. Але сам по собі jar контейнер ще не оживляє. Контейнер живе рівно доти, доки працює його головний процес, тож тепер треба розібратися, якою командою ми запускаємо сервіс і що вважати параметрами за замовчуванням.

Коли ви вперше запускаєте контейнер, легко думати: «Це така коробочка, всередині якої живе застосунок». Але Docker влаштований простіше й менш романтично: контейнер — це спосіб запустити процес в ізольованому середовищі. Тому запитання «якою командою стартуємо?» і «які в неї параметри за замовчуванням?» — не придирка до стилю Dockerfile, а частина інженерного контролю над запуском.

Давайте переведемо цю думку на мову нашої реальності. Наш Spring Boot-сервіс у підсумку стартує командою рівня:

# Базовий запуск Spring Boot-застосунку як JAR
java -jar app.jar

І тут починаються важливі нюанси. Команда java -jar app.jar — це «скелет» запуску, який зазвичай не змінюється щодня. Нам не хочеться, щоб хтось випадково замінив його чимось іншим одним необережним рухом у docker run. А параметри, які іноді треба змінювати — порт, профіль, можливо, якісь прості прапорці застосунку, — це вже «мʼясо» запуску. Їх якраз зручно переозначувати.

Щоб не плутатися, корисно памʼятати, що Docker CLI сам підказує формат:

docker run [OPTIONS] IMAGE [COMMAND] [ARG...]

Частина після IMAGE — саме те місце, де люди найчастіше випадково ламають собі життя. Бо COMMAND і ARG... дуже легко сприймати як аргументи застосунку, а Docker іноді сприймає це як повну заміну команди запуску.

Якщо зовсім по-побутовому (і так, аналогія навмисно трохи безглузда): ENTRYPOINT — це «що взагалі готуємо: суп», а CMD — «скільки солі за замовчуванням». А ось docker run ... після імені образу — це момент, коли хтось на кухні кричить: «Я не люблю сіль!» І ви змінюєте саме те, що задумали… або випадково викидаєте суп і починаєте смажити цвяхи. Наша мета — зробити так, щоб другий сценарій траплявся якомога рідше.

2. ENTRYPOINT: стабільна команда запуску

Якщо ви робите образ для сервісу, а не для «контейнера на один запуск», вам майже завжди потрібен стабільний спосіб старту. Це і є зона відповідальності ENTRYPOINT. Він відповідає на запитання: «Який головний процес має стартувати, коли контейнер запускають?» Для Spring Boot-застосунку відповідь зазвичай максимально нудна, і це добре: java -jar ....

Повʼяжемо це з Java-кодом, щоб не здавалося, ніби Docker — окремий всесвіт. У Spring Boot-сервісу є main(), який запускається JVM і приймає аргументи командного рядка:

package com.example.catalog;

// Головна точка входу Spring Boot-застосунку
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication // Увімкнемо автоконфігурацію та сканування компонентів
public class CatalogApplication {
  public static void main(String[] args) {
    // args — це ті самі аргументи, які ви передаєте після app.jar у командному рядку
    SpringApplication.run(CatalogApplication.class, args);
  }
}

Усі параметри, які ви передасте після app.jar у командному рядку, потраплять у args і будуть оброблені Spring Boot. Тобто запуск контейнера — це буквально спосіб сформувати командний рядок, з якого стартує JVM.

Найчитабельніший запис для сервісного образу виглядає так: ми використовуємо масив, тому що для сервісів це найпередбачуваніший спосіб запускати процес.

FROM eclipse-temurin:25-jre-jammy
WORKDIR /opt/app

# Кладемо зібраний артефакт усередину образу під фіксованим іменем
COPY build/libs/*.jar app.jar

# Фіксуємо стабільну команду запуску сервісу (те, що не хочеться випадково “переписати” під час docker run)
ENTRYPOINT ["java", "-jar", "app.jar"]

Тут є важлива психологічна штука: ENTRYPOINT робить образ виконуваним. Зʼявляється відчуття, що image — це вже не «контейнер, усередині якого щось лежить», а застосунок, який можна запустити. І це відчуття правильне: docker run docker-java-catalog-service починає читатися як «запусти сервіс», а не як «запусти контейнер і потім розбирайся, що там усередині».

З точки зору щоденної дисципліни це дуже допомагає: у команди зʼявляється єдиний, очікуваний спосіб запуску. І ось тут ENTRYPOINT справді нас страхує: навіть якщо хтось почне експериментувати з параметрами, викинути java -jar app.jar із запуску стає набагато важче.

3. CMD: параметри за замовчуванням

Якщо ENTRYPOINT відповідає за «що запускаємо», то CMD найчастіше відповідає за «з якими параметрами запускаємо за замовчуванням». І ця ідея особливо корисна саме в сервісних образах: ви хочете, щоб docker run <image> просто працював, але при цьому дозволяв легко замінити якісь параметри без переписування Dockerfile.

У Dockerfile CMD теж можна записувати як масив. І в поєднанні з ENTRYPOINT це читається майже як речення: «Запускай Java-застосунок, а за замовчуванням передай йому ось такі аргументи».

Приклад базової ідеї (зараз нас цікавить саме механіка запуску):

# Команда запуску — стабільна
ENTRYPOINT ["java", "-jar", "app.jar"]

# Аргументи за замовчуванням — легко переозначуються під час docker run
CMD ["--server.port=8080"]

У цій схемі відбувається приємна магія без магії. Коли контейнер стартує без додаткових аргументів, Docker бере ENTRYPOINT і додає до нього CMD, отримуючи:

java -jar app.jar --server.port=8080

А коли ви хочете замінити значення за замовчуванням, запускаєте так:

# Публікуємо потрібний порт назовні й переозначуємо дефолтний аргумент застосунку
docker run -p 9090:9090 docker-java-catalog-service --server.port=9090

І Docker уже запускає:

java -jar app.jar --server.port=9090

Тобто ви замінили значення за замовчуванням із CMD, але не чіпали стабільний ENTRYPOINT. Це саме той поділ відповідальності, який нам і потрібен.

Тут важливо правильно вловити статус цієї схеми. ENTRYPOINT + CMD — це хороший патерн механіки: стабільна команда живе в ENTRYPOINT, а замінювані аргументи застосунку — у CMD. Але це не означає, що будь-який сервіс зобовʼязаний тримати значення за замовчуванням саме в CMD. Якщо ті самі значення вже живуть у змінних середовища образу або приходять у конкретному запуску, дублювати їх ще й у CMD не потрібно.

І ще важливе уточнення: не треба намагатися запхати в CMD взагалі все, що тільки можна. CMD — гарне місце для одного-двох простих аргументів застосунку за замовчуванням, але не варто перетворювати його на склад усього підряд. Якщо ви бачите CMD на 12 рядків, це зазвичай знак, що ви розвʼязуєте конфігураційну задачу не тим інструментом (а Dockerfile уже починає виглядати як мінісервер конфігурації… причому з характером).

4. Склеювання ENTRYPOINT/CMD і docker run

Тема ENTRYPOINT і CMD завжди здається зрозумілою… рівно до першого запуску контейнера з аргументами й помилки рівня «executable file not found». У цей момент у голові зазвичай виникає філософське запитання: «Docker, ти взагалі зрозумів, що я мав на увазі?» Щоб таких сюрпризів було менше, давайте один раз чесно розберемо механіку, не перетворюючи лекцію на довідник.

Базова модель така: Docker бере те, що написано в Dockerfile, і будує підсумкову команду запуску. Якщо задано і ENTRYPOINT, і CMD, то підсумкова команда виглядає як «ENTRYPOINT + CMD». А те, що ви пишете після імені image у docker run, зазвичай розглядається як заміна CMD — у сервісному сценарії це якраз найчастіший і бажаний ефект.

Схематично це можна подати так:

flowchart LR
    A[Точка входу Dockerfile] --> C[Підсумкова команда]
    B[Dockerfile CMD] --> C
    D["docker run ... IMAGE args"] -->|заміщує CMD| C

Тепер найважливіший момент: чому ми так наполягаємо на ENTRYPOINT для стабільної команди запуску.

Подивімося на небезпечний, але дуже поширений варіант: «а давайте все покладемо в CMD, і буде нормально».

FROM eclipse-temurin:25-jre-jammy
WORKDIR /opt/app

# Копіюємо JAR усередину образу
COPY build/libs/*.jar app.jar

# Небезпечна схема: вся команда запуску лежить у CMD, її дуже легко випадково замінити під час docker run
CMD ["java", "-jar", "app.jar"]

Здається, що контейнер стартує — і так, це правда:

# Сервіс стартує, бо CMD містить команду запуску
docker run --rm -p 8080:8080 docker-java-catalog-service

Але тепер уявімо, що ви хочете додати аргумент застосунку. Логіка новачка тут абсолютно природна: «Ну я ж просто дописую параметр після імені image».

# Здається, що ми “додали” аргумент застосунку...
docker run --rm -p 9090:9090 docker-java-catalog-service --server.port=9090

І ось тут Docker думає інакше. Він каже: «Ага, користувач указав COMMAND після імені image. Отже, треба повністю замінити CMD». У результаті він намагається запустити контейнер, де командою є --server.port=9090. А це не команда, а рядок, який Spring Boot зрозумів би як аргумент, але Docker намагається прочитати його як виконуваний бінарник.

Результат виглядає приблизно так:

exec: "--server.port=9090": виконуваний файл не знайдено в $PATH

Це одна з найкорисніших навчальних помилок у Docker: вона наочно показує, що без ENTRYPOINT ви занадто легко перетворюєте аргументи застосунку на команду контейнера.

Порівняйте з варіантом, де ENTRYPOINT фіксує java -jar, а CMD зберігає аргументи за замовчуванням:

# Команда запуску завжди одна й та сама
ENTRYPOINT ["java", "-jar", "app.jar"]

# Дефолтні аргументи — їх можна замінити через docker run ... IMAGE <args>
CMD ["--server.port=8080"]

Тоді:

# Тепер аргумент після імені image — це заміна CMD, а не “нова команда запуску”
docker run --rm -p 9090:9090 docker-java-catalog-service --server.port=9090

Docker уже читає це як «заміни CMD на --server.port=9090», і все запускається так, як ви очікували.

5. Правило для Spring Boot: стабільне й змінне

Зараз акуратно зберемо це в інженерне правило, яке стане вам у пригоді не лише в навчальному проєкті. У Spring Boot-сервісі майже завжди є частина запуску, яку треба прибити цвяхами: це сам факт запуску JVM і самого jar. І є параметри, які мають легко змінюватися між запусками: порт, профіль, іноді увімкнення або вимкнення певної поведінки в розумних межах. Якщо змішати ці дві групи, Dockerfile вийде або крихким, або незручним.

Щоб було простіше тримати це в голові, давайте зведемо все в таблицю саме в контексті нашого Boot-сервісу:

Елемент запуску Приклад Характер Куди зазвичай класти в Dockerfile Чому
Команда запуску JVM java стабільний ENTRYPOINT це «двигун» контейнера; його рідко хочуть змінювати
Спосіб запуску артефакту -jar app.jar стабільний ENTRYPOINT це «що саме ми запускаємо»
Параметри застосунку (прості) --server.port=8080 часто змінюється CMD або параметри docker run зручно переозначувати без перескладання образу
Параметри застосунку (режим/профіль) --spring.profiles.active=standalone часто змінюється CMD або параметри docker run образ один, середовище різне
“Діагностичні” команди sh ситуативно зазвичай не кладемо в Dockerfile це не штатний старт сервісу, а налагодження

Зверніть увагу: поки ми свідомо говоримо саме про прості параметри застосунку. Детально розбирати канали передавання — змінні середовища, JVM-параметри, JAVA_TOOL_OPTIONS, аргументи застосунку тощо — будемо окремо. Зараз нам важливо на рівні Dockerfile розділити команду й налаштування за замовчуванням.

Є ще одне тонке відчуття, яке варто впіймати: ENTRYPOINT — це про намір образу. Якщо ваш образ — це сервіс, то ENTRYPOINT має говорити про це прямо: «Я запускаю застосунок». Якщо образ — утиліта, ENTRYPOINT міг би бути утилітою. Але наш образ — саме сервіс, тому чесний ENTRYPOINT тут робить поведінку передбачуваною.

Але поділити стабільне й змінне — ще не вся історія запуску. Якщо між Docker і JVM опиниться shell, зупинка й перезапуск раптово стануть окремою проблемою навіть за гарного ENTRYPOINT + CMD.

6. Шаблони Dockerfile для Boot-сервісу

Зараз у вас може зʼявитися запитання: «Гаразд, ENTRYPOINT і CMD є, але як саме їх писати? І скільки варіантів узагалі вважається нормальними?» У Docker, як завжди, варіантів багато, але для навчального Java/Spring Boot-сервісу нам потрібні буквально кілька стійких схем. Ми не хочемо зоопарк підходів — нам потрібна одна зрозуміла базова схема, яку легко пояснити людині, яка ще вчора думала, що Dockerfile — це «якась магічна інструкція для докера».

Нижче — кілька моделей, які трапляються в реальному житті, і як до них ставитися саме в контексті нашого сервісу.

Схема Приклад у Dockerfile Що вийде під час звичайного docker run Загальна оцінка для сервісу
Лише ENTRYPOINT ENTRYPOINT ["java","-jar","app.jar"] запускається сервіс “як є” добре, коли не потрібні дефолтні аргументи
ENTRYPOINT + CMD як args
ENTRYPOINT ["java","-jar","app.jar"]
CMD ["--server.port=8080"]
запускається сервіс із дефолтним аргументом чудовий баланс, коли значення за замовчуванням для аргументів застосунку справді зручно тримати окремо
Лише CMD як команда CMD ["java","-jar","app.jar"] запускається сервіс небезпечно: легко випадково замінити команду запуску
ENTRYPOINT охоплює все підряд ENTRYPOINT ["java","-jar","app.jar","--server.port=8080", ...] запускається сервіс незручно: параметри складно переозначувати без переписування ENTRYPOINT

Особливо важливо відчути, чому схема «лише CMD як команда» небезпечна саме для новачка. Не тому, що так «не можна», а тому, що її занадто легко зламати однією спробою додати аргумент. А новачок додаватиме аргументи — і це нормально, це частина навчання. Тому наше завдання — дати схему, яка витримує звичайні людські експерименти.

ENTRYPOINT + CMD корисно саме тим, що під час запуску стає менше прихованих місць. Якщо якісь параметри вже живуть у змінних середовища образу, не треба ще раз запікати їх у ENTRYPOINT або дублювати в CMD — від цього сервіс не стає зрозумілішим. Нам тут важливий сам принцип: команда запуску стабільна, а змінні параметри не прибиті до неї назавжди.

7. Типові помилки під час роботи з ENTRYPOINT і CMD

Помилка № 1: зберігати запуск сервісу лише в CMD, а потім «додавати аргумент» через docker run image --something.
Це найчастіша рання пастка. Здається, що ви просто дописуєте параметр, але Docker інтерпретує це як заміну команди запуску. У результаті він намагається запустити --server.port=9090 як виконуваний файл і падає з помилкою “executable file not found”. Лікується просто: для сервісів фіксуйте стабільний запуск через ENTRYPOINT, а CMD використовуйте для аргументів за замовчуванням.

Помилка № 2: складати в ENTRYPOINT і команду запуску, і всі «часто змінні» параметри.
Такий Dockerfile виглядає компактно, але в реальному житті швидко починає дратувати: ви захочете змінити порт, профіль або прапорець — і виявиться, що це вже частина «прибитої цвяхами» команди. У результаті ви або перескладаєте образ заради параметра (що в корені суперечить ідеї контейнерів), або починаєте споруджувати хаотичні способи переозначення.

Помилка № 3: очікувати, що аргументи з docker run «додадуться» до CMD, а не замінять його.
Психологічно хочеться думати, що docker run image --x просто дописує --x до тих аргументів, що вже є в CMD. Але в базовій моделі це саме заміна значення за замовчуванням. Тому якщо ви використовуєте CMD, тримайте його коротким і не розраховуйте на те, що «воно само якось склеїться». Краще явно контролювати, що є значенням за замовчуванням, а що ви передаєте під час запуску.

Помилка № 4: плутати «аргументи контейнера» та «аргументи застосунку».
Команда docker run має свої опції (-p, -e, --name), а після імені image починається зовсім інший світ — те, що Docker вважає командою й аргументами процесу. Новачки часто ставлять --server.port=... поруч із -p, очікуючи однакової поведінки. Але -p налаштовує публікацію порту контейнера назовні, а --server.port налаштовує внутрішній порт застосунку. Це два різні рівні, і ENTRYPOINT/CMD допомагають зробити другий рівень більш передбачуваним.

Помилка № 5: намагатися «зробити універсальний Dockerfile на всі випадки життя» просто зараз.
На цьому етапі достатньо одного зрозумілого сценарію: сервіс стартує стабільною командою через ENTRYPOINT, а змінні значення можна замінити через CMD/docker run. Універсальність зазвичай приходить не від того, що ви одразу написали Dockerfile на 60 рядків, а від того, що обрали просту модель і не порушуєте її в дрібницях. CMD корисний тоді, коли справді зберігає значення за замовчуванням для аргументів застосунку, а не дублює те, що вже прийшло через змінні середовища.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ