Введение в Docker и Java: почему контейнеризация меняет разработку
Современная разработка программного обеспечения немыслима без контейнеризации. Docker стал стандартом де-факто для упаковки, доставки и запуска приложений. Для Java-разработчиков использование Docker решает ключевые проблемы: "это работает на моей машине" и сложности с настройкой окружения. В этой статье мы разберём, как контейнеризировать Java-приложения, от простого JAR-файла до сложного микросервиса на Spring Boot.
Docker позволяет изолировать приложение вместе с его зависимостями (JVM, библиотеки, системные утилиты) в лёгкий переносимый контейнер. Это особенно важно для Java, где версия JDK, настройки памяти и системные библиотеки могут сильно влиять на поведение программы.
Что вы узнаете из этой статьи
- Создание Dockerfile для Java-приложений
- Оптимизация размеров образов
- Работа с многомодульными проектами Maven/Gradle
- Лучшие практики безопасности и производительности
1. Основы: создание Dockerfile для Java-приложения
Начнём с простого примера. Предположим, у вас есть Java-приложение, собранное в JAR-файл. Вот минимальный Dockerfile, который упакует его в контейнер:
# Используем официальный образ OpenJDK
FROM openjdk:17-jdk-slim
# Устанавливаем рабочую директорию
WORKDIR /app
# Копируем JAR-файл в контейнер
COPY target/myapp.jar app.jar
# Открываем порт, который слушает приложение
EXPOSE 8080
# Команда для запуска
ENTRYPOINT ["java", "-jar", "app.jar"]
Этот Dockerfile делает три простых вещи: берёт базовый образ с JDK 17, копирует скомпилированный JAR и запускает его. Однако в реальных проектах такой подход неэффективен — каждый раз при изменении кода Docker пересобирает весь образ, включая копирование JAR.
Многоступенчатая сборка (Multi-stage build)
Для разделения этапов сборки и выполнения используйте многоступенчатые Dockerfile. Это уменьшает финальный образ, исключая инструменты сборки (Maven, Gradle, JDK для компиляции).
# Этап сборки
FROM maven:3.9.6-eclipse-temurin-17 AS builder
WORKDIR /build
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn clean package -DskipTests
# Финальный этап
FROM openjdk:17-jdk-slim
WORKDIR /app
COPY --from=builder /build/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
Первый этап использует полный Maven-образ для компиляции, второй — минимальный JRE-образ для запуска. Финальный образ весит ~200 МБ вместо 700+ МБ.
2. Оптимизация образов: уменьшаем размер и ускоряем сборку
Большие образы замедляют развёртывание и увеличивают время загрузки. Вот несколько техник для Java:
Используйте JLink для создания минимальной JRE
Начиная с Java 9, вы можете создать кастомную JRE, содержащую только необходимые модули. Это радикально уменьшает размер образа.
FROM openjdk:17-jdk-slim AS jre-builder
RUN jlink \
--add-modules java.base,java.logging,java.sql \
--strip-debug \
--no-man-pages \
--no-header-files \
--compress=2 \
--output /jre
FROM debian:bookworm-slim
COPY --from=jre-builder /jre /jre
WORKDIR /app
COPY target/myapp.jar app.jar
ENV PATH="/jre/bin:${PATH}"
ENTRYPOINT ["java", "-jar", "app.jar"]
Такой образ может весить всего 40-60 МБ вместо 200+ МБ.
Кэширование слоёв Docker
Docker кэширует каждый слой. Чтобы ускорить сборку, размещайте редко изменяемые файлы (зависимости) в начале Dockerfile, а часто изменяемые (исходный код) — в конце.
FROM maven:3.9.6-eclipse-temurin-17 AS builder
WORKDIR /build
# Сначала копируем только pom.xml и скачиваем зависимости
COPY pom.xml .
RUN mvn dependency:go-offline
# Затем копируем исходники и собираем
COPY src ./src
RUN mvn clean package
Теперь, если вы измените только код, Docker пересоберёт только последние два слоя, а не все зависимости.
3. Docker Compose: запуск Java-приложения с зависимостями
Реальные приложения редко работают в одиночку — им нужна база данных, кеш или очередь сообщений. Docker Compose позволяет описать всю инфраструктуру в одном YAML-файле.
version: '3.8'
services:
app:
build: .
ports:
- "8080:8080"
environment:
- SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/mydb
- SPRING_DATASOURCE_USERNAME=user
- SPRING_DATASOURCE_PASSWORD=pass
depends_on:
- db
db:
image: postgres:16-alpine
environment:
- POSTGRES_DB=mydb
- POSTGRES_USER=user
- POSTGRES_PASSWORD=pass
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
Этот Compose-файл поднимает ваше Java-приложение и PostgreSQL. Сервис app использует depends_on, чтобы гарантировать, что база данных запустится первой (но не дожидается её готовности — для этого нужны healthcheck).
Работа с переменными окружения
Java-приложения, особенно на Spring Boot, активно используют переменные окружения для конфигурации. В Dockerfile или Compose вы можете задать их:
# В Dockerfile
ENV JAVA_OPTS="-Xmx512m -Xms256m"
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
Или в Docker Compose:
environment:
- JAVA_OPTS=-Xmx512m -Xms256m
- SPRING_PROFILES_ACTIVE=production
4. Безопасность и лучшие практики для Java-контейнеров
Контейнеризация не отменяет необходимость безопасности. Вот ключевые моменты для Java:
Не запускайте контейнер от root
По умолчанию