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.