Почему контейнеризация Kotlin — это маст-хев для современного разработчика?
Представьте ситуацию: вы написали крутое Kotlin-приложение на своей MacBook. Всё работает идеально. Вы скидываете код коллеге на Windows — база данных не подключается. Ещё одному на Linux — версия JDK не та. Знакомо? Docker решает эту проблему раз и навсегда.
Контейнеризация Kotlin приложений — это не просто модный тренд. Это способ гарантировать, что ваше приложение будет работать одинаково на любой машине: от ноутбука джуниора до production-сервера в AWS. В этой статье мы разберём, как правильно упаковать Kotlin-приложение в Docker-образ, оптимизировать его размер и настроить для продакшена.
Что вы узнаете:
- Как написать Dockerfile для Kotlin-приложения с Gradle
- Как уменьшить размер образа в 3-5 раз
- Как использовать Docker Compose для связки Kotlin + база данных
- Best practices для production-контейнеризации
Базовый Dockerfile для Kotlin-приложения
Начнём с простого. Допустим, у вас есть стандартное Kotlin-приложение, которое собирается с помощью Gradle. Первое, что приходит в голову — взять образ JDK, скопировать исходники и запустить сборку. Давайте так и сделаем, но сразу с учётом best practices.
Монолитная сборка (не делайте так в продакшене)
# Dockerfile
FROM gradle:7.6-jdk17 AS build
WORKDIR /app
COPY build.gradle.kts settings.gradle.kts ./
COPY src ./src
RUN gradle build --no-daemon -x test
FROM openjdk:17-jdk-slim
WORKDIR /app
COPY --from=build /app/build/libs/app.jar app.jar
EXPOSE 8080
CMD ["java", "-jar", "app.jar"]
Этот подход рабочий, но у него есть три серьёзных недостатка:
- Размер: образ весит около 400-500 МБ из-за полного JDK
- Кэширование: при любом изменении исходников Gradle будет скачивать зависимости заново
- Безопасность: в образе остаются инструменты сборки, которые не нужны в рантайме
Оптимизированная сборка: многоэтапность и кэширование
Настоящая магия Docker — это multi-stage builds. Мы разделим процесс на три этапа:
- Загрузка зависимостей (кэшируется)
- Сборка приложения
- Финальный минимальный образ
Правильный Dockerfile для Kotlin + Gradle
# Stage 1: Загрузка зависимостей
FROM gradle:7.6-jdk17 AS dependencies
WORKDIR /app
COPY build.gradle.kts settings.gradle.kts ./
RUN gradle dependencies --no-daemon
# Stage 2: Сборка
FROM dependencies AS build
COPY src ./src
RUN gradle build --no-daemon -x test
# Stage 3: Финальный образ
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY --from=build /app/build/libs/app.jar app.jar
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
EXPOSE 8080
CMD ["java", "-jar", "app.jar"]
Что изменилось?
- Alpine JRE вместо полного JDK — образ уменьшается до ~120 МБ
- Кэширование зависимостей — если не менялся build.gradle.kts, Docker использует кэш
- Безопасность — приложение запускается от непривилегированного пользователя
Сборка с Gradle Wrapper (рекомендуемый способ)
Если вы используете Gradle Wrapper (а вы должны!), Dockerfile становится ещё элегантнее. Wrapper гарантирует, что версия Gradle будет одинаковой у всех разработчиков и в CI/CD.
FROM eclipse-temurin:17-jdk-alpine AS builder
WORKDIR /app
COPY gradlew gradlew.bat ./
COPY gradle ./gradle
RUN ./gradlew --version
COPY build.gradle.kts settings.gradle.kts ./
RUN ./gradlew dependencies --no-daemon
COPY src ./src
RUN ./gradlew build --no-daemon -x test
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY --from=builder /app/build/libs/app.jar app.jar
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
EXPOSE 8080
CMD ["java", "-jar", "app.jar"]
Обратите внимание: мы сначала копируем только Wrapper и конфигурационные файлы, чтобы максимально эффективно использовать кэш Docker.
Контейнеризация Kotlin-приложения с базой данных: Docker Compose
В реальных проектах Kotlin-приложение редко живёт само по себе. Чаще всего оно взаимодействует с PostgreSQL, Redis или Kafka. Здесь на помощь приходит Docker Compose.
Пример docker-compose.yml для Kotlin + PostgreSQL
version: '3.8'
services:
app:
build: .
ports:
- "8080:8080"
environment:
- DB_URL=jdbc:postgresql://db:5432/mydb
- DB_USER=user
- DB_PASSWORD=secret
depends_on:
db:
condition: service_healthy
restart: unless-stopped
db:
image: postgres:15-alpine
environment:
- POSTGRES_DB=mydb
- POSTGRES_USER=user
- POSTGRES_PASSWORD=secret
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d mydb"]
interval: 5s
timeout: 5s
retries: 5
volumes:
pgdata:
Ключевые моменты:
- Healthcheck — приложение стартует только после того, как PostgreSQL полностью готов
- Volumes — данные базы не теряются при перезапуске контейнера
- Переменные окружения — конфигурация вынесена из кода
Оптимизация образа для продакшена
Когда дело доходит до продакшена, каждый мегабайт имеет значение. Вот несколько техник, которые помогут сделать образ ещё легче и безопаснее.
1. Используйте jlink для создания минимальной JRE
jlink позволяет собрать кастомную JRE, включив только те модули, которые реально нужны вашему приложению. Это может уменьшить размер образа до 40-50 МБ.
FROM eclipse-temurin:17-jdk-alpine AS jre-builder
RUN jlink \
--add-modules java.base,java.sql,java.naming,java.management \
--strip-debug \
--no-man-pages \
--no-header-files \
--compress=2 \
--output /jre
FROM alpine:3.18
ENV JAVA_HOME=/jre
ENV PATH="$JAVA_HOME/bin:$PATH"
COPY --from=jre-builder /jre