Docker, Java, and Why It Changes the Game
\n\nImagine: you've written a cool Java application. It works on your MacBook, but when you give it to a colleague on Windows — everything breaks. Sound familiar? And if the server is on Linux? You don't even need to guess — it's the classic "it works on my machine" story.
\n\nContainerization with Docker solves this problem once and for all. You package the application together with its environment (JVM, libraries, dependencies) into a lightweight container that runs identically everywhere. This is especially relevant for Java — after all, we're used to "Write Once, Run Anywhere", but in practice this idiom often fails due to JDK versions, system libraries, or configurations.
\n\nIn this article, we'll break down containerization of Java applications with Docker from A to Z. You'll learn how to write a Dockerfile, optimize the image, use Docker Compose, and avoid common pitfalls. Let's go!
\n\nWhat is Docker and Why Does a Java Developer Need It?
\n\nDocker is a platform for packaging, delivering, and running applications in isolated environments — containers. Unlike virtual machines, containers share the host OS kernel but have their own filesystem, network, and processes. This makes them lightweight (megabytes, not gigabytes) and fast (startup in seconds).
\n\nFor a Java developer, Docker provides three superpowers:
\n- \n
- Reproducibility — no more "it works on my machine". If a container runs locally, it will run on any server. \n
- Isolation — each application lives in its own world without interfering with others. Want two Java applications with different JDK versions? Easy. \n
- Scalability — containers are easy to clone, balance, and orchestrate (Kubernetes, Docker Swarm). \n
First Dockerfile for a Java Application
\n\nLet's start simple. We have a standard Java application (e.g., Spring Boot or just Main.java). We want to package it into a Docker image. The simplest way is to use the official OpenJDK image.
\n\nHere's a minimal Dockerfile:
# Use the official OpenJDK 17 image\nFROM openjdk:17-jdk-alpine\n\n# Set the working directory\nWORKDIR /app\n\n# Copy the JAR file into the container\nCOPY target/my-app.jar /app/my-app.jar\n\n# Run the application\nCMD ["java", "-jar", "my-app.jar"]\n\nNow let's build the image and run the container:
\n\n# Build the image\ndocker build -t my-java-app .\n\n# Run the container\ndocker run -p 8080:8080 my-java-app\n\nEverything works. But this is a bad example. Why? Let's break it down.
\n\nProblem #1: Image Size
\nThe openjdk:17-jdk-alpine image weighs about 150 MB. That's okay, but if you add all the dependencies, the size can grow to 500+ MB. That's a lot for production.
Problem #2: Build and Runtime in One Image
\nWe copy a pre-built JAR, meaning the build (mvn clean package) needs to be done beforehand. It's better to use a multi-stage build to separate the compilation stage from the execution stage.
\n\nOptimization: Multi-Stage Build
\n\nThe idea is simple: in the first stage, we use an image with Maven and JDK, build the project, and in the second stage — only the JRE and the finished JAR. This reduces the final image size significantly.
\n\n# Build stage\nFROM maven:3.8.5-openjdk-17 AS builder\nWORKDIR /build\nCOPY pom.xml .\nCOPY src ./src\nRUN mvn clean package -DskipTests\n\n# Runtime stage\nFROM openjdk:17-jre-alpine\nWORKDIR /app\nCOPY --from=builder /build/target/my-app.jar /app/my-app.jar\nEXPOSE 8080\nCMD ["java", "-jar", "my-app.jar"]\n\nWhat changed?
\n- \n
- The first stage (
builder) contains Maven and JDK — it's only needed for compilation. \n - The second stage uses a lightweight JRE image (~50 M