Docker and Kotlin: Advanced-Level Application Containerization
\n\nContainerization has become an integral part of modern development, and Kotlin is no exception. If you've already mastered basic Dockerfiles and running simple JAR files, it's time to move to the next level. In this article, we'll cover advanced techniques for containerizing Kotlin applications: multi-stage builds, image size optimization, configuration for the Ktor and Spring Boot frameworks, as well as working with Docker Compose in a production environment.
\n\n1. Multi-Stage Builds for Kotlin Applications
\n\nOne of the main problems when containerizing JVM applications is the size of the final image. A standard OpenJDK image can weigh over 400 MB. Multi-stage builds allow you to separate the build environment from the runtime environment, using a minimal runtime image.
\n\nExample for a Kotlin Gradle Project
\n\n# First stage: build\nFROM gradle:8.5-jdk21 AS builder\nWORKDIR /app\nCOPY build.gradle.kts settings.gradle.kts ./\nCOPY src ./src\nRUN gradle build --no-daemon -x test\n\n# Second stage: execution\nFROM eclipse-temurin:21-jre-alpine\nWORKDIR /app\nCOPY --from=builder /app/build/libs/app.jar app.jar\nEXPOSE 8080\nENTRYPOINT ["java", "-jar", "app.jar"]\n\nUsing eclipse-temurin:21-jre-alpine instead of a full JDK image reduces the size from 400+ MB to ~120 MB. For Kotlin applications on Ktor or Spring Boot, this is more than sufficient.
Optimizing Layer Caching
\n\nTo speed up rebuilds, copy dependencies separately from the source code:
\n\nFROM gradle:8.5-jdk21 AS builder\nWORKDIR /app\n# First dependencies (cached if unchanged)\nCOPY build.gradle.kts settings.gradle.kts ./\nRUN gradle dependencies --no-daemon\n# Then source code\nCOPY src ./src\nRUN gradle build --no-daemon -x test\n\nThis technique is especially useful in CI/CD, where every second counts.
\n\n2. Image Optimization for Kotlin Applications
\n\nIn addition to choosing the base image, there are additional techniques for reducing size and improving security.
\n\nUsing JLink to Create a Custom JRE
\n\nIf your application uses only a subset of JDK modules, you can create a minimal JRE:
\n\n# In the builder stage\nRUN jlink --module-path /opt/java/openjdk/jmods \\\n --add-modules java.base,java.logging,java.naming,java.sql \\\n --output /custom-jre \\\n --strip-debug --no-man-pages --no-header-files\n\n# In the runtime stage\nFROM alpine:3.19\nCOPY --from=builder /custom-jre /opt/jre\nCOPY --from=builder /app/build/libs/app.jar /app/app.jar\nENV PATH="/opt/jre/bin:${PATH}"\nEXPOSE 8080\nCMD ["java", "-jar", "/app/app.jar"]\n\nThe image size can be reduced to 50-70 MB.
\n\nMulti-Layer Architecture with Spring Boot
\n\nSpring Boot 3+ supports splitting the fat JAR into layers. Use this in your Dockerfile:
\n\nFROM eclipse-temurin:21-jre-alpine AS builder\nWORKDIR /app\nCOPY build/libs/*.jar app.jar\nRUN java -Djarmode=layertools -jar app.jar extract\n\nFROM eclipse-temurin:21-jre-alpine\nWORKDIR /app\nCOPY --from=builder /app/dependencies/ ./\nCOPY --from=builder /app/spring-boot-loader/ ./\nCOPY --from=builder /app/snapshot-dependencies/ ./\nCOPY --from=builder /app/application/ ./\nENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]\n\nNow, when code changes, only the application layer is rebuilt, while dependencies and the Spring Boot loader remain cached.
3. Containerizing Ktor Applications
\n\nKtor is a lightweight framework for creating asynchronous servers in Kotlin. Its containerization has its own specifics.
\n\nDockerfile for Ktor with Netty
\n\nFROM gradle:8.5-jdk21 AS builder\nWORKDIR /app\nCOPY build.gradle.kts settings.gradle.kts ./\nRUN gradle dependencies --