JavaRush /Курси /Docker for Spring /bootBuildImage у Grad...

bootBuildImage у Gradle

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

1. bootBuildImage і Dockerfile: роль

Buildpacks уже не здаються абстракцією: це керований шлях пакування, який усе одно завершується звичайним OCI/Docker image. Тепер постає практичне питання: як цей шлях виглядає у звичайному Spring Boot проєкті так, щоб не виникав ще один паралельний спосіб збирання.

Відповідь — bootBuildImage. Це Gradle-задача плагіна Spring Boot, яка збирає образ через buildpacks і залишає результат у локальному Docker daemon. Вона не читає Dockerfile і не підміняє його: це просто другий офіційний шлях пакування того самого сервісу, але всередині звичного робочого процесу ./gradlew.

2. Як працює bootBuildImage: входи та результат

Якщо ставитися до bootBuildImage як до магії, вона неодмінно вас вкусить. Тому зробімо те, що Docker привчав нас робити від початку курсу: побудуємо спостережувану модель «вхід → процес → вихід», щоб було зрозуміло, що саме відбувається.

На вхід bootBuildImage отримує ваш Spring Boot проєкт — його залежності, налаштування збирання та, по суті, артефакт, який треба запакувати (зазвичай це bootJar). Далі Gradle запускає збирання через buildpacks: усередині спеціального builder image виконуються кроки визначення типу застосунку й пакування. У підсумку ви отримуєте повноцінний OCI/Docker-compatible image, який зʼявляється у вашому локальному Docker daemon. З ним можна робити все те саме, що й з образом із Dockerfile: docker image ls, docker run, docker logs.

Зручно тримати це в голові як просту схему:

flowchart TD
    A[Gradle-проєкт] --> B[bootJar]
    B --> C[bootBuildImage]
    C --> D["builder image: збирання за правилами buildpacks"]
    D --> E["готовий OCI image у локальному Docker daemon"]

Щоб було ще менш «розмито», зберемо мінітаблицю:

Питання Коротка відповідь для практики
Де живе bootBuildImage? У Gradle-задачах плагіна Spring Boot (org.springframework.boot).
Що вона не робить? Не читає ваш Dockerfile і не виконує з нього FROM/COPY/ENTRYPOINT.
Що вона робить? Запускає buildpacks-процес збирання і створює Docker-сумісний образ.
Де зʼявляється результат? У локальному Docker daemon (як звичайний image).

Якщо запамʼятаєте лише одне речення з цього розділу, нехай воно буде таким: bootBuildImage — це «збери мені образ», але не через Dockerfile, а через buildpacks, і поклади результат у Docker як звичайний image.

3. Запуск bootBuildImage та вимоги до оточення

У світі Gradle є чудова річ: майже все виглядає однаково. Неважливо, чи ви запускаєте тести, чи збирання jar — це просто команда ./gradlew <taskName>. Тому bootBuildImage у навчальному проєкті хочеться запускати максимально прямолінійно, щоб мозок не перегрівався через питання «а це точно не потрібно робити через docker build?».

Базова команда така:

# Збираємо Docker-сумісний образ через buildpacks
./gradlew bootBuildImage

На практиці під час першого запуску зазвичай відбуваються дві речі, які можуть здивувати новачка. По-перше, задача може виконуватися довше, ніж ви очікували, тому що Docker підтягує builder/run-образи (це нормальна «плата за перший раз»). По-друге, якщо Docker daemon не запущений, ви побачите помилку рівня «не можу підключитися до Docker» — і це теж нормально: образ фізично має кудись «завантажитися», а для цього потрібен працюючий Docker.

Щоб не перетворювати це на ворожіння, запамʼятайте одну річ: bootBuildImage залежить від вашого Docker runtime, тому що результат — це реальний образ, який має потрапити в локальне Docker-середовище.

4. Налаштування образу в bootBuildImage

Імʼя образу: imageName

Коли ви працюєте в проєкті, де є кілька способів пакування (Dockerfile-шлях і buildpacks-шлях), імʼя образу стає не «приємною опцією», а способом зберегти собі здоровий глузд. Інакше ви зібрали одне, запустили інше, а винуватим, як завжди, чомусь виходить Spring.

