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

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

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

Начать курс

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

Современная разработка на Kotlin всё чаще требует использования контейнеризации. Docker позволяет упаковать приложение вместе со всеми зависимостями в изолированную среду, что гарантирует одинаковое поведение на локальной машине разработчика, тестовом сервере и в production. В этой статье мы разберём, как правильно контейнеризировать Kotlin-проекты, используя лучшие практики и примеры кода.

Зачем контейнеризировать Kotlin-приложения?

Kotlin работает поверх JVM, поэтому приложения на нём наследуют все особенности Java-экосистемы. Основные преимущества контейнеризации:

  • Воспроизводимость окружения — избегаем проблем "на моей машине работает".
  • Изоляция зависимостей — версии JDK, библиотек и системных утилит фиксируются в образе.
  • Масштабирование — контейнеры легко разворачивать в Kubernetes и других оркестраторах.
  • Быстрое развёртывание — образы Docker меньше по размеру, чем виртуальные машины.

Создание Dockerfile для Kotlin-приложения

Рассмотрим типичный проект на Kotlin с использованием Gradle. Наш build.gradle.kts может выглядеть так:

plugins {
    kotlin("jvm") version "1.9.22"
    application
}

application {
    mainClass.set("com.example.MainKt")
}

dependencies {
    implementation(kotlin("stdlib"))
    // другие зависимости
}

Теперь создадим Dockerfile. Первый вариант — многоступенчатая сборка (multistage build), которая отделяет этап компиляции от этапа выполнения:

# Этап сборки
FROM gradle:8.5-jdk21 AS build
COPY --chown=gradle:gradle . /home/gradle/src
WORKDIR /home/gradle/src
RUN gradle build --no-daemon

# Этап выполнения
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=build /home/gradle/src/build/libs/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

Разберём ключевые моменты:

  • gradle:8.5-jdk21 — официальный образ с Gradle и JDK для сборки.
  • eclipse-temurin:21-jre-alpine — лёгкий образ JRE на Alpine Linux (около 80 МБ).
  • COPY --from=build — копируем только собранный JAR, исключая исходники и кэш Gradle.
  • EXPOSE 8080 — документально указываем порт (не открывает его автоматически).

Для сборки образа выполните:

docker build -t kotlin-app:latest .
docker run -p 8080:8080 kotlin-app:latest

Оптимизация размера образа

Размер образа критичен для скорости развёртывания. Вот несколько приёмов для Kotlin-приложений:

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

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

# Этап сборки
FROM gradle:8.5-jdk21 AS build
COPY . /app
WORKDIR /app
RUN gradle build --no-daemon

# Этап создания кастомной JRE
FROM eclipse-temurin:21-jdk-alpine AS jre-builder
RUN jlink \
    --add-modules java.base,java.logging,java.naming,java.sql \
    --strip-debug \
    --no-man-pages \
    --no-header-files \
    --compress=2 \
    --output /jre

# Финальный этап
FROM alpine:3.19
ENV JAVA_HOME=/jre
ENV PATH="${JAVA_HOME}/bin:${PATH}"
COPY --from=jre-builder /jre /jre
COPY --from=build /app/build/libs/*.jar /app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app.jar"]

Этот подход уменьшает размер образа с ~200 МБ до ~40-50 МБ.

2. Использование Spring Boot layertools

Для проектов на Spring Boot (с Kotlin) можно разделить JAR на слои для кэширования Docker:

# build.gradle.kts
tasks.bootJar {
    layered {
        enabled = true
    }
}

В Dockerfile копируйте слои отдельно, чтобы изменения в коде не сбрасывали кэш зависимостей:

COPY --from=build /app/build/libs/app.jar app.jar
RUN java -Djarmode=layertools -jar app.jar extract

COPY --from=build dependencies/ ./
COPY --from=build spring-boot-loader/ ./
COPY --from=build snapshot-dependencies/ ./
COPY --from=build application/ ./

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

Kotlin-приложения часто используют конфигурацию через переменные окружения. В Docker их можно передать несколькими способами:

# Через аргументы командной строки
docker run -e DATABASE_URL=jdbc:postgresql://db:5432/mydb -e PROFILE=prod kotlin-app

# Через файл .env
docker run --env-file .env kotlin-app

В коде Kotlin это выглядит так:

fun loadConfig(): AppConfig {
    val dbUrl = System.getenv("DATABASE_URL") ?: "jdbc:h2:mem:default"
    val profile = System.getenv("PROFILE") ?: "dev"
    return AppConfig(dbUrl, profile)
}

Для разных окружений можно использовать docker-compose.yml:

version: '3.8'
services:
  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      - DATABASE_URL=jdbc:postgresql://db:5432/appdb
      - PROFILE=prod
    depends_on:
      - db
  db:
    image: postgres:16
    environment:
      POSTGRES_DB: appdb
      POSTGRES_PASSWORD: secret

Многомодульные проекты и Docker

В больших проектах Kotlin часто используется многомодульная структура. Для таких случаев важно правильно организовать сборку:

# Dockerfile для многомодульного проекта
FROM gradle:8.5-jdk21 AS build
WORKDIR /app
COPY settings.gradle.kts .
COPY build.gradle.kts .
COPY gradle.properties .
COPY module-a/ module-a/
COPY module-b/ module-b/
COPY module-main/ module-main/
RUN gradle :module-main:build --no-daemon

FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=                

Блоги

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