JavaRush /Курси /Docker for Spring /Канонічний шлях пакування

Канонічний шлях пакування

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

1. Сенс канонічного шляху пакування

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

Проблема не в тому, що існує кілька шляхів. Проблема в тому, що без канону команда починає жити в режимі «кожен робить так, як звик». Один збирає docker build, другий — ./gradlew bootBuildImage, третій узагалі випадково запускає старий тег, бо він у нього лишився в кеші. І коли щось ламається, ви навіть не впевнені, який саме образ запустили, яким способом його зібрали й які «приховані» налаштування в ньому опинилися.

Для навчального сервісу це теж критично: без шляху за замовчуванням будь-яка діагностика перетворюється на вгадування. Ви можете бачити один і той самий контейнерний симптом, але при цьому порівнювати два різні образи, зібрані різними способами.

2. Канон у репозиторії та теги

Слово «канонічний» звучить так, ніби ми зараз обиратимемо «єдину істинну релігію пакування». Насправді все простіше й приземленіше. Канонічний шлях — це той, який ви вважаєте базовим за замовчуванням: він описаний у README першим, за ним зроблено всі приклади в документації проєкту, на нього спираються smoke-скрипти, і саме його ви називаєте, коли хтось запитує: «як зібрати й запустити?».

Важливо: канонічний шлях не зобовʼязаний бути єдиним. Він зобовʼязаний бути першим і однозначним. Другий шлях у цьому разі стає «підтримуваною альтернативою»: він теж робочий, теж описаний, але має свою роль. Наприклад: «якщо вам потрібно швидко отримати стандартний образ без ручного Dockerfile — використовуйте buildpacks». Або навпаки: «якщо вам потрібен повний контроль над кроками збирання — використовуйте Dockerfile».

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

Щоб це відчувалося не як філософія, а як інженерія, зручно тримати в голові схему «один сервіс → два конвеєри пакування → два образи → один і той самий запуск».

flowchart TD
    A["docker-java-catalog-service (один Spring Boot-сервіс)"] --> B["Шлях через Dockerfile `docker build`"]
    A --> C["Шлях через buildpacks `./gradlew bootBuildImage`"]
    B --> D["Образ: `:dockerfile`"]
    C --> E["Образ: `:buildpacks`"]
    D --> F["docker run (контейнер)"]
    E --> F

Імена образів і теги

Один із найдешевших способів влаштувати собі хаос — зібрати різними способами один і той самий тег. Умовно: вчора ви зібрали docker-java-catalog-service:latest через Dockerfile, сьогодні — через buildpacks, і в обох випадках задоволені. А потім запускаєте контейнер і не розумієте, чому він поводиться «як учора, але не зовсім».

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

- docker-java-catalog-service:dockerfile
- docker-java-catalog-service:buildpacks

Тоді навіть у розмові ви не плутатиметеся. Фраза «запусти :dockerfile» набагато краща, ніж «запусти latest, але той, що через Dockerfile, не переплутай».

Мінімальний сценарій шляху Dockerfile виглядає так:

# Збираємо образ через Dockerfile і фіксуємо тег, щоб його неможливо було переплутати
docker build -t docker-java-catalog-service:dockerfile .
# Запускаємо контейнер саме з цього образу
docker run --rm -p 8080:8080 docker-java-catalog-service:dockerfile

А шлях buildpacks (після того, як ми зафіксували imageName, про це за хвилину) — ось так:

# Збираємо образ через buildpacks (імʼя/тег образу фіксуємо в Gradle-конфігурації)
./gradlew bootBuildImage
# Запускаємо контейнер із тега buildpacks
docker run --rm -p 8080:8080 docker-java-catalog-service:buildpacks

До речі, корисно памʼятати, що сам по собі факт «образ зібрано buildpacks» не робить його якимось «особливим». Це все одно звичайний Docker-образ, який ви запускаєте звичайною командою, так само як і будь-який інший. І Spring Boot загалом прямо підтримує такий шлях: bootBuildImage створює образ, після чого ви запускаєте його docker run.

Фіксуємо обидва шляхи в репозиторії

Якщо ви хочете, щоб обидва шляхи були не «випадково робочими», а підтримуваними, їм потрібно дати зрозумілі точки входу. У нашому проєкті точок входу рівно дві: Dockerfile і Gradle-завдання bootBuildImage. Наша мета — не ускладнити репозиторій, а зробити його читабельним. Тому ми не заводимо десять файлів і не починаємо будувати міні-CI в себе на ноутбуці. Ми просто акуратно фіксуємо назви та порядок.

Фіксуємо imageName для buildpacks

За замовчуванням bootBuildImage може автоматично обирати імʼя образу, і це «нормально», доки ви працюєте лише з одним шляхом. Але щойно шляхів два, імʼя мусить стати явним.

import org.springframework.boot.gradle.tasks.bundling.BootBuildImage // Тип завдання Spring Boot, що збирає образ через buildpacks

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

Тут важливий не сам Kotlin-код (він простий), а ідея: ви робите результат buildpacks передбачуваним. І тоді в README можна чесно написати: «команда створює ось такий тег».

