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

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

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

Начать курс

Docker и Kotlin: контейнеризация приложений на продвинутом уровне

Контейнеризация стала неотъемлемой частью современной разработки, и Kotlin — не исключение. Если вы уже освоили базовые Dockerfile и запуск простых JAR-файлов, пришло время перейти на следующий уровень. В этой статье мы рассмотрим продвинутые техники контейнеризации Kotlin-приложений: многоступенчатую сборку (multi-stage builds), оптимизацию размера образов, настройку для фреймворков Ktor и Spring Boot, а также работу с Docker Compose в production-окружении.

1. Многоступенчатая сборка Kotlin-приложений

Одна из главных проблем при контейнеризации JVM-приложений — размер финального образа. Стандартный образ OpenJDK может весить более 400 МБ. Многоступенчатая сборка позволяет отделить среду сборки от среды выполнения, используя минимальный runtime-образ.

Пример для Gradle-проекта на Kotlin

# Первый этап: сборка
FROM gradle:8.5-jdk21 AS builder
WORKDIR /app
COPY build.gradle.kts settings.gradle.kts ./
COPY src ./src
RUN gradle build --no-daemon -x test

# Второй этап: выполнение
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=builder /app/build/libs/app.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

Использование eclipse-temurin:21-jre-alpine вместо полного JDK-образа сокращает размер с 400+ МБ до ~120 МБ. Для Kotlin-приложений на Ktor или Spring Boot этого более чем достаточно.

Оптимизация кэширования слоёв

Чтобы ускорить повторные сборки, копируйте зависимости отдельно от исходного кода:

FROM gradle:8.5-jdk21 AS builder
WORKDIR /app
# Сначала зависимости (кэшируется, если не менялись)
COPY build.gradle.kts settings.gradle.kts ./
RUN gradle dependencies --no-daemon
# Затем исходники
COPY src ./src
RUN gradle build --no-daemon -x test

Этот приём особенно полезен в CI/CD, где каждая секунда на счету.

2. Оптимизация образов для Kotlin-приложений

Помимо выбора базового образа, существуют дополнительные техники для уменьшения размера и повышения безопасности.

Использование JLink для создания кастомной JRE

Если ваше приложение использует только часть модулей JDK, можно создать минимальную JRE:

# В builder-этапе
RUN jlink --module-path /opt/java/openjdk/jmods \
          --add-modules java.base,java.logging,java.naming,java.sql \
          --output /custom-jre \
          --strip-debug --no-man-pages --no-header-files

# В runtime-этапе
FROM alpine:3.19
COPY --from=builder /custom-jre /opt/jre
COPY --from=builder /app/build/libs/app.jar /app/app.jar
ENV PATH="/opt/jre/bin:${PATH}"
EXPOSE 8080
CMD ["java", "-jar", "/app/app.jar"]

Размер образа может снизиться до 50-70 МБ.

Многослойная архитектура с Spring Boot

Spring Boot 3+ поддерживает разбиение fat JAR на слои. Используйте это в Dockerfile:

FROM eclipse-temurin:21-jre-alpine AS builder
WORKDIR /app
COPY build/libs/*.jar app.jar
RUN java -Djarmode=layertools -jar app.jar extract

FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=builder /app/dependencies/ ./
COPY --from=builder /app/spring-boot-loader/ ./
COPY --from=builder /app/snapshot-dependencies/ ./
COPY --from=builder /app/application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]

Теперь при изменении кода пересобирается только слой application, а зависимости и Spring Boot loader остаются закэшированными.

3. Контейнеризация Ktor-приложений

Ktor — лёгкий фреймворк для создания асинхронных серверов на Kotlin. Его контейнеризация имеет свои особенности.

Dockerfile для Ktor с Netty

FROM gradle:8.5-jdk21 AS builder
WORKDIR /app
COPY build.gradle.kts settings.gradle.kts ./
RUN gradle dependencies --no-daemon
COPY src ./src
RUN gradle buildFatJar --no-daemon

FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=builder /app/build/libs/*-all.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-Xmx256m", "-Xms128m", "-jar", "app.jar"]

Обратите внимание на флаги -Xmx и -Xms. В контейнерах важно ограничивать память JVM, чтобы избежать OOM-kill.

Docker Compose для Ktor + PostgreSQL

version: '3.8'
services:
  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      - DB_URL=jdbc:postgresql://db:5432/ktor_db
      - DB_USER=user
      - DB_PASSWORD=pass
    depends_on:
      db:
        condition: service_healthy
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: ktor_db
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user -d ktor_db"]
      interval: 5s
      timeout: 5s
      retries: 5

Использование healthcheck гарантирует, что приложение запустится только после готовности базы данных.

4. Продвинутые техники: Jib и Distroless-образы

Если вы хотите полностью отказаться от Dockerfile, используйте плагин Jib для Gradle. Он собирает оптимизированные образы без Docker daemon.

Настройка Jib в build.gradle.kts

plugins {
    id("com.google.cloud.tools.jib") version "3.4.0"
}

jib {
    from {
        image = "eclipse-temurin:21-jre-alpine"
    }
    to {
        image = "myregistry/myapp:latest"
    }
    container {
        jvmFlags = listOf("-Xmx256m", "-Xms128m")
        ports = listOf("8080")
    }
}

Запуск сборки: ./gradlew jibDockerBuild. Jib автомати

Блоги

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