Контейнеризация Java приложений с Docker: пошаговое руководство

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Докер, Java и почему это меняет правила игры

Представьте: вы написали крутое Java-приложение. Оно работает на вашем MacBook, но когда вы отдаёте его коллеге на Windows — всё ломается. Знакомо? А если сервер на Linux? Тут даже гадать не надо — классическая история "на моей машине работает".

Контейнеризация с Docker решает эту проблему раз и навсегда. Вы упаковываете приложение вместе с его окружением (JVM, библиотеки, зависимости) в лёгкий контейнер, который запускается одинаково везде. Для Java это особенно актуально — ведь мы привыкли к "Write Once, Run Anywhere", но на практике эта идиома часто даёт сбой из-за версий JDK, системных библиотек или конфигураций.

В этой статье мы разберём контейнеризацию Java приложений с Docker от А до Я. Вы узнаете, как написать Dockerfile, оптимизировать образ, использовать Docker Compose и не наступить на типичные грабли. Поехали!

Что такое Docker и зачем он Java-разработчику?

Docker — это платформа для упаковки, доставки и запуска приложений в изолированных средах — контейнерах. В отличие от виртуальных машин, контейнеры используют общее ядро ОС, но имеют своё файловое пространство, сеть и процессы. Это делает их лёгкими (мегабайты, а не гигабайты) и быстрыми (запуск за секунды).

Для Java-разработчика Docker даёт три суперсилы:

  • Воспроизводимость — больше никаких "у меня работает". Если контейнер запустился локально, он запустится на любом сервере.
  • Изоляция — каждое приложение живёт в своём мире, не мешая другим. Хотите два Java-приложения с разными версиями JDK? Легко.
  • Масштабирование — контейнеры легко клонировать, балансировать и оркестрировать (Kubernetes, Docker Swarm).

Первый Dockerfile для Java-приложения

Начнём с простого. У нас есть стандартное Java-приложение (например, Spring Boot или просто Main.java). Мы хотим упаковать его в Docker-образ. Самый простой способ — использовать официальный образ OpenJDK.

Вот минимальный Dockerfile:

# Используем официальный образ OpenJDK 17
FROM openjdk:17-jdk-alpine

# Устанавливаем рабочую директорию
WORKDIR /app

# Копируем JAR-файл в контейнер
COPY target/my-app.jar /app/my-app.jar

# Запускаем приложение
CMD ["java", "-jar", "my-app.jar"]

Теперь соберём образ и запустим контейнер:

# Сборка образа
docker build -t my-java-app .

# Запуск контейнера
docker run -p 8080:8080 my-java-app

Всё работает. Но это плохой пример. Почему? Давайте разберёмся.

Проблема №1: Размер образа

Образ openjdk:17-jdk-alpine весит около 150 МБ. Это нормально, но если вы добавите туда все зависимости, размер может вырасти до 500+ МБ. Для продакшена это много.

Проблема №2: Сборка и рантайм в одном образе

Мы копируем готовый JAR, а значит, сборку (mvn clean package) нужно делать заранее. Лучше использовать многостадийную сборку (multi-stage build), чтобы отделить этап компиляции от этапа выполнения.

Оптимизация: многостадийная сборка (multi-stage build)

Идея простая: на первом этапе мы используем образ с Maven и JDK, собираем проект, а на втором — только JRE и готовый JAR. Это уменьшает финальный образ в разы.

# Этап сборки
FROM maven:3.8.5-openjdk-17 AS builder
WORKDIR /build
COPY pom.xml .
COPY src ./src
RUN mvn clean package -DskipTests

# Этап выполнения
FROM openjdk:17-jre-alpine
WORKDIR /app
COPY --from=builder /build/target/my-app.jar /app/my-app.jar
EXPOSE 8080
CMD ["java", "-jar", "my-app.jar"]

Что изменилось?

  • Первый этап (builder) содержит Maven и JDK — он нужен только для компиляции.
  • Второй этап использует лёгкий JRE-образ (~50 МБ) и копирует только JAR.
  • Финальный образ весит ~120 МБ вместо 400+.

Важно: для Spring Boot приложений используйте openjdk:17-jre-alpine или ещё лучше — eclipse-temurin:17-jre-alpine (поддерживается сообществом).

Работа с переменными окружения и конфигурацией

Java-приложения часто используют конфигурационные файлы (application.properties, application.yml). В контейнере лучше передавать настройки через переменные окружения. Это делает образ универсальным — один и тот же JAR можно запустить с разными базами данных или API-ключами.

Пример Dockerfile с поддержкой переменных:

FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY target/my-app.jar /app/my-app.jar

# Объявляем переменные (значения по умолчанию)
ENV DB_HOST=localhost \
    DB_PORT=5432 \
    APP_PROFILE=prod

EXPOSE 8080

# Используем переменные в команде запуска
CMD java -jar -Dspring.profiles.active=${APP_PROFILE} \
             -Ddb.host=${DB_HOST} \
             -Ddb.port=${DB_PORT} \
             my-app.jar

Запуск с переопределением:

docker run -e DB_HOST=my-db-server -e APP_PROFILE=dev -p 8080:8080 my-java-app

Docker Compose: поднимаем стек целиком

Редко когда Java-приложение живёт само по себе. Обычно рядом есть PostgreSQL, Redis, Kafka. Docker Compose позволяет описать всю инфраструктуру в одном YAML-файле и запустить одной командой.

Пример docker-compose.yml для Spring Boot + PostgreSQL:

version: '3.8'

services:
  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      - DB_HOST=db
      - DB_PORT=5432
      - DB_NAME=mydb
      - DB_USER=user
      - DB_PASSWORD=secret
    depends_on:
      - db

  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: mydb
      POSTGRES_USER: user
      POSTGRES_PASSWORD: secret
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:

Теперь одной командой docker-compose up -d вы поднимаете и приложение, и базу данных, и всё это в изолированной сети.

Типичные ошибки при контейнеризации Java

Даже опытные разработчики иногда спотыкаются. Вот топ-5 граблей:

  • С

Блоги

Рекомендаций по книгам