Документуємо канон і альтернативу в README

README має бути таким, щоб людина без телепатії могла:

1) зібрати образ,
2) запустити контейнер,
3) перевірити, що сервіс живий.

Якщо в нас два шляхи пакування, README зобовʼязаний сказати, який з них основний. Приклад того, як це може виглядати:

## Образ контейнера

Канонічний шлях: Dockerfile

```bash
docker build -t docker-java-catalog-service:dockerfile .
```

Підтримувана альтернатива: Buildpacks (Spring Boot `bootBuildImage`)

```bash
./gradlew bootBuildImage
```

Зверніть увагу: ми не сперечаємося, який шлях кращий. Ми просто фіксуємо, який шлях є основним у цьому репозиторії. Це економить неймовірну кількість часу на комунікацію. І так, це той випадок, коли «зайві два рядки в README» дешевші, ніж одна розмова в стилі «а в тебе як зібрано?».

Не плодимо зайві Dockerfile

Дуже популярна пастка: «давайте зробимо Dockerfile.dockerfile, Dockerfile.buildpacks, Dockerfile.v2, Dockerfile.final…». У підсумку репозиторій перетворюється на музей археології, де кожен експонат «важливий», але ніхто не впевнений, який із них живий.

Buildpacks-шлях за визначенням не потребує Dockerfile. Тому не потрібно намагатися «наблизити» його до Dockerfile-шляху, створюючи окремі Dockerfile для кожної стратегії. У нашому проєкті один Dockerfile лишається головним ручним сценарієм, а buildpacks живуть у Gradle-світі. Це різні рівні керування, і саме в цьому їхній сенс.

4. Критерії вибору: Dockerfile vs buildpacks

Після порівняння правила зазвичай дуже прості:

- якщо важливіші прозорість кроків збирання, ручний контроль і зрозумілість пакування, шлях за замовчуванням частіше стає Dockerfile;
- якщо важливіше стандартизувати десяток схожих Boot-сервісів і прибрати зайвий boilerplate, шлях за замовчуванням цілком може стати buildpacks;
- підтримувана альтернатива від цього не зникає: вона просто перестає вдавати другий стандарт.

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

І тут важливе одне розмежування. Шлях за замовчуванням стосується звичайного runtime-збирання сервісу. Це не означає, що будь-який дружній до розробника варіант запуску має виглядати точно так само: головне, щоб він був явно підписаний і не підміняв собою основний образ проєкту.

5. Одна smoke-перевірка для обох шляхів

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

# Замість <tag> підставте dockerfile або buildpacks
docker run -d -p 8080:8080 --name catalog \
  docker-java-catalog-service:<tag>

curl -s http://localhost:8080/actuator/health   # {"status":"UP"}

docker rm -f catalog

Така перевірка дисциплінує краще за будь-яку суперечку. Якщо smoke-check однаковий, значить ви дійсно порівнюєте два шляхи пакування одного й того самого сервісу, а не два випадково різні стани проєкту.

6. Типові помилки під час вибору канонічного шляху

Помилка №1: у README вказано два шляхи без канону.
Помилка №1 — залишити в README дві команди без пояснення, яка з них основна. Тоді на будь-яке запитання «як зібрати?» ви отримуєте дві відповіді, і команда починає жити в режимі вічного контексту: хто з ким говорив, той так і робить.

Помилка №2: один і той самий тег використовується для різних шляхів пакування.
Використовувати один і той самий тег для різних шляхів — дрібниця лише на перший погляд, доки ви не зловите ситуацію, коли docker run docker-java-catalog-service:latest запускає не те, чого ви очікували. Docker дуже чесний: він запустить те, що в нього є. А от ви можете бути не такими чесними із собою, якщо не фіксуєте теги на кшталт :dockerfile і :buildpacks.

Помилка №3: очікування, що bootBuildImage «прочитає Dockerfile».
У голові це звучить логічно («я ж про Docker!»), але на практиці це різні механізми. Dockerfile — це ваш явний сценарій збирання. Buildpacks — це стандартизований сценарій, що живе в buildpack-логіці. Якщо ви змішуєте ці світи в голові, ви починаєте «налаштовувати не там»: правите Dockerfile й дивуєтеся, чому образ buildpacks не змінився.

Помилка №4: порівняння Dockerfile і buildpacks у різних умовах.
Наприклад, Dockerfile-образ ви збираєте зі свіжого коду, а buildpacks — із кешу, або навпаки. Потім ви робите висновок «цей шлях швидший» або «цей менший», але порівняння нечесне. Інженерне порівняння вимагає однакових умов: той самий код, той самий запуск, та сама перевірка.

Помилка №5: документуються команди, але не документується зміст.
Фраза «ось дві команди» — не документація. Документація — це коли поруч написано, яка команда для чого. І тут усе дивовижно просто: Dockerfile — коли потрібні явний контроль і прозорість, buildpacks — коли потрібен стандартний шлях без ручного опису кроків. Коли ви пишете це в README, проєкт перестає бути загадкою й починає бути інструментом.

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