1. Артефакт і завдання Gradle
Docker: починаємо з артефакту
Контейнеризація починається не зі спроби «запхнути в Docker весь проєкт», а з одного зрозумілого артефакта. Дуже хочеться взяти весь проєкт — папку src, Gradle, залежності, кота, який лежав на клавіатурі, — і якось відправити це в Docker. Але Docker не має ставати вашим новим компілятором чи IDE. На першому кроці нам потрібен один перевірений результат збирання — артефакт, який можна запускати без участі Gradle, звичайною командою java -jar.
У нашому курсі це принципово: ми будуємо контейнеризацію навколо вже наявного Spring Boot-сервісу. Тому, коли ви пишете перший Dockerfile, на запитання «що саме ми кладемо в образ?» потрібно відповідати спокійно й конкретно: «цей файл .jar». Не «папку проєкту», не «всі вихідні тексти», не «Gradle wrapper», а один виконуваний файл — як добра валіза в подорожі: поклали все потрібне всередину, закрили й далі мандруєте без шафи.
Щоб картинка трималася в голові, запам’ятайте простий ланцюжок:
flowchart TD
A["Вихідні тексти Spring Boot-проєкту"] --> B["Завдання Gradle: bootJar"]
B --> C["Виконуваний .jar у build/libs"]
C --> D["Запуск: java -jar ..."]
D --> E["Пізніше: COPY цього JAR у Docker-образ"]
Зараз ми на ділянці B → C → D: навчимося отримувати правильний .jar, знаходити його й один раз чесно запускати локально.
JAR як переносний файл
Слово jar звучить як «банка», і аналогія тут навіть допомагає: JAR (Java ARchive) — це, по суті, ZIP-архів, усередині якого лежать .class файли та ресурси. У звичайній Java-практиці JAR часто буває «тонким»: у ньому є лише ваш код, а залежності лежать окремо, і запуск потребує акуратно налаштованого classpath. Але Spring Boot зробив розробникам подарунок (так, жарт про GiftGenius тут напрошується, але ми втримуємося): він уміє збирати виконуваний JAR, у якому вже лежить майже все потрібне для старту.
Для першого Docker-образу це особливо важливо, бо Docker любить зрозумілі вхідні дані. Коли ваш артефакт — один файл, далі все передбачувано: Dockerfile просто копіює цей файл і запускає його. Вам не потрібно на першому кроці пояснювати контейнеру, як збирати проєкт, як завантажувати залежності, як вибирати JDK і чому Gradle іноді вередує (а він уміє, як будь-яка творча натура).
Виконуваний JAR у Spring Boot часто називають «fat jar» або «uber jar», але лякатися цих термінів не потрібно: сенс один — усередині лежить і ваш код, і потрібні бібліотеки. Саме тому такий JAR можна запускати без IDE і без Gradle: у будь-якій папці й на будь-якій машині, де є відповідна Java.
Практична думка тут проста: якщо ви вмієте зібрати JAR і запустити його командою java -jar, то ви вже на пів шляху до контейнера. Docker не лікує проблеми застосунку, він просто запускає його в іншому середовищі. Тому почнемо з нормального JAR.
bootRun, jar і bootJar
У Spring Boot-проєкті на Gradle команди bootRun, jar і bootJar легко переплутати. Новачок часто думає: «Я ж запускав через bootRun — отже, усе нормально». Для локальної розробки — так. Для контейнеризації — не зовсім. Нам важливо не «якось запустити», а отримати переносний артефакт, який однаково стартує і у вас, і в колеги, і всередині контейнера.
Розкладемо різницю максимально приземлено. bootRun — це запуск застосунку з вихідних текстів через Gradle, у контексті розробки. Він зручний, коли ви пишете код і хочете швидко перезапускати сервіс. jar — звичайне завдання Gradle для збирання «звичайного» JAR: часто без залежностей, а в Boot-проєктах — ще й не той артефакт, який вам потрібен. А bootJar — спеціальне завдання Spring Boot, яке робить виконуваний JAR, придатний для java -jar.
Для закріплення тримайте маленьку таблицю — не для іспиту, а щоб мозок не плутався:
| Команда/завдання | Що робить по-людськи | Підходить для першого Docker-образу? |
|---|---|---|
| bootRun | Запускає сервіс у режимі розробки, без окремого артефакта | Ні: нам потрібен файл, який можна копіювати |
| jar | Збирає «звичайний» JAR (може виявитися невиконуваним) | Зазвичай ні: легко отримати JAR, який не стартує |
| bootJar | Збирає виконуваний Spring Boot JAR | Так: це наш стартовий артефакт |
Сьогодні свідомо обираємо bootJar як джерело істини. Нам потрібен файл, який далі стане тим самим «одним файлом», який ми копіюватимемо в образ.
Мінімальна команда виглядає так:
# Збираємо виконуваний Spring Boot JAR через завдання bootJar
./gradlew bootJar
Якщо ви на Windows і у вашому проєкті є Gradle wrapper (а в нормальному проєкті він є), команда буде такою:
:: Те саме для Windows (Gradle Wrapper)
gradlew.bat bootJar
Сенс той самий: зібрати виконуваний JAR.
2. Де лежить артефакт: build/libs
Після bootJar у новачка часто починається мініквест: «Я все зібрав, але де файл?» Gradle за замовчуванням складає результати збирання в папку build/, а конкретно JAR зазвичай лежить у build/libs. Це просте правило варто запам’ятати одразу — воно ще не раз зекономить час, особливо коли дійдемо до COPY у Dockerfile.
Тут усе доволі рутинно. Спочатку збираємо JAR:
# Збираємо артефакт (без запуску застосунку)
./gradlew bootJar
Потім дивимося, що з’явилося в build/libs:
# Перевіряємо, що Gradle справді поклав JAR у build/libs
ls build/libs
# docker-java-catalog-service-0.0.1-SNAPSHOT.jar
Ім’я файла може відрізнятися, наприклад через іншу версію, але сенс один: з’явився один головний JAR, який ви маєте вміти назвати. Чому це важливо? Бо Dockerfile не вміє читати думки. Пізніше ви напишете COPY, і якщо точного імені файла ви не знаєте, почнеться режим «ну, приблизно ось так» — а Docker, як на зло, сприймає «приблизно» як «помилку».
Є ще один корисний прийом, який допомагає перестати сприймати JAR як «чорний ящик». Можна краєм ока зазирнути всередину й побачити характерну структуру Spring Boot JAR:
# Дивимося список файлів усередині JAR (Spring Boot зазвичай містить BOOT-INF)
jar tf build/libs/docker-java-catalog-service-0.0.1-SNAPSHOT.jar | head
# META-INF/
# META-INF/MANIFEST.MF
# org/
# BOOT-INF/
Лякатися не потрібно. Важливо лише запам’ятати: наявність BOOT-INF/ — типова ознака того, що перед вами саме Spring Boot executable JAR, а не «тонкий» JAR. Ми не заглиблюємося в устрій завантажувача — це справді окрема тема, — але вміти зрозуміти «це схоже на те, що має бути» корисно навіть на старті.
І ще один момент про дисципліну: якщо ви змінювали код і хочете бути впевнені, що JAR свіжий, можна збирати з очищенням:
# Про всяк випадок очищаємо build/ і збираємо заново, щоб не запускати «вчорашній» JAR
./gradlew clean bootJar
Це не обов’язково робити щоразу, але на етапі навчання такий крок корисний: менше шансів випадково запустити «вчорашній» JAR і почати сперечатися з реальністю.
3. Локальна перевірка java -jar
Перед Docker важливо один раз чесно запустити JAR локально через java -jar. Інакше будь-яка проблема на старті контейнеризації швидко перетворюється на детектив, де незрозуміло, що саме зламалося: сам застосунок чи вже контейнеризація. Тому робимо простий крок: запускаємо артефакт локально командою, максимально схожою на ту, що буде всередині контейнера. Без IDE, без Gradle і без «воно в мене в IntelliJ стартує, отже все ок». Для Docker важливий саме java -jar.
Команда виглядає так:
# Запускаємо зібраний артефакт напряму, без Gradle і IDE
java -jar build/libs/docker-java-catalog-service-0.0.1-SNAPSHOT.jar
Після запуску ви побачите логи Spring Boot. Зараз нас цікавлять не всі деталі, а один ключовий сигнал: застосунок дійшов до стану «Started …», тобто підняв вебсервер і готовий приймати HTTP-запити. Приблизно це виглядатиме так (рядки у вас можуть відрізнятися, але суть упізнавана):
... Starting CatalogApplication ...
... Tomcat started on port(s): 8080 ...
... Started CatalogApplication in 2.4 seconds ...
Якщо JAR стартує локально таким способом, ми фіксуємо дуже важливу річ: артефакт робочий. Отже, якщо пізніше щось піде не так у Docker, у вас буде тверда точка опори: «сам застосунок стартує, проблема точно не в коді й не в збиранні JAR, проблема в контейнеризації або середовищі». Це економить години, а іноді й нерви — а нерви в IT є обмеженим ресурсом, як оперативка в контейнері… але про це пізніше.
Зупиняють такий запуск зазвичай Ctrl + C у терміналі. Це теж корисно буквально відчути: застосунок — це процес, процес можна запустити й зупинити, і контейнер потім робитиме рівно те саме, лише в іншій «кімнаті».
4. Сервіс у standalone режимі
Ми збираємо не абстрактний JAR «заради JAR», а артефакт конкретного навчального сервісу — Container-Ready Catalog Service. На цьому етапі курсу сервіс має впевнено запускатися без зовнішніх залежностей: без PostgreSQL, без Redis і без RabbitMQ. У цьому й сенс standalone-режиму: швидко отримати перший робочий результат контейнеризації й не розпорошувати увагу на інфраструктуру.
Тому в ідеальному сценарії після java -jar сервіс стартує і піднімає HTTP-порт, зазвичай 8080. Якщо в логах видно, що він намагається підключитися до бази даних і падає, це сигнал: ви випадково ввімкнули не той режим або у вас змінилися локальні налаштування. Сьогодні ми не заглиблюємося в профілі та зовнішню конфігурацію; на цьому кроці нам достатньо простого правила: «перший JAR має бути самодостатнім».
Чому це особливо важливо для Docker? Бо Docker — про відтворюваність. Ми хочемо, щоб перший контейнер запускався в ізоляції й давав передбачуваний результат. Якщо вже на першому кроці зав’язатися на зовнішню базу чи кеш, зникне головна дидактична користь: стане незрозуміло, де помилка — у Docker, у мережі, у БД, у конфігурації чи в самому застосунку. А standalone-режим якраз тримає картинку чистою: один сервіс, один процес, один порт, один JAR.
І ось тепер у нас є те, що справді потрібно перед наступною лекцією: ми можемо назвати точний файл, який стане основою контейнеризації, і знаємо, що цей файл реально запускається. Далі Docker працюватиме не з вашим «проєктом взагалі», а з конкретними файлами, які потрапляють у контекст збирання. Тому так важливо вже зараз розуміти, який саме JAR ми вважаємо вхідним артефактом.
5. Типові помилки під час роботи з bootJar і виконуваним jar
Помилка № 1: плутати bootRun і bootJar та думати, що «раз через IDE працює — значить JAR точно робочий».
bootRun запускає застосунок у зручному режимі розробки, і це справді корисно. Але Docker-контейнеру байдуже, наскільки вам було зручно в IDE. Контейнеру потрібен артефакт. Якщо ви не зібрали bootJar, ви просто не підготували ту саму «упаковану валізу», яку можна переносити між середовищами.
Помилка № 2: шукати JAR «десь поруч», а не в build/libs.
Новачки іноді починають порпатися в src/, у .gradle/, по всій папці проєкту, а іноді ще й створюють на робочому столі «final-final.jar». Насправді шлях майже завжди один: build/libs. Ця звичка економить час: спочатку дивимося туди, де Gradle за замовчуванням кладе результат, і лише потім починаємо фантазувати.
Помилка № 3: запускати старий JAR після змін у коді.
Це класика жанру: ви змінили код, але JAR не пересобрали, потім запускаєте java -jar, бачите «стару поведінку» і починаєте підозрювати магію. Магії немає — є просто забутий збір. Лікується це просто: після змін знову робимо ./gradlew bootJar (або ./gradlew clean bootJar, якщо хочете максимально чесно).
Помилка № 4: зібрати не той артефакт і отримати JAR, який не стартує командою java -jar.
Якщо ви випадково використовуєте jar замість bootJar, можна отримати «тонкий» JAR без залежностей. Він може успішно зібратися, але при запуску впасти з ClassNotFoundException або схожими помилками. Це неприємно, але певною мірою навіть корисно для навчання: такий збій швидко показує, чому нам важливий саме bootJar, а не «будь-який JAR».
Помилка № 5: одразу йти в Docker, не перевіривши java -jar.
Коли JAR не запускається локально, спроба контейнеризувати його перетворюється на подвійну складність: ви одночасно намагаєтеся зрозуміти і проблеми застосунку, і проблеми Docker. Набагато спокійніше спочатку домогтися старту через java -jar, а вже потім додавати контейнерний шар. Це як спочатку навчитися їздити на велосипеді, а потім уже ставити на нього мотор (і так, мотор вібруватиме, а ви — страждатимете).
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