Мінімальне корисне налаштування в bootBuildImage — це явно задати імʼя підсумкового образу. У build.gradle.kts це робиться так:

import org.springframework.boot.gradle.tasks.bundling.BootBuildImage

tasks.named<BootBuildImage>("bootBuildImage") {
    // Явно задаємо імʼя і тег образу, щоб не плутати його з образом, зібраним через Dockerfile
    imageName.set("docker-java-catalog-service:buildpacks")
}

Зверніть увагу на дві деталі. По-перше, ми імпортуємо BootBuildImage, щоб Gradle знав, з яким типом задачі ми працюємо. По-друге, ми задаємо тег :buildpacks, щоб не зіткнутися з нашим образом, зібраним через Dockerfile (який, наприклад, може бути :dockerfile). Це як підписати контейнери й банки на кухні: «сіль» і «цукор» виглядають однаково, доки ви не спробуєте чай.

Щоб побачити ефект одразу, після збирання просто подивіться список образів:

# Перевіряємо, що образ зʼявився локально і має очікуваний тег
docker image ls docker-java-catalog-service

Тоді серед результатів має бути тег buildpacks.

Дуже поширена рання помилка — намагатися «змішати» два світи. Людина бачить Dockerfile і думає: «Ну от там же FROM і ENTRYPOINT, мабуть, bootBuildImage якось це використовує». Не використовує. Це, по суті, два паралельні шляхи пакування.

Правильна ментальна модель така: якщо ви налаштовуєте збирання образу через buildpacks, то «точки керування» знаходяться в Gradle-конфігурації (тобто у build.gradle.kts), а не в Dockerfile. Dockerfile залишається вашим текстовим сценарієм для Dockerfile-шляху, а bootBuildImage — звичайною Gradle-задачею для buildpacks-шляху.

Для повноти картини (і щоб ви розуміли, звідки взагалі зʼявляється ця задача), переконайтеся, що у вас підключений Spring Boot плагін. Мінімально це виглядає так:

plugins {
    // Без плагіна Spring Boot задача bootBuildImage у Gradle просто не зʼявиться
    id("org.springframework.boot") version "4.0.3"
    id("io.spring.dependency-management") version "1.1.7"
    java
}

Якщо Boot-плагіна немає, bootBuildImage просто не зʼявиться — і Gradle чесно скаже: «Task not found». Це не означає, що buildpacks зламалися: просто самої задачі немає.

5. builder image і run image: базові поняття

У світі buildpacks теж є два образи: builder image потрібен для збирання, run image — для запуску. Для практики тут важливий не другий круг аналогій, а прикладний висновок: builder можна фіксувати з Gradle, щоб збирання не плавало між машинами та CI. На старті нам достатньо саме цього важеля: разом із builder приходить узгоджена runtime-база, тому тонке налаштування тут поки що не потрібне.

builder image містить інструменти та buildpacks-логіку, тобто те середовище, де готується підсумковий образ. run image — це основа, на якій потім реально стартує застосунок. Розділення дуже схоже на multi-stage Dockerfile, тільки у світі buildpacks воно виражене готовою стандартизованою механікою.

У мінімальній конфігурації вам достатньо розуміти, що builder можна зафіксувати так само, як ви фіксуєте базовий образ у Dockerfile, тільки робиться це в Gradle:

import org.springframework.boot.gradle.tasks.bundling.BootBuildImage

tasks.named<BootBuildImage>("bootBuildImage") {
    // Фіксуємо builder image, щоб збирання було передбачуваним між машинами та CI
    builder.set("paketobuildpacks/builder-noble-java-tiny")
}

Це налаштування корисне не тому, що «ми хочемо все кастомізувати», а тому, що воно робить збирання більш передбачуваним: команда знає, яка база використовується для збирання, а не живе в стані «а що воно сьогодні вибрало?».

Ще один практичний нюанс, який часто цікавить Java-розробника: яку версію JVM використає buildpacks. У нормальному проєкті це зазвичай автоматично випливає з налаштувань Java в Gradle (наприклад, через toolchain). Приклад того, як у проєкті фіксують Java 25:

