JavaRush /Курсы /Docker for Spring /Dockerfile против buildpacks

Dockerfile против buildpacks

Docker for Spring
9 уровень , 3 лекция
Открыта

1. Сравнение Dockerfile и buildpacks сейчас

Когда уже видно, что buildpacks тоже дают layered и inspectable image, сравнение с ручным path получается честным. Ручной путь у нас к этому моменту уже не на уровне «скопировал jar и запустил». Это нормальный manual baseline: multi-stage, слойность, продуманный runtime image. Теперь можно спокойно сравнить его с buildpacks без религии и без спора, кто красивее выглядит в README.

Главная мысль: и Dockerfile, и buildpacks приводят к одному и тому же типу результата — к контейнерному образу, который можно запускать docker run.... Разница не в том, что один путь «настоящий», а другой «ненастоящий». Разница в том, где выражается управление и кто принимает решения.

Для ориентира можно держать в голове простую схему:

flowchart LR
    A[Один и тот же Spring Boot сервис] --> B1[Dockerfile: явные шаги]
    A --> B2[Buildpacks: стандартизованные шаги]
    B1 --> C[OCI/Docker image]
    B2 --> C
    C --> D[Контейнер: docker run]

2. Где живёт управление: Dockerfile и buildpacks

В ручном пути управление лежит в текущем Dockerfile baseline проекта. Причём важно не откатываться мысленно к наивному варианту «скопировал один jar и запустил». Сравниваем именно с тем manual path, который уже накопили: multi-stage сборка, layered Boot jar и runtime image под Java 25.

Ниже не новый альтернативный Dockerfile, а только короткий фрагмент того же manual path — просто чтобы увидеть, где именно в нём живёт управление:

FROM eclipse-temurin:25-jdk AS builder
# ... Gradle build + layered extraction через jarmode=tools

FROM eclipse-temurin:25-jre AS runtime
WORKDIR /app
# ... COPY --from=builder слоёв dependencies / spring-boot-loader / application
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]

Здесь управление видно глазами: вы сами выбираете stages, сами решаете, как извлекать слои, что копировать в runtime и чем стартовать контейнер. Мы уже умеем делать это аккуратно; важна сама точка контроля — управление живёт в Dockerfile.

Gradle-конфигурация bootBuildImage — точка правды в buildpacks пути. Например, минимальная полезная настройка — фиксировать имя образа, чтобы вы не искали его потом по догадкам:

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

tasks.named<BootBuildImage>("bootBuildImage") {
    // Явно задаём имя/тег образа, чтобы потом не искать «куда оно собралось»
    imageName.set("docker-java-catalog-service:buildpacks")
}

Обратите внимание на ощущение: в Dockerfile вы описываете «сначала сделай A, потом B, потом C». В buildpacks вы скорее говорите: «собери мне корректный образ для этого Spring Boot проекта, а итог назови вот так». И это нормально — просто другой стиль контроля.

3. Сравнение по критериям: плюсы и цена

Сравнивать эти подходы по одному признаку (обычно по размеру образа) — почти гарантированно ошибиться, потому что вы измеряете одну цифру, а выбираете целую стратегию. Гораздо полезнее сравнивать по нескольким критериям одновременно: где вам важнее контроль, где важнее стандартизация, где важнее объяснимость для команды, а где важнее скорость и «меньше ручных решений».

Ниже — таблица, которую можно держать как шпаргалку. Она не «истина навсегда», но для уровня курса и для типичного Spring Boot сервиса работает отлично.

Критерий Dockerfile Buildpacks (bootBuildImage) Как это ощущается на практике
Прозрачность шагов сборки Максимальная: шаги видны в Dockerfile Шаги «внутри» buildpacks, глазами их не прочитать как текст Dockerfile легче объяснить новичку как сценарий, buildpacks — как процесс по правилам
Скорость старта (в новом проекте) Часто медленнее: нужно написать и отладить Dockerfile Часто быстрее: одна задача, минимум ручного boilerplate Buildpacks дают быстрый «первый взрослый образ» без танцев
Стандартизация между сервисами Нужно дисциплинировать вручную (шаблоны, ревью) По умолчанию выше: одинаковые правила упаковки Особенно заметно, когда в компании 10+ Spring Boot сервисов
Контроль над базой и слоями Полный, вплоть до мелочей Ограниченный, но есть точки настройки (builder/run image, env) Если вам нужно «вот именно так и никак иначе» — Dockerfile проще
Кэш и слойность Полностью ваша ответственность Часто «из коробки» хорошо для Spring Boot Важный момент: результат всё равно слоистый и кешируемый
Сопровождение и дебаг сборки Вы дебажите свой Dockerfile Вы дебажите buildpacks-процесс В Dockerfile вы чините рецепт, в buildpacks — пытаетесь понять, что выбрал «автомат»
Воспроизводимость Достижима, если фиксировать базовые образы и версии Достижима, если фиксировать builder и договориться о правилах В обоих случаях без дисциплины будет лотерея
Цена обучения команды Нужно учить Dockerfile и мышление слоями Нужно учить, что делает bootBuildImage, и как читать результат Buildpacks не отменяют Docker-грамотность, просто меняют место контроля

