DEV Community

Sam T
Sam T

Posted on • Originally published at toolshref.com

How to Reduce Java Docker Image Size by 90% (Multi-Stage & Distroless)

You finish your Spring Boot app. The JAR file is only 40MB. You write a Dockerfile, build the image, and—shockingly—it’s 850MB.

Pushing almost 1GB of data to your registry every time you change one line of code is a disaster. It slows down CI/CD pipelines, increases your AWS ECR storage costs, and makes auto-scaling slower because Kubernetes takes longer to pull the image.

The good news? You can shrink that image to under 100MB without changing a single line of Java code. You just need to stop shipping the entire Operating System and Compiler to production.

⚡ TL;DR Summary:

  • The Problem: Using maven or openjdk (JDK) images in production adds 600MB+ of unnecessary bloat.
  • The Fix: Use Multi-Stage Builds to compile in one step and run in another.
  • The Base: Switch from full OS images to eclipse-temurin:17-jre-alpine or Google's distroless.
  • The Shortcut: Don't memorize boilerplate. Use our Docker Generator to create optimized Dockerfiles instantly.

1. The "Fat Jar" Mistake (The Wrong Way)

This is the standard Dockerfile most tutorials teach you. It works, but it is terrible for production.

# ❌ BAD PRACTICE FROM maven:3.8.5-openjdk-17 WORKDIR /app COPY . . RUN mvn clean package CMD ["java", "-jar", "target/myapp.jar"]

Why this is bad:

  • It includes Maven: You don't need a build tool in production.
  • It includes the JDK: You only need the JRE (Runtime) to run Java, not the JDK (Compiler). The JDK is significantly larger.
  • It creates a "Fat Layer": Every time you change code, Docker has to re-download dependencies because of how the COPY . . command works.

2. The Solution: Multi-Stage Builds

Docker allows you to use multiple FROM statements. We can use a heavy image to build the artifact, and then copy only the JAR file to a tiny image for running it.

Step A: The Builder Stage

We use the heavy Maven image to compile the code. We name this stage builder.

FROM maven:3.8.5-openjdk-17 AS builder WORKDIR /app COPY pom.xml . COPY src ./src RUN mvn clean package -DskipTests

Step B: The Runner Stage (JRE Only)

Now, we pick a "Slim" or "Alpine" version of the JRE (Java Runtime Environment). We copy the JAR from the builder stage.

FROM eclipse-temurin:17-jre-alpine WORKDIR /app COPY --from=builder /app/target/*.jar app.jar ENTRYPOINT ["java", "-jar", "app.jar"]

Result: Your image drops from ~800MB to ~120MB.

3. Optimizing for Cache (The "Layer" Trick)

Even with multi-stage builds, if you change one line of Java code, Docker usually re-downloads all your Maven dependencies. That’s slow.

To fix this, we need to leverage Docker Layer Caching. We copy the pom.xml and download dependencies before we copy the source code.

The Final, Optimized Dockerfile

# Stage 1: Build FROM maven:3.8.5-openjdk-17-slim AS build WORKDIR /home/app
Copy pom.xml and install dependencies FIRST

COPY pom.xml . RUN mvn dependency:go-offline
Copy source code and build

COPY src ./src RUN mvn clean package -DskipTests
Stage 2: Run (Production)

FROM eclipse-temurin:17-jre-alpine WORKDIR /app
Create a non-root user for security

RUN addgroup -S spring && adduser -S spring -G spring USER spring:spring
Copy only the built JAR

COPY --from=build /home/app/target/*.jar app.jar

ENTRYPOINT ["java", "-jar", "app.jar"]

By running mvn dependency:go-offline before copying src, Docker caches the dependencies layer. If you change your Java code but not your pom.xml, the build takes seconds, not minutes.

4. Alpine vs. Distroless: Which is smaller?

You will often see debates about which base image is best. Here is the breakdown:

Image Type Size Pros & Cons
Debian Slim ~200MB Standard compatibility. Safe bet.
Alpine Linux ~120MB Tiny. Uses musl instead of glibc, which can rarely cause C-library bugs in Java.
Google Distroless ~100MB Smallest & Most Secure. No shell (/bin/bash), so you can't debug inside the container.

My Recommendation: Start with Alpine. If you run into weird DNS or library issues, switch to Debian Slim. Only use Distroless if you have a mature logging setup, because you cannot "exec" into the container to debug.

Frequently Asked Questions

Why is my Java Docker image still large?

Check if you are copying the target/ folder manually or if you are copying unnecessary files like logs, .git folder, or local uploads. Use a .dockerignore file to exclude these.

Can I use JLink to make it even smaller?

Yes. Java 9+ introduced `jlink`, which allows you to create a custom JRE containing only the modules your app uses. This can get images down to 40MB, but it requires complex module configuration and is often overkill for standard microservices.

Does Multi-Stage build slow down CI?

Slightly, because you are downloading the Maven image every time. However, the time saved during the "Push" and "Pull" phases (because the image is 90% smaller) usually outweighs the build time.

🚀 Stop Writing Dockerfiles From Scratch

Getting the layer caching order and syntax right is annoying. One typo breaks the build.

Use our Online Dockerfile Generator.

Select "Java", choose your version (17/21), and toggle "Multi-Stage Build." It generates the optimized, production-ready code instantly.

⚠️ Critical Warning for Mac Users (M1/M2/M3)

    If you build this Docker image on a newer Mac (Apple Silicon), it creates an ARM64 image. If you push this to a standard AWS EC2 instance (which is usually AMD64), your container will crash with an exec format error
    The Fix:Always force the platform when building for production:
   docker build --platform linux/amd64 -t my-app .
Enter fullscreen mode Exit fullscreen mode

Disclaimer: The code snippets and configurations provided in this article are for educational purposes only. While we strive for accuracy, software environments vary. Toolshref.com is not responsible for any data loss, downtime, or security issues that may arise from using this information. Always test code in a staging environment before deploying to production.

Top comments (0)