Introdução: por que Docker e Kotlin são a combinação perfeita
O desenvolvimento moderno é impensável sem conteinerização. Docker se tornou o padrão de facto para empacotar, distribuir e executar aplicações. Kotlin, por sua vez, está ganhando popularidade rapidamente devido à sua sintaxe concisa, compatibilidade total com Java e suporte multiplataforma. A combinação de Docker e Kotlin permite criar ambientes leves, escaláveis e previsíveis — desde uma simples aplicação servidora na JVM até um binário nativo para IoT.
Neste artigo, vamos analisar como conteinerizar corretamente projetos Kotlin, otimizar o tamanho das imagens e evitar erros comuns. Todos os exemplos de código podem ser adaptados para o seu projeto.
1. Conteinerização básica de uma aplicação Kotlin/JVM
Vamos começar com o cenário clássico: uma aplicação Kotlin rodando na JVM. Para a construção, usaremos Gradle — a ferramenta padrão para projetos Kotlin.
1.1. Estrutura do projeto
Suponha que temos um servidor HTTP simples em Ktor. A estrutura do projeto:
my-kotlin-app/
├── build.gradle.kts
├── settings.gradle.kts
└── src/
└── main/
└── kotlin/
└── com/
└── example/
└── App.kt
1.2. Dockerfile para construção multi-estágio
A prática fundamental é a construção multi-estágio (multi-stage build). O primeiro estágio compila o fat JAR usando Gradle, o segundo usa uma imagem JRE mínima para execução.
# Estágio 1: compilação
FROM gradle:8.5-jdk21 AS build
WORKDIR /app
COPY --chown=gradle:gradle . .
RUN gradle build --no-daemon -x test
# Estágio 2: execução
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=build /app/build/libs/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
1.3. Otimização: cache de dependências
Para não recompilar as dependências a cada alteração no código, divida o COPY em duas partes:
FROM gradle:8.5-jdk21 AS build
WORKDIR /app
# Primeiro, copie apenas os arquivos de compilação
COPY build.gradle.kts settings.gradle.kts ./
COPY gradle/ gradle/
RUN gradle dependencies --no-daemon
# Depois, copie os fontes
COPY src/ src/
RUN gradle build --no-daemon -x test
Esse truque utiliza o mecanismo de cache de camadas do Docker: a imagem é reconstruída apenas quando build.gradle.kts é alterado, e não a cada modificação no código.
2. Conteinerização de aplicações Kotlin/Native
Kotlin/Native permite compilar código em binários nativos sem JVM. Isso é ideal para microsserviços que precisam de tamanho de imagem mínimo e inicialização rápida.
2.1. Dockerfile para Native
Para compilar o binário nativo, use uma imagem com o compilador Kotlin/Native. A imagem final é scratch (vazia) ou distroless.
# Estágio de compilação
FROM jetbrains/kotlin-native:2.0.0 AS build
WORKDIR /app
COPY . .
RUN kotlinc-native src/main/kotlin/Main.kt -o app
# Estágio de execução: imagem mínima
FROM alpine:3.19
WORKDIR /app
COPY --from=build /app/app.kexe app
EXPOSE 8080
CMD ["./app"]
Importante: Para Alpine, pode ser necessário instalar libc6-compat se o binário foi compilado com glibc. Use FROM busybox:glibc para um tamanho ainda menor.
2.2. Comparação de tamanhos de imagem
- JVM (fat JAR + JRE): ~180-250 MB
- JVM (distroless + slim JRE): ~80-120 MB
- Native (Alpine): ~10-25 MB
- Native (scratch): ~5-10 MB
Para microsserviços de alta carga, a abordagem Native oferece uma economia significativa de recursos.
3. Docker Compose e projetos multimódulo
Aplicações Kotlin complexas geralmente consistem em vários módulos: API, banco de dados, cache. O Docker Compose simplifica a orquestração.