Docker revolucionou a forma como empacotamos e distribuimos aplicacoes, e projetos Kotlin se beneficiam enormemente dessa tecnologia. Containerizar uma aplicacao Kotlin garante que ela rode de forma identica em qualquer ambiente, eliminando o classico problema do “funciona na minha maquina”. Neste guia, vamos desde a criacao de Dockerfiles otimizados ate orquestracao com Docker Compose, cobrindo aplicacoes Spring Boot, Ktor e scripts Kotlin puros. Voce vai aprender a construir imagens leves, seguras e prontas para producao.
Por Que Docker para Projetos Kotlin
Aplicacoes Kotlin rodam na JVM, que precisa estar instalada e configurada corretamente no servidor. Docker encapsula a JVM, as dependencias e a aplicacao em um container isolado. As vantagens incluem ambiente consistente entre desenvolvimento e producao, facilidade de escalar horizontalmente, isolamento de processos e deploy simplificado.
Dockerfile Basico para Kotlin
Vamos comecar com um Dockerfile simples para uma aplicacao Kotlin com Gradle:
// Dockerfile
FROM eclipse-temurin:17-jdk AS build
WORKDIR /app
# Copiar arquivos de configuracao do Gradle primeiro (para cache de dependencias)
COPY gradle gradle
COPY gradlew .
COPY build.gradle.kts .
COPY settings.gradle.kts .
COPY gradle.properties .
# Baixar dependencias (camada cacheada)
RUN chmod +x gradlew && ./gradlew dependencies --no-daemon
# Copiar codigo fonte
COPY src src
# Compilar
RUN ./gradlew build -x test --no-daemon
# Imagem de runtime
FROM eclipse-temurin:17-jre
WORKDIR /app
COPY --from=build /app/build/libs/*-all.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
Multi-Stage Build Otimizado
O multi-stage build separa o ambiente de compilacao do runtime, resultando em imagens significativamente menores:
// Dockerfile otimizado
# Etapa 1: Cache de dependencias
FROM eclipse-temurin:17-jdk-alpine AS deps
WORKDIR /app
COPY gradle gradle
COPY gradlew .
COPY build.gradle.kts .
COPY settings.gradle.kts .
RUN chmod +x gradlew && ./gradlew dependencies --no-daemon
# Etapa 2: Build
FROM eclipse-temurin:17-jdk-alpine AS build
WORKDIR /app
COPY --from=deps /root/.gradle /root/.gradle
COPY . .
RUN chmod +x gradlew && ./gradlew shadowJar --no-daemon
# Etapa 3: Runtime
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
# Criar usuario nao-root
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
# Copiar apenas o JAR
COPY --from=build /app/build/libs/*-all.jar app.jar
# Configuracao de seguranca
USER appuser
# Configuracao da JVM
ENV JAVA_OPTS="-XX:+UseContainerSupport \
-XX:MaxRAMPercentage=75.0 \
-XX:+UseG1GC \
-Djava.security.egd=file:/dev/./urandom"
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s \
CMD wget -qO- http://localhost:8080/health || exit 1
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
A imagem final usa jre-alpine em vez de jdk, reduzindo o tamanho de centenas de MB para dezenas de MB.
Dockerfile para Spring Boot
O Spring Boot oferece suporte nativo a build de imagens via Buildpacks, mas Dockerfiles manuais oferecem mais controle:
// Dockerfile para Spring Boot com layers
FROM eclipse-temurin:17-jdk-alpine AS build
WORKDIR /app
COPY . .
RUN chmod +x gradlew && ./gradlew bootJar --no-daemon
# Extrair layers do Spring Boot
FROM eclipse-temurin:17-jdk-alpine AS layers
WORKDIR /app
COPY --from=build /app/build/libs/*.jar app.jar
RUN java -Djarmode=layertools -jar app.jar extract
# Runtime com layers separadas (melhor cache do Docker)
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
RUN addgroup -S spring && adduser -S spring -G spring
USER spring
COPY --from=layers /app/dependencies/ ./
COPY --from=layers /app/spring-boot-loader/ ./
COPY --from=layers /app/snapshot-dependencies/ ./
COPY --from=layers /app/application/ ./
EXPOSE 8080
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]
As layers do Spring Boot permitem que o Docker reutilize cache para dependencias que nao mudaram, acelerando builds subsequentes.
Dockerfile para Ktor
// Dockerfile para Ktor
FROM eclipse-temurin:17-jdk-alpine AS build
WORKDIR /app
COPY . .
RUN chmod +x gradlew && ./gradlew buildFatJar --no-daemon
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
RUN addgroup -S ktor && adduser -S ktor -G ktor
USER ktor
COPY --from=build /app/build/libs/*-all.jar app.jar
ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0"
EXPOSE 8080
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
Docker Compose para Desenvolvimento
O Docker Compose orquestra multiplos containers para desenvolvimento local:
// docker-compose.yml
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile
ports:
- "8080:8080"
environment:
- DATABASE_URL=jdbc:postgresql://postgres:5432/meuapp
- DATABASE_USER=appuser
- DATABASE_PASSWORD=apppass
- REDIS_HOST=redis
- REDIS_PORT=6379
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
networks:
- app-network
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: meuapp
POSTGRES_USER: appuser
POSTGRES_PASSWORD: apppass
ports:
- "5432:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
- ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U appuser -d meuapp"]
interval: 5s
timeout: 3s
retries: 5
networks:
- app-network
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis-data:/data
networks:
- app-network
pgadmin:
image: dpage/pgadmin4
environment:
PGADMIN_DEFAULT_EMAIL: admin@local.dev
PGADMIN_DEFAULT_PASSWORD: admin
ports:
- "5050:80"
depends_on:
- postgres
networks:
- app-network
volumes:
postgres-data:
redis-data:
networks:
app-network:
driver: bridge
Docker Compose para Testes de Integracao
Crie um compose separado para testes:
// docker-compose.test.yml
version: '3.8'
services:
test-db:
image: postgres:16-alpine
environment:
POSTGRES_DB: testdb
POSTGRES_USER: testuser
POSTGRES_PASSWORD: testpass
ports:
- "5433:5432"
tmpfs:
- /var/lib/postgresql/data # Mais rapido para testes
No Gradle, crie uma task que gerencia o ciclo de vida dos containers de teste:
// build.gradle.kts
tasks.register<Exec>("startTestContainers") {
commandLine("docker-compose", "-f", "docker-compose.test.yml", "up", "-d")
}
tasks.register<Exec>("stopTestContainers") {
commandLine("docker-compose", "-f", "docker-compose.test.yml", "down")
}
Configuracao da JVM para Containers
A JVM precisa de configuracoes especificas para funcionar bem em containers:
// application.conf (Ktor) ou application.yml (Spring)
// A JVM deve respeitar os limites de memoria do container
// Flags importantes:
// -XX:+UseContainerSupport -> JVM respeita cgroups
// -XX:MaxRAMPercentage=75.0 -> Usa 75% da RAM do container
// -XX:+UseG1GC -> GC recomendado para containers
// -XX:+ExitOnOutOfMemoryError -> Reinicia o container em OOM
No Kotlin, configure o health check endpoint:
// Health check para Docker
fun Application.configurarHealthCheck() {
routing {
get("/health") {
// Verificar dependencias
val dbOk = verificarBancoDeDados()
val redisOk = verificarRedis()
if (dbOk && redisOk) {
call.respond(HttpStatusCode.OK, mapOf(
"status" to "UP",
"database" to "OK",
"redis" to "OK"
))
} else {
call.respond(HttpStatusCode.ServiceUnavailable, mapOf(
"status" to "DOWN",
"database" to if (dbOk) "OK" else "FAIL",
"redis" to if (redisOk) "OK" else "FAIL"
))
}
}
}
}
Boas Praticas para Kotlin com Docker
- Use multi-stage builds: separe compilacao de runtime para imagens menores e mais seguras.
- Imagens Alpine: prefira imagens baseadas em Alpine Linux para reduzir tamanho.
- Usuario nao-root: nunca rode a aplicacao como root no container.
- Ordene COPY para cache: copie arquivos que mudam menos (gradle, build configs) antes do codigo fonte.
- Use .dockerignore: exclua
.git,build/,.gradle/,*.mde outros arquivos desnecessarios. - Configure health checks: permita que orquestradores detectem e reiniciem containers com problemas.
- Limite recursos: defina limites de CPU e memoria no Docker Compose ou Kubernetes.
- Use variaveis de ambiente para configuracao: nunca hardcode URLs, credenciais ou parametros de ambiente na imagem.
Erros Comuns e Armadilhas
- Imagem grande demais: usar
jdkem vez dejreno runtime ou nao usar Alpine dobra ou triplica o tamanho da imagem. - Cache de build nao aproveitado: copiar todo o projeto antes de baixar dependencias invalida o cache a cada mudanca de codigo.
- JVM ignorando limites do container: sem
-XX:+UseContainerSupport, a JVM pode alocar mais memoria do que o container permite, causando OOM killer. - Rodar como root: vulnerabilidades na aplicacao podem comprometer o host se o container roda como root.
- Secrets em variaveis de ambiente visíveis: use Docker secrets ou ferramentas como HashiCorp Vault para credenciais sensiveis.
- Nao configurar graceful shutdown: a aplicacao deve responder ao sinal SIGTERM para encerrar conexoes e processos pendentes antes de parar.
Conclusao e Proximos Passos
Docker e uma ferramenta indispensavel para projetos Kotlin modernos, desde desenvolvimento local ate deploy em producao. Com multi-stage builds, configuracao adequada da JVM e Docker Compose, voce tem um ambiente reproduzível e eficiente. Para ir alem, explore Kubernetes para orquestracao de containers em escala, consulte nosso guia de CI/CD para integrar Docker ao pipeline de deploy e estude microservicos para arquiteturas distribuídas containerizadas.