Пока что не делаем финального «вывода кто лучше». Мы просто фиксируем: подходы решают одну задачу, но оптимизируют разные боли.

4. Как исследовать собранный образ

На этом месте часто рождается миф: «если сборка через buildpacks — это чёрный ящик, значит образ нельзя анализировать». На практике это неверно. Да, шаги сборки вы не читаете как текст в репозитории, но результат — всё равно обычный Docker-образ, и вы можете исследовать его обычными командами. Просто вместо чтения Dockerfile вы будете чаще читать вывод docker image inspect и docker image history.

Начнём с простого. После сборки через buildpacks вы можете посмотреть, чем запускается контейнер, то есть Entrypoint:

# Смотрим, чем реально запускается контейнер (Entrypoint/Cmd) внутри образа
docker image inspect docker-java-catalog-service:buildpacks \
  --format 'Entrypoint={{json .Config.Entrypoint}} Cmd={{json .Config.Cmd}}'
# Конкретное значение зависит от builder/stack/version.
# Часто увидите что-то вроде:
# Entrypoint=["/cnb/lifecycle/launcher"] Cmd=[]
# или:
# Entrypoint=["/cnb/process/web"] Cmd=[]

Даже если вы раньше не видели эти значения, уже полезно, что они не прячутся. Buildpacks положили в образ свой runtime launcher/process wrapper, и это можно увидеть обычной Docker-командой. В buildpacks-пути прозрачность смещается в сторону результата.

Точно так же вы можете посмотреть переменные окружения, которые образ несёт внутри себя:

# Смотрим окружение внутри образа: часто там лежат подсказки про JVM-флаги и PATH
docker image inspect docker-java-catalog-service:buildpacks \
  --format '{{json .Config.Env}}'
# ["PATH=...","JAVA_TOOL_OPTIONS=...","..."]

Это хороший способ понять, почему, например, JVM стартует с какими-то параметрами. Не нужно угадывать — можно посмотреть.

А если хочется увидеть слойность, то есть «из каких кусков образ собран», подойдёт docker image history:

# Смотрим «историю» образа: слои, их размер и порядок
docker image history docker-java-catalog-service:buildpacks
# (увидите список слоёв и их размер)

Важно поймать правильное ощущение: Dockerfile-путь прозрачен по шагам, buildpacks-путь прозрачен по результату. В жизни вы всё равно будете смотреть и на то, и на другое, просто со смещением акцента.

5. Сопровождение: команда и рутина

Когда проект один и учебный, хочется всё сделать руками «чтобы понимать». Но как только появляется команда, несколько сервисов и реальная рутина, возникает прагматичный вопрос: «а сколько это стоит в поддержке?» И вот здесь Dockerfile и buildpacks начинают вести себя очень по-разному — не потому что один плохой, а потому что они рассчитаны на разные привычки управления.

Dockerfile — это как личный шеф-повар: вы получаете блюдо именно таким, как хотите, но вам надо уметь готовить, и уметь объяснять рецепт другим. Если вы сделали отличный Dockerfile и закрепили его как канон, команда живёт хорошо. Если Dockerfile у каждого «чуть свой», начинается цирк: кто-то добавил лишний RUN, кто-то случайно сломал кэш, кто-то сменил базовый образ на «потому что видел в статье».

Buildpacks — это ближе к «стандартизированной кухне»: правила упаковки общие, часть решений принимается за вас, и в среднем вы меньше времени тратите на поддержание десятка похожих Dockerfile. Особенно это полезно, когда у вас много типовых Spring Boot сервисов, и вы хотите, чтобы они собирались одинаково без вечного копипаста.

Но есть обратная сторона. Когда вам понадобится сделать что-то не очень типовое, Dockerfile обычно «честнее». Не потому что buildpacks «не умеют», а потому что вы упираетесь в философию: buildpacks оптимизируют стандартный путь и не хотят превращаться в бесконечный конструктор «а давайте ещё вот этот хитрый шаг». В Dockerfile вы можете явно выразить любые необычные требования к сборке, к файловой структуре, к старту процесса. В buildpacks вы будете искать, как «впихнуть» своё требование в доступные точки настройки, и иногда это становится отдельным мини-квестом.

Самая практичная формулировка для Junior-уровня звучит так: Dockerfile требует больше дисциплины и знаний, но даёт максимальный контроль; buildpacks требуют меньше ручной рутины, но уменьшают видимость конкретных шагов упаковки.

6. Честное сравнение на Container-Ready Catalog Service

Сравнение «по ощущениям» обычно заканчивается тем, что побеждает тот, кто громче вздыхает. Чтобы сравнение было инженерным, нужно держать одинаковыми три вещи: один и тот же сервис, одно и то же состояние кода, одна и та же проверка запуска. И ещё один важный момент: тег :dockerfile здесь означает именно текущий manual baseline проекта, а не какой-то новый упрощённый Dockerfile «для сравнения».

