Docker and Kotlin: Application Containerization for Developers

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

Introduction: Why Docker and Kotlin are the Perfect Pair

Modern development is unthinkable without containerization. Docker has become the de facto standard for packaging, delivering, and running applications. Kotlin, in turn, is rapidly gaining popularity thanks to its concise syntax, full compatibility with Java, and support for multiplatform development. The combination of Docker and Kotlin allows you to create lightweight, scalable, and predictable environments — from a simple JVM server application to a native binary for IoT.

In this article, we will break down how to properly containerize Kotlin projects, optimize image sizes, and avoid common mistakes. You can adapt all the code examples to your own project.



1. Basic Containerization of a Kotlin/JVM Application

Let's start with a classic scenario: a Kotlin application running on the JVM. For building, we will use Gradle — the standard tool for Kotlin projects.



1.1. Project Structure

Suppose we have a simple HTTP server on Ktor. The project structure:

my-kotlin-app/├── build.gradle.kts├── settings.gradle.kts└── src/    └── main/        └── kotlin/            └── com/                └── example/                    └── App.kt


1.2. Dockerfile for Multi-Stage Build

The key practice is a multi-stage build. The first stage builds a fat JAR using Gradle, the second uses a minimal JRE image for running.

# Stage 1: buildFROM gradle:8.5-jdk21 AS buildWORKDIR /appCOPY --chown=gradle:gradle . .RUN gradle build --no-daemon -x test

# Stage 2: runFROM eclipse-temurin:21-jre-alpineWORKDIR /appCOPY --from=build /app/build/libs/*.jar app.jarEXPOSE 8080ENTRYPOINT ["java", "-jar", "app.jar"]


1.3. Optimization: Caching Dependencies

To avoid rebuilding dependencies with every code change, split the COPY into two parts:

FROM gradle:8.5-jdk21 AS buildWORKDIR /app# First, copy only the build filesCOPY build.gradle.kts settings.gradle.kts ./COPY gradle/ gradle/RUN gradle dependencies --no-daemon# Then copy the source codeCOPY src/ src/RUN gradle build --no-daemon -x test

This trick uses Docker's layer caching mechanism: the image is only rebuilt when build.gradle.kts changes, not with every code edit.



2. Containerization of Kotlin/Native Applications

Kotlin/Native allows you to compile code into native binaries without the JVM. This is ideal for microservices that need a minimal image size and fast startup.



2.1. Dockerfile for Native

To build a native binary, use an image with the Kotlin/Native compiler. The final image is scratch (empty) or distroless.

# Build stageFROM jetbrains/kotlin-native:2.0.0 AS buildWORKDIR /appCOPY . .RUN kotlinc-native src/main/kotlin/Main.kt -o app

# Run stage: minimal imageFROM alpine:3.19WORKDIR /appCOPY --from=build /app/app.kexe appEXPOSE 8080CMD ["./app"]

Important: For Alpine, you may need to install libc6-compat if the binary was built with glibc. Use FROM busybox:glibc for an even smaller size.



2.2. Image Size Comparison

  • JVM (fat JAR + JRE): ~180-250 MB
  • JVM (distroless + slim JRE): ~80-120 MB
  • Native (Alpine): ~10-25 MB
  • Native (scratch): ~5-10 MB

For high-load microservices, the Native approach provides significant resource savings.



3. Docker Compose and Multi-Module Projects

Complex Kotlin applications often consist of several modules: API, database, cache. Docker Compose simplifies orchestration.



3.1. Example docker-compose.

Blogs

Book Recommendations