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=