← назад к разделу

Spring Boot умеет упаковываться в исполняемый jar — файл, который содержит и код приложения, и все зависимости, и встроенный Tomcat. Этот jar легко запустить одной командой. Добавить Docker сюда несложно, но есть несколько решений, которые сразу поставят вас на правильный путь — и несколько ошибок, которые чаще всего делают новички.

Почему нельзя просто скопировать jar в образ и на этом остановиться

Технически — можно. Вот самый простой Dockerfile, который работает:

FROM eclipse-temurin:21-jre
COPY build/libs/app.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]

Он соберёт образ, и приложение запустится. Проблема в другом: каждый раз, когда вы меняете одну строку кода, Docker пересобирает слой с jar заново. Jar весит 50–150 МБ и содержит ваши зависимости (Spring Boot, библиотеки) — они не менялись, но Docker об этом не знает и загружает весь файл заново. В команде, где образы раскатываются часто, это превращается в медленные сборки и потерянный трафик.

Решение — разделить зависимости и код приложения на разные слои.

Базовый образ: JRE, не JDK

Первое решение, которое принимают при написании Dockerfile, — выбор базового образа.

JDK (Java Development Kit) — полный набор инструментов для разработки: компилятор, отладчик, инструменты для профилирования. Он нужен, чтобы писать и компилировать Java-код.

JRE (Java Runtime Environment) — только среда выполнения. Её достаточно, чтобы запускать уже скомпилированное приложение.

В продакшене вы запускаете готовый jar — компилятор не нужен. Поэтому используйте JRE:

FROM eclipse-temurin:21-jre

eclipse-temurin — это дистрибутив OpenJDK от проекта Eclipse Adoptium, один из наиболее распространённых и поддерживаемых образов для Java в Docker. Указывать конкретную версию (21-jre) — хорошая практика: образ latest может в любой момент перейти на следующую мажорную версию и сломать сборку.

Использование JDK вместо JRE в продакшене — типичная ошибка. Образ становится тяжелее на 200–300 МБ без всякой пользы, а поверхность атаки увеличивается.

Правильный порядок инструкций: кэш зависимостей

Docker собирает образ послойно. Каждая инструкция (COPY, RUN, ENV и другие) создаёт новый слой. Если слой не изменился — Docker берёт его из кэша и не пересобирает. Слои кэшируются сверху вниз: как только один слой изменился, все слои ниже пересобираются заново.

Это значит: то, что меняется редко, должно идти выше; то, что меняется часто — ниже.

Зависимости (Spring Boot, библиотеки) меняются редко — обычно раз в несколько недель или месяцев. Код приложения меняется при каждом коммите. Если положить их в один слой, Docker будет перекачивать все зависимости при каждой сборке.

Вот шаблон Dockerfile с разделением на слои:

FROM eclipse-temurin:21-jre

WORKDIR /app

# Зависимости — в отдельный слой (меняются редко, долго кэшируются)
COPY build/libs/dependencies/ ./dependencies/
COPY build/libs/spring-boot-loader/ ./spring-boot-loader/
COPY build/libs/snapshot-dependencies/ ./snapshot-dependencies/

# Код приложения — последним (меняется при каждой сборке)
COPY build/libs/application/ ./application/

ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]

Но этот подход требует, чтобы Gradle или Maven распаковал jar на слои перед сборкой. Об этом — в следующем разделе.

Layered jars: встроенная поддержка в Spring Boot

Начиная с Spring Boot 2.3, в плагине сборки есть встроенная поддержка layered jars — исполняемых jar, внутри которых содержимое уже разложено по папкам-слоям.

В build.gradle включите слойную сборку:

bootJar {
    layered {
        enabled = true
    }
}

После этого ./gradlew bootJar создаст jar, из которого можно извлечь слои командой:

java -Djarmode=tools -jar build/libs/app.jar extract --layers --launcher

В рабочей директории появятся папки: dependencies/, spring-boot-loader/, snapshot-dependencies/, application/. Теперь Dockerfile может копировать их по отдельности — и Docker будет кэшировать каждый слой независимо.

Полный пример сборки с layered jars:

FROM eclipse-temurin:21-jre AS builder

WORKDIR /app

# Копируем jar и распаковываем слои
COPY build/libs/app.jar app.jar
RUN java -Djarmode=tools -jar app.jar extract --layers --launcher --destination extracted

# Финальный образ — только то, что нужно для запуска
FROM eclipse-temurin:21-jre

