Introducción: por qué Docker y Kotlin son la pareja ideal
El desarrollo moderno es impensable sin la contenerización. Docker se ha convertido en el estándar de facto para empaquetar, distribuir y ejecutar aplicaciones. Kotlin, por su parte, está ganando popularidad rápidamente gracias a su sintaxis concisa, compatibilidad total con Java y soporte multiplataforma. La combinación de Docker y Kotlin permite crear entornos ligeros, escalables y predecibles, desde una simple aplicación de servidor en JVM hasta un binario nativo para IoT.
En este artículo, analizaremos cómo contenerizar correctamente proyectos Kotlin, optimizar el tamaño de las imágenes y evitar errores típicos. Todos los ejemplos de código se pueden adaptar a su proyecto.
1. Contenerización básica de una aplicación Kotlin/JVM
Empecemos con el escenario clásico: una aplicación Kotlin que se ejecuta en JVM. Para la compilación usaremos Gradle, la herramienta estándar para proyectos Kotlin.
1.1. Estructura del proyecto
Supongamos que tenemos un servidor HTTP simple en Ktor. La estructura del proyecto:
my-kotlin-app/├── build.gradle.kts├── settings.gradle.kts└── src/ └── main/ └── kotlin/ └── com/ └── example/ └── App.kt1.2. Dockerfile para compilación multi-etapa
La práctica clave es la compilación multi-etapa (multi-stage build). La primera etapa compila el fat JAR con Gradle, la segunda utiliza una imagen JRE mínima para la ejecución.
# Etapa 1: compilaciónFROM gradle:8.5-jdk21 AS buildWORKDIR /appCOPY --chown=gradle:gradle . .RUN gradle build --no-daemon -x test
# Etapa 2: ejecuciónFROM eclipse-temurin:21-jre-alpineWORKDIR /appCOPY --from=build /app/build/libs/*.jar app.jarEXPOSE 8080ENTRYPOINT ["java", "-jar", "app.jar"]1.3. Optimización: almacenamiento en caché de dependencias
Para no recompilar las dependencias con cada cambio de código, divida el COPY en dos partes:
FROM gradle:8.5-jdk21 AS buildWORKDIR /app# Primero copiamos solo los archivos de compilaciónCOPY build.gradle.kts settings.gradle.kts ./COPY gradle/ gradle/RUN gradle dependencies --no-daemon# Luego copiamos los fuentesCOPY src/ src/RUN gradle build --no-daemon -x testEste truco utiliza el mecanismo de almacenamiento en caché de capas de Docker: la imagen se reconstruye solo cuando cambia build.gradle.kts, no con cada modificación del código.
2. Contenerización de aplicaciones Kotlin/Native
Kotlin/Native permite compilar código en binarios nativos sin JVM. Esto es ideal para microservicios que necesitan un tamaño de imagen mínimo y un inicio rápido.
2.1. Dockerfile para Native
Para compilar un binario nativo, usamos una imagen con el compilador Kotlin/Native. La imagen final es scratch (vacía) o distroless.
# Etapa de compilaciónFROM jetbrains/kotlin-native:2.0.0 AS buildWORKDIR /appCOPY . .RUN kotlinc-native src/main/kotlin/Main.kt -o app
# Etapa de ejecución: imagen mínimaFROM alpine:3.19WORKDIR /appCOPY --from=build /app/app.kexe appEXPOSE 8080CMD ["./app"]Importante: Para Alpine puede ser necesario instalar libc6-compat si el binario se compiló con glibc. Use FROM busybox:glibc para un tamaño aún menor.
2.2. Comparación de tamaños de imágenes
- 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 microservicios de alta carga, el enfoque Native proporciona un ahorro significativo de recursos.
3. Docker Compose y proyectos multimódulo
Las aplicaciones Kotlin complejas a menudo constan de varios módulos: API, base de datos, caché. Docker Compose simplifica la orquestación.