JavaRush /Курси /Docker for Spring /Читабельний Dockerfile

Читабельний Dockerfile

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

1. Що таке читабельний Dockerfile

«Читабельний Dockerfile» звучить як фраза зі світу дорослих людей, які ставлять коми в README і не називають змінні x2final_final2. Але сенс тут дуже практичний: Dockerfile має бути таким, щоб ви через місяць відкрили його і зрозуміли, що відбувається, не викликаючи шамана з контейнерами і не перечитуючи весь інтернет.

Читабельність тут — не краса заради краси. Це спосіб тримати під контролем три речі: де ми беремо Java-середовище, куди кладемо артефакт (bootJar) і як запускаємо процес усередині контейнера. Щойно Dockerfile стає з розряду «ніби працює, але незрозуміло чому», ви втрачаєте головну перевагу інженерного підходу: можливість передбачувано змінювати поведінку запуску, не ламаючи все довкола.

Наївний Dockerfile уже розвʼязував одне завдання: взяти Java runtime, покласти jar в образ і якось його запустити. Але окремо ми вже побачили, що цього замало: build-time і runtime треба розвести, ENTRYPOINT має запускати Java напряму, а параметри, що змінюються, не можна запікати в нове перезбирання щоразу. Тепер лишилося зібрати все це в один стійкий варіант Dockerfile, щоб оптимізація шарів і кеш потім працювали поверх стабільного запуску, а не замість нього.

2. Каркас Dockerfile: п’ять блоків

Наївний варіант зазвичай виглядає дуже коротко: базовий образ, COPY jar, java -jar. Для першого запуску цього достатньо. Читабельний варіант відрізняється не кількістю рядків, а тим, що кожен рядок фіксує окрему домовленість: де живе застосунок, як він потрапляє в образ, які значення за замовчуванням у ньому вже задані та який процес Docker вважає головним.

Підсумковий Dockerfile дня

# 1) База: середовище виконання Java 25 (тільки для запуску, без збірки)
FROM eclipse-temurin:25-jre-jammy

# 2) Робоча директорія застосунку всередині контейнера
WORKDIR /opt/app

# 3) Кладемо в образ готовий артефакт збірки (Spring Boot bootJar)
ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} app.jar

# 4) Значення за замовчуванням для запуску (можна перевизначити через docker run -e ...)
ENV SPRING_PROFILES_ACTIVE=standalone
ENV SERVER_PORT=8080

# 5) Документуємо порт, який слухає застосунок усередині контейнера
EXPOSE 8080

# 6) Запускаємо Java як головний процес контейнера (exec-form)
ENTRYPOINT ["java", "-jar", "app.jar"]

Це той варіант, де всі домовленості нарешті зібрані в одному місці. Тут ми свідомо не додаємо ще й CMD з тим самим портом або профілем: команда запуску вже стабільна через ENTRYPOINT, а значення за замовчуванням живуть у ENV і можуть бути перевизначені під час конкретного запуску.

Згори вниз цей Dockerfile читається так:

flowchart TD
    A["FROM (база: середовище виконання Java)"] --> B["WORKDIR (робоча папка)"]
    B --> C["ARG + COPY (перенесення bootJar)"]
    C --> D["ENV + EXPOSE (значення за замовчуванням і документація порту)"]
    D --> E["ENTRYPOINT (запуск java у exec-form)"]
