Einleitung: Warum Docker und Kotlin das ideale Paar sind
Moderne Entwicklung ist ohne Containerisierung undenkbar. Docker hat sich zum De-facto-Standard für das Verpacken, Ausliefern und Ausführen von Anwendungen entwickelt. Kotlin wiederum gewinnt dank seiner prägnanten Syntax, vollständigen Kompatibilität mit Java und Unterstützung für Multiplattform-Entwicklung rasant an Popularität. Die Kombination von Docker und Kotlin ermöglicht es, leichtgewichtige, skalierbare und vorhersagbare Umgebungen zu schaffen – von einer einfachen Serveranwendung auf der JVM bis hin zu einem nativen Binary für IoT.
In diesem Artikel werden wir uns ansehen, wie man Kotlin-Projekte richtig containerisiert, die Image-Größe optimiert und typische Fehler vermeidet. Alle Codebeispiele können Sie an Ihr eigenes Projekt anpassen.
1. Basis-Containerisierung einer Kotlin/JVM-Anwendung
Beginnen wir mit dem klassischen Szenario: einer Kotlin-Anwendung, die auf der JVM läuft. Für den Build verwenden wir Gradle – das Standardwerkzeug für Kotlin-Projekte.
1.1. Projektstruktur
Nehmen wir an, wir haben einen einfachen HTTP-Server mit Ktor. Die Projektstruktur sieht so aus:
my-kotlin-app/├── build.gradle.kts├── settings.gradle.kts└── src/ └── main/ └── kotlin/ └── com/ └── example/ └── App.kt1.2. Dockerfile für einen Multi-Stage-Build
Eine entscheidende Praxis ist der Multi-Stage-Build. Die erste Stufe erstellt mit Gradle ein fat JAR, die zweite verwendet ein minimales JRE-Image für den Start.
# Stufe 1: BuildFROM gradle:8.5-jdk21 AS buildWORKDIR /appCOPY --chown=gradle:gradle . .RUN gradle build --no-daemon -x test
# Stufe 2: AusführungFROM eclipse-temurin:21-jre-alpineWORKDIR /appCOPY --from=build /app/build/libs/*.jar app.jarEXPOSE 8080ENTRYPOINT ["java", "-jar", "app.jar"]1.3. Optimierung: Caching von Abhängigkeiten
Um Abhängigkeiten nicht bei jeder Code-Änderung neu zu bauen, teilen Sie den COPY-Befehl in zwei Teile auf:
FROM gradle:8.5-jdk21 AS buildWORKDIR /app# Zuerst nur die Build-Dateien kopierenCOPY build.gradle.kts settings.gradle.kts ./COPY gradle/ gradle/RUN gradle dependencies --no-daemon# Dann die Quelltexte kopierenCOPY src/ src/RUN gradle build --no-daemon -x testDieser Trick nutzt den Layer-Caching-Mechanismus von Docker: Das Image wird nur neu gebaut, wenn sich build.gradle.kts ändert, nicht bei jeder Code-Änderung.
2. Containerisierung von Kotlin/Native-Anwendungen
Kotlin/Native ermöglicht es, Code ohne JVM in native Binaries zu kompilieren. Das ist ideal für Microservices, die eine minimale Image-Größe und einen schnellen Start benötigen.
2.1. Dockerfile für Native
Für den Build des nativen Binaries verwenden wir ein Image mit dem Kotlin/Native Compiler. Das finale Image ist scratch (leer) oder distroless.
# Build-StufeFROM jetbrains/kotlin-native:2.0.0 AS buildWORKDIR /appCOPY . .RUN kotlinc-native src/main/kotlin/Main.kt -o app
# Ausführungsstufe: minimales ImageFROM alpine:3.19WORKDIR /appCOPY --from=build /app/app.kexe appEXPOSE 8080CMD ["./app"]Wichtig: Für Alpine kann die Installation von libc6-compat erforderlich sein, wenn das Binary mit glibc erstellt wurde. Verwenden Sie FROM busybox:glibc für eine noch kleinere Größe.
2.2. Vergleich der Image-Größen
- JVM (fat JAR + JRE): ~180-250 MB
- JVM (distroless + slim JRE): ~80-120 MB
- Native (Alpine): ~10-25 MB
- Native (scratch): ~5-10 MB
Für hochlastige Microservices bietet der Native-Ansatz eine erhebliche Ressourceneinsparung.
3. Docker Compose und Multi-Modul-Projekte
Komplexe Kotlin-Anwendungen bestehen oft aus mehreren Modulen: API, Datenbank, Cache. Docker Compose vereinfacht die Orchestrierung.