java {
    toolchain {
        // Вказуємо версію Java як вхідну умову збирання (це впливає і на пакування)
        languageVersion.set(JavaLanguageVersion.of(25))
    }
}

Це не «налаштування buildpacks», але це важлива частина вхідних умов: ви збираєте Boot-застосунок під конкретну Java, і пакувальний механізм має поважати цей вибір.

6. Перевірка результату: образ і запуск контейнера

Коли збирання завершилося, найнебезпечніше — вважати, що «раз Gradle написав BUILD SUCCESSFUL, значить усе добре». У Docker-світі прийнято інакше: результат підтверджується запуском і спостереженням.

Перевірка починається з найпростішого — переконатися, що образ зʼявився:

docker image ls docker-java-catalog-service

Далі — запуск. Для навчального сервісу це зазвичай виглядає так:

# Запускаємо контейнер із buildpacks-образу і відкриваємо порт на хості
docker run --rm -p 8080:8080 docker-java-catalog-service:buildpacks

Якщо все добре, ви побачите звичні логи запуску Spring Boot. І ось тепер можна вважати, що «образ існує» не філософськи, а практично: він запускається як контейнер і поводиться як сервіс.

Якщо ви хочете швидко зупинити контейнер, натисніть Ctrl+C у терміналі, якщо запустили його у foreground. Якщо контейнер працює у фоні, зупиніть його звичною командою docker stop <containerId>. Тобто runtime-поведінка тут цілком звичайна: buildpacks змінюють шлях збирання, а не правила життя контейнера.

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

Помилка № 1: Docker не запущений, а ви чекаєте на образ.
Виглядає це зазвичай як повідомлення про неможливість підключитися до Docker daemon. bootBuildImage може зібрати артефакт, але їй потрібно «покласти» підсумок у Docker. Тому якщо Docker Desktop/Engine вимкнено, задача не перетворюється на образ. Лікування просте і нудне: запустити Docker і повторити команду.

Помилка № 2: очікування, що bootBuildImage прочитає Dockerfile.
Це майже класика: «я ж акуратно писав multi-stage, чому це не використалося?». Тому що це інший шлях. Dockerfile-шлях керується вашим Dockerfile і docker build, buildpacks-шлях керується Gradle-конфігурацією і bootBuildImage. Якщо ви тримаєте в голові цю розвилку, плутанини майже не виникає.

Помилка № 3: образ зібрався, але ви не можете його знайти, тому що імʼя «випадкове».
Якщо не задавати imageName, у вас може вийти імʼя за замовчуванням, яке не збігається з тим, що ви звично вводите в docker run. У результаті ви запускаєте старий образ, дивитеся стару поведінку і дивуєтеся, чому зміни «не застосувалися». Фікс із мінімальним болем — задайте imageName і використовуйте тег на кшталт :buildpacks.

Помилка № 4: у вас два образи одного сервісу, і ви запускаєте не той.
Це нормальна ситуація для проєкту, де є Dockerfile і buildpacks. Ненормально — запускати їх упереміш без тегів і без дисципліни імен. Якщо Dockerfile-образ називається docker-java-catalog-service:latest, а buildpacks-образ теж за замовчуванням потрапив у latest, ви самі собі влаштували квест «вгадай, хто зараз стартував». Розрулюється це тим самим способом, що й у будь-якій інженерній реальності: домовленістю про імена (:dockerfile, :buildpacks) і акуратним README.

Помилка № 5: перше збирання займає вічність, і здається, що «зламалося».
У перший раз buildpacks підтягують builder/run-образи, плюс завантажуються і кешуються потрібні компоненти. Це може бути помітно за часом, особливо на не надто швидкому інтернеті. Якщо процес іде (у консолі щось відбувається, Docker завантажує шари), це зазвичай нормально. Лякатися варто радше тоді, коли все зависло без виводу і без мережевої активності.

1
Задача
Docker for Spring, 9 рівень, 1 лекція
Недоступна
Явне ім'я образу для `bootBuildImage`
Явне ім'я образу для `bootBuildImage`
1
Задача
Docker for Spring, 9 рівень, 1 лекція
Недоступна
Фіксація `builder image` для `bootBuildImage`
Фіксація `builder image` для `bootBuildImage`
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