Блок Dockerfile За що відповідає Що в нашому Dockerfile
FROM Яка Java і яке базове середовище потраплять у контейнер образ середовища виконання з Java 25
WORKDIR Де живе застосунок усередині образу /opt/app
ARG + COPY Як bootJar потрапляє в образ під стабільною назвою build/libs/*.jarapp.jar
ENV + EXPOSE Які значення за замовчуванням бачить контейнер і який внутрішній порт очікується SPRING_PROFILES_ACTIVE, SERVER_PORT, EXPOSE 8080
ENTRYPOINT Як запускається головний процес контейнера
["java","-jar","app.jar"]

CMD тримаємо в голові як окремий патерн для значень за замовчуванням аргументів застосунку. Але в цьому Dockerfile він свідомо не потрібен: ті самі стартові значення вже живуть у ENV, і друге джерело значень за замовчуванням тільки заплутає.

База і робоча папка

FROM і WORKDIR тут потрібні не заради зайвої теорії, а щоб Dockerfile одразу читався згори вниз без хаосу шляхів. Ми відразу бачимо, на якій Java працює сервіс і де всередині образу знаходиться його «дім».

Завдяки цьому app.jar не бовтається по випадкових каталогах, а всі наступні інструкції працюють відносно однієї зрозумілої директорії. Річ маленька, але саме на таких дрібницях файл перестає бути крихким.

Артефакт збірки

Ми свідомо копіюємо не весь проєкт, а результат bootJar. ARG JAR_FILE залишає збірці невеликий люфт, якщо імʼя jar потрібно вказати явно, а COPY ${JAR_FILE} app.jar фіксує всередині образу стабільне імʼя файла.

Це зручно і для читання, і для запуску: ENTRYPOINT більше не залежить від версійованої назви артефакта з build/libs/, а Dockerfile не перетворюється на список випадкових файлів, що потрапили в образ «про всяк випадок».

Значення за замовчуванням і порт

ENV задає розумні значення за замовчуванням: standalone-профіль і внутрішній порт 8080. Це не «навіки прибиті» значення, а стартові домовленості, які можна перебити через docker run -e або аргументи застосунку.

EXPOSE поруч потрібен не для публікації порту назовні, а щоб одразу було видно, який порт сервіс очікує всередині контейнера. Зовнішній доступ і далі налаштовується тільки через docker run -p.

Запуск процесу

ENTRYPOINT ["java", "-jar", "app.jar"] фіксує найважливіше: Docker запускає Java напряму, без shell-посередника, і саме JVM стає головним процесом контейнера. Завдяки цьому stop/restart поводяться передбачувано, а Spring отримує нормальний шанс завершитися акуратно.

CMD пам’ятаємо як робочий патерн для аргументів застосунку за замовчуванням, коли їх справді зручно тримати окремо. Але в цьому Dockerfile ми свідомо не використовуємо його для порту і профілю: дублювати одні й ті самі значення і в ENV, і в CMD — це тільки множити плутанину.

3. Збирання і запуск без правки Dockerfile

Тепер корисно перевірити сам принцип на практиці: один і той самий образ запускається в кількох режимах, а Dockerfile при цьому лишається незмінним.

Спершу, як і раніше, збираємо jar і образ:

# Збираємо виконуваний jar (Spring Boot bootJar)
./gradlew bootJar

# Збираємо Docker-образ із поточного каталогу (Docker build context = .)
docker build -t docker-java-catalog-service .

Базовий запуск «як є», зі значеннями за замовчуванням із Dockerfile:

docker run --rm -p 8080:8080 docker-java-catalog-service

Якщо все добре, у логах ви побачите ознаки старту та порт. Логи у Spring Boot бувають довгими, але вам зараз важливо побачити смислові шматочки: рядок про активний профіль і рядок про те, що сервер піднявся на потрібному порту. Формулювання може відрізнятися, але ідея одна.

Тепер запускаємо той самий образ на іншому порту через змінні середовища:

docker run --rm -p 9090:9090 \
  -e SERVER_PORT=9090 \
  -e SPRING_PROFILES_ACTIVE=standalone \
  docker-java-catalog-service

Тут важливо не переплутати два світи. -p 9090:9090 — це про мережу Docker (host:container). SERVER_PORT=9090 — це про те, на якому порту сам застосунок усередині контейнера слухатиме HTTP. Якщо змінили одне й забули інше, отримаєте класичне «контейнер живий, але сервіс не відкривається».

Той самий ефект можна отримати через аргументи застосунку: при exec-form ENTRYPOINT усе, що ви допишете після імені образу, потрапить у Spring Boot як app args.

docker run --rm -p 9090:9090 \
  docker-java-catalog-service \
  --server.port=9090 \
  --spring.profiles.active=standalone

А ось окремий канал для JVM-опцій — JAVA_TOOL_OPTIONS. Його сенс у тому, що це спосіб додати параметри JVM, не перетворюючи ENTRYPOINT на shell-form і не ліплячи туди «зшитий рядок».

Наприклад, обмежимо heap — чисто як демонстрацію каналу, без фанатизму:

docker run --rm -p 8080:8080 \
  -e JAVA_TOOL_OPTIONS="-Xms256m -Xmx256m" \
  docker-java-catalog-service

Зверніть увагу на приємну річ: Dockerfile не змінюється. Ми не пересбираємо образ заради «інший порт» або «менше памʼяті». Ми просто запускаємо один і той самий артефакт у різних режимах, і саме для цього взагалі й замислювалася нормальна модель ENTRYPOINT + зовнішня конфігурація.

Щоб не тримати все в голові, можна користуватися короткою пам’яткою:

Що ви хочете змінити Найпростіший канал сьогодні Приклад
Порт застосунку змінна середовища або аргумент застосунку -e SERVER_PORT=9090 або --server.port=9090
Активний профіль змінна середовища або аргумент застосунку -e SPRING_PROFILES_ACTIVE=standalone або --spring.profiles.active=standalone
JVM-параметри JAVA_TOOL_OPTIONS -e JAVA_TOOL_OPTIONS="-Xmx256m"

Ця таблиця потрібна не для зубріння, а щоб швидко відрізняти канал Docker від каналу JVM і каналу Spring Boot.

4. Типові помилки під час роботи з Dockerfile

Помилка № 1: Dockerfile не бачить jar, хоча він точно є.
Найчастіше причина банальна: jar не потрапив у контекст збирання. Іноді студенти занадто агресивно налаштували .dockerignore і виключили build/ цілком, а потім дивуються, що COPY build/libs/*.jar не працює. Лікується це не містикою, а перевіркою: де лежить jar, що реально надсилається в контекст збирання і які правила ігнорування спрацювали.

Помилка № 2: EXPOSE 8080 написали, але сервіс не відкривається.
EXPOSE не публікує порт назовні. Він лише документує очікуваний порт усередині контейнера. Зовнішній доступ з’являється тільки через docker run -p hostPort:containerPort. Якщо переплутати ці ролі, можна безкінечно змінювати Dockerfile, хоча проблема в команді запуску.

Помилка № 3: перехід на shell-form заради «красивого рядка».
Іноді хочеться написати ENTRYPOINT java -jar app.jar, бо це виглядає «як у терміналі». Але це shell-form, і він тягне за собою оболонку як проміжний процес. У результаті ви погіршуєте передавання сигналів Java-процесу і отримуєте менш стабільну поведінку під час зупинки контейнера. Для Java-сервісу базовий варіант має бути нудним і правильним: ENTRYPOINT ["java", "-jar", "app.jar"].

Помилка № 4: плутаються ARG і ENV, і все починає здаватися непередбачуваним.
ARG живе лише під час збирання. Якщо ви спробуєте через ARG «задати порт» або «увімкнути профіль», отримаєте сюрприз: контейнеру це не дістанеться. Для runtime-параметрів потрібен ENV як значення за замовчуванням образу або параметри docker run як перевизначення. Щойно ви розділяєте build-time і runtime в голові, половина плутанини зникає.

Помилка № 5: неузгодженість порту в трьох місцях (і потім боляче).
Студент змінює -p 9090:8080, потім ставить SERVER_PORT=9090, потім додає --server.port=7070, а зверху ще хоче покласти той самий порт у CMD. Тут допомагає просте правило: в одному запуску намагайтеся задавати параметр одним каналом і тримати відповідність між портом застосунку та зіставленням портів контейнера. Інакше ви діагностуватимете не Docker, а власну комбінацію параметрів.

1
Опитування
Dockerfile. Основи, рівень 4, лекція 4
Недоступний
Dockerfile. Основи
Інструкції та запуск контейнерів
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