Docker and Kotlin: Advanced-Level Application Containerization

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Docker and Kotlin: Advanced-Level Application Containerization



Containerization has become an integral part of modern development. If you have already mastered the basic principles of Docker and written a few simple images in Kotlin, it's time to move to the next level. In this article, we will cover advanced techniques: multi-stage builds, optimizing the final image size, working with popular frameworks (Ktor, Spring Boot), and efficient coroutine management in a containerized environment.



1. Multi-stage builds



One of the main problems when containerizing JVM applications is the image size. A base JDK image can weigh hundreds of megabytes. Multi-stage builds allow you to separate the compilation and execution stages, leaving only the JRE and the built JAR in the final image.



Consider an example for a Kotlin application built with Gradle:



# Build stageFROM gradle:7.6-jdk17 AS builderWORKDIR /appCOPY build.gradle.kts settings.gradle.kts ./COPY src ./srcRUN gradle build --no-daemon -x test

# Execution stageFROM eclipse-temurin:17-jre-alpineWORKDIR /appCOPY --from=builder /app/build/libs/*.jar app.jarEXPOSE 8080CMD ["java", "-jar", "app.jar"]


Why is this effective?

  • The build stage uses the full JDK and Gradle, but they do not end up in the final image.
  • The final image contains only the JRE (Java Runtime Environment), reducing the size by 3-5 times.
  • Alpine Linux further reduces the size to ~150-200 MB instead of 600+ MB.


2. Layer optimization and caching



Docker caches layers, but only if they haven't changed. To speed up rebuilds, separate dependencies and source code:



FROM gradle:7.6-jdk17 AS builderWORKDIR /app

# First, copy only dependency filesCOPY build.gradle.kts settings.gradle.kts ./RUN gradle dependencies --no-daemon

# Then source code (changes more often)COPY src ./srcRUN gradle build --no-daemon -x test


This approach ensures that the dependency layer is rebuilt only when build.gradle.kts changes. Otherwise, Docker uses the cache, saving minutes on each iteration.



Tip: For Kotlin projects with many dependencies (e.g., Spring Boot), this technique is especially relevant — loading the Gradle cache can take up to 2-3 minutes.



3. Containerizing Ktor applications



Ktor is a lightweight asynchronous framework in Kotlin, ideal for microservices. Containerizing a Ktor application requires minimal configuration, but there are nuances related to coroutines.



Example Dockerfile for Ktor:



FROM gradle:7.6-jdk17 AS builderWORKDIR /appCOPY build.gradle.kts settings.gradle.kts ./RUN gradle dependencies --no-daemonCOPY src ./srcRUN gradle build --no-daemon -x test

FROM eclipse-temurin:17-jre-alpineWORKDIR /appCOPY --from=builder /app/build/libs/ktor-app-*.jar app.jarEXPOSE 8080

# Important: JVM configuration for coroutinesCMD ["java", "-XX:+UseZGC", "-Xms256m", "-Xmx512m", "-jar", "app.jar"]


Key points:

  • Use ZGC (Z Garbage Collector) — it minimizes pauses, which is critical for asynchronous applications with a large number of coroutines.
  • Limit memory via -Xms and -Xmx. In containers, this helps avoid OOM errors.
  • Ensure the Ktor server does not block the main thread — coroutines should be configured via Dispatchers.IO or a custom pool.


Example of configuring Ktor to work in a container:



fun main() {    embeddedServer(Netty, port = 8080, host = "0.0.0.0") {        routing {            get("/") {                

Blogs

Book Recommendations