Представим, что у нас в репозитории docker-java-catalog-service настроено имя buildpacks-образа как в примерах выше, например docker-java-catalog-service:buildpacks. Тогда честный сценарий выглядит примерно так.

Сборка Dockerfile-пути (мы явно тегируем, чтобы не путаться):

# Собираем образ по Dockerfile и сразу ставим понятный тег
docker build -t docker-java-catalog-service:dockerfile .

Сборка buildpacks-пути:

# Собираем образ через buildpacks (Spring Boot Gradle Plugin)
./gradlew bootBuildImage

Дальше запускаем оба образа одинаково, меняя только порт на host (чтобы не конфликтовать). Например, Dockerfile-образ — на 8080:

# Запускаем контейнер из Dockerfile-образа
docker run --rm -p 8080:8080 docker-java-catalog-service:dockerfile

А buildpacks-образ — на 8081:

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

И теперь проверка должна быть одинаковой по смыслу. Мы не придумываем «особые проверки» для buildpacks. Мы делаем то же самое, что уже делали на ранних днях курса: дергаем один и тот же endpoint.

# Проверяем, что сервис живой (actuator health)
curl -s http://localhost:8080/actuator/health
# {"status":"UP",...}

curl -s http://localhost:8081/actuator/health
# {"status":"UP",...}

Дальше вы можете сравнить слои и метаданные:

# Сравниваем, из чего состоят образы, и чем они отличаются по слоям
docker image history docker-java-catalog-service:dockerfile
docker image history docker-java-catalog-service:buildpacks

И ещё раз зафиксировать главную мысль дня: оба пути дают runnable container image, и различие — в том, где описана логика упаковки и кто принимает решения.

7. Типичные ошибки при выборе подхода

На этом этапе студенты часто начинают выбирать «что красивее», а потом удивляются, почему сборка стала непредсказуемой, а команда спорит о вкусе вместо того, чтобы выпускать фичи. Ошибки здесь обычно не технические, а методические: неправильно поставили вопрос, а потом героически победили не ту проблему. Давайте проговорим самые частые грабли, чтобы вы на них наступили хотя бы в учебной среде, а не на работе.

Ошибка №1: выбирать подход по принципу “один раз понравилось — значит всегда так”.
Иногда человек попробовал bootBuildImage, увидел, что образ собрался, и решил: «ну всё, Dockerfile больше не нужен». Или наоборот: написал Dockerfile и решил: «всё, buildpacks — для слабаков». На практике подход выбирают не по эмоции, а по требованиям: нужен ли вам полный контроль над шагами упаковки, или вы хотите стандартный путь с меньшим количеством ручных решений.

Ошибка №2: сравнивать Dockerfile и buildpacks на разных входных условиях.
Очень легко случайно сравнить «Dockerfile-образ после вчерашних оптимизаций» с «buildpacks-образом, собранным на другом состоянии проекта», а потом сделать выводы о скорости и размере. Такой эксперимент похож на сравнение двух машин, где в одной полный бак и новая резина, а во второй — пустой бак и проколотое колесо. В инженерном сравнении важно держать одинаковыми версию кода, настройки запуска и способ проверки результата.

Ошибка №3: ожидать, что bootBuildImage «учтёт» ваш Dockerfile.
Это типичная путаница: студент думает, что buildpacks — это «просто другой способ вызвать Docker build», и ждёт, что Gradle-задача прочитает Dockerfile. Она не читает. Dockerfile — это отдельный путь. Buildpacks — отдельный путь. Если вы хотите изменить buildpacks-сборку, вы изменяете настройки bootBuildImage, а не Dockerfile.

Ошибка №4: не фиксировать имя образа и потом “искать свой образ глазами”.
Если не задать imageName, вы легко получите ситуацию «я что-то собрал, но что именно и как оно называется — непонятно». В учебном проекте это превращается в путаницу тегов, а в реальном — в хаос, когда команда не может договориться, какой образ запускать. Привычка явно задавать imageName в Gradle — это маленькая вещь, которая экономит много нервов.

Ошибка №5: думать, что buildpacks отменяют необходимость понимать Docker.
Buildpacks убирают часть рутины, но результат всё равно живёт в Docker-мире: образ, слои, контейнер, логи, порты. Если контейнер не стартует, вы всё равно будете смотреть docker logs и docker inspect. Поэтому buildpacks — это не «магия вместо знаний», а «другой уровень автоматизации при сохранении того же результата».

1
Задача
Docker for Spring, 9 уровень, 3 лекция
Недоступна
Один сервис, два packaging path
Один сервис, два packaging path
1
Задача
Docker for Spring, 9 уровень, 3 лекция
Недоступна
Скрипт краткого сравнения двух образов
Скрипт краткого сравнения двух образов
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