WORKDIR /app

# Копируем слои в правильном порядке: редко меняемые — первыми
COPY --from=builder /app/extracted/dependencies/ ./
COPY --from=builder /app/extracted/spring-boot-loader/ ./
COPY --from=builder /app/extracted/snapshot-dependencies/ ./
COPY --from=builder /app/extracted/application/ ./

ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]

Здесь используется многоэтапная сборка (FROM ... AS builder): первый этап распаковывает jar, второй берёт только нужные файлы. Итоговый образ не содержит исходного jar и промежуточных инструментов.

Передача профиля и переменных среды

Spring Boot определяет активный профиль через переменную SPRING_PROFILES_ACTIVE. В Docker её удобно передавать при запуске контейнера — не хардкодить в Dockerfile:

docker run -e SPRING_PROFILES_ACTIVE=prod myapp:latest

Или в docker-compose.yml:

services:
  app:
    image: myapp:latest
    environment:
      SPRING_PROFILES_ACTIVE: prod
      DB_URL: jdbc:postgresql://db:5432/mydb
      DB_PASSWORD: secret

Строка подключения к базе, секреты, порты — всё это лучше передавать через переменные среды, а не запекать в образ. Образ должен быть универсальным: один и тот же образ запускается в тестовом окружении с одними переменными и в продакшене с другими.

Если нужно задать переменные по умолчанию прямо в Dockerfile, используйте ENV:

ENV SERVER_PORT=8080
ENV JAVA_OPTS=""

А запуск можно параметризовать через JAVA_OPTS:

ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]

Альтернативы ручному Dockerfile

Ручной Dockerfile — не единственный способ упаковать Spring Boot приложение в образ.

Cloud Native Buildpacks встроены в Spring Boot плагин начиная с версии 2.3. Они анализируют проект и автоматически создают оптимизированный образ — без Dockerfile:

./gradlew bootBuildImage

Образ создаётся по лучшим практикам: JRE вместо JDK, разделение на слои, настройка памяти JVM. Подходит, если вы не хотите вручную поддерживать Dockerfile.

Jib — плагин от Google, который собирает образ напрямую из исходников Java, не требуя ни Docker daemon на машине, ни Dockerfile:

./gradlew jibDockerBuild

Jib сам определяет слои и собирает образ инкрементально. Удобен для CI/CD-конвейеров, где Docker может быть недоступен.

Оба инструмента подходят для стандартных случаев. Ручной Dockerfile даёт больше контроля — например, когда нужно установить системные зависимости или тонко настроить параметры JVM.

Типичные ошибки

Весь проект одним слоем. COPY . . в начале Dockerfile копирует всё — исходники, тесты, конфигурацию Gradle — в образ. Docker пересобирает этот слой при любом изменении. Копируйте только то, что нужно для запуска: собранный jar или распакованные слои.

Full JDK в продакшене. Образ с JDK тяжелее и содержит лишние инструменты. Используйте eclipse-temurin:21-jre.

Секреты в образе. Не передавайте пароли через ENV в Dockerfile — они попадут в историю слоёв образа и будут видны при docker history. Используйте переменные среды при запуске или Docker Secrets.

Тег latest. Образ eclipse-temurin:latest или myapp:latest нестабилен: при следующей сборке вы можете получить другую версию. В продакшене всегда указывайте конкретный тег.

Коротко

  • Используйте eclipse-temurin:21-jre — JRE достаточно для запуска, JDK в продакшене не нужен.
  • Разделяйте зависимости и код приложения на разные слои — Docker кэширует слои, и пересборка ускоряется в разы.
  • ENTRYPOINT ["java", "-jar", "app.jar"] — базовый запуск; для layered jars используйте JarLauncher.
  • Профиль и конфигурацию передавайте через переменные среды при запуске, не запекайте в образ.
  • Layered jars включаются в build.gradle и дают разделение на слои без лишнего кода.
  • Buildpacks и Jib — автоматические альтернативы ручному Dockerfile, подходят для большинства случаев.
  • Не кладите секреты в образ: они попадают в историю слоёв.

Что почитать дальше

  • Образы и Dockerfile — как устроены образы, что такое слои и как Docker использует кэш при сборке.
  • Многоэтапная сборка и оптимизация слоёв — подробно о многоэтапных Dockerfile и стратегиях уменьшения размера образа.
  • JVM внутри контейнера — как JVM видит ресурсы контейнера, настройка памяти и проблемы с cgroups.