Shrink Your Java App Docker Image with Multi‑Stage Builds
Large Docker images slow CI pipelines and increase cloud costs. Multi‑stage builds separate compile‑time and runtime dependencies, cutting image size by 3–10×. This guide shows a concrete Maven‑to‑OpenJDK example, explains trade‑offs, and gives you the steps to get started.
15 May 2026, 23:53 UTC

Problem: Monolithic Docker Images
When you package a Java application into a single Docker image, the container usually contains the JDK, Maven/Gradle, build tools, and the compiled JAR. A typical image may end up between 200 MB and 1 GB. This bloat inflates deployment time, consumes network bandwidth, and expands the attack surface because every package is exposed to the running container.
Thesis: Multi‑Stage Builds Separate Build Artifacts
Docker’s multi‑stage build feature lets you define several named stages in one Dockerfile. The first stage (often called builder) pulls a full JDK image, runs the build, and produces the final JAR. The second stage starts from a lightweight runtime image (e.g., openjdk:17-jre-slim) and copies only the artifact produced by the builder. The resulting image contains only what the application needs at runtime, usually 3–10× smaller than the monolithic version.
Example Dockerfile
Below is a minimal Dockerfile that builds a Maven project and then packages it into a small runtime image. Replace my-app with your project name.
# syntax=docker/dockerfile:1
# Stage 1 – Build the application
FROM maven:3.9.6-openjdk-17 AS builder
WORKDIR /app
# Copy pom.xml and download dependencies first to leverage Docker cache
COPY pom.xml .
RUN mvn dependency:go-offline -B
# Copy source code and build
COPY src ./src
RUN mvn package -DskipTests -B
# Stage 2 – Runtime image
FROM eclipse-temurin:17-jre-jammy
WORKDIR /app
# Copy only the built JAR
COPY --from=builder /app/target/my-app-1.0.0.jar ./app.jar
# Expose the application port
EXPOSE 8080
ENTRYPOINT ["java","-jar","app.jar"]
Build Stage
- Uses
maven:3.9.6-openjdk-17, which contains Maven, JDK, and all build tools. - Copies
pom.xmlfirst to take advantage of Docker’s layer caching; if dependencies haven’t changed, this step is fast. - Runs
mvn dependency:go-offlineto pre‑download all dependencies. - Copies source code and runs
mvn packageto producemy-app-1.0.0.jar.
Runtime Stage
- Starts from
eclipse-temurin:17-jre-jammy, a 70‑MB lightweight image that contains only the JRE. - Copies the JAR from the builder stage using
--from=builder. - Sets the working directory, exposes port 8080, and defines the entrypoint.
Trade‑Offs & Limitations
Build Cache & Build Time
Multi‑stage builds can reduce cache hit rates if the Dockerfile is not structured carefully. For example, copying the entire source tree before downloading dependencies forces Docker to rebuild the dependency layer on every code change. The example above mitigates this by layering pom.xml first.
Missing Runtime Config
Only the JAR is copied into the runtime stage. If your application relies on external configuration files (e.g., application.yml), you must copy them explicitly. Otherwise the container will start but fail due to missing configuration.
Local Build Cache Leakage
When you build locally, the builder stage can contain sensitive artifacts (e.g., credentials from settings.xml). Ensure that such files are not copied into the final stage and that the builder stage is removed from the final image. The COPY --from=builder syntax only copies the specified files, so accidental leakage is unlikely if the Dockerfile is written correctly.
Actionable Next Steps
- Verify Docker Version
Rundocker versionand ensure the Engine is 17.05 or newer. Thesyntax=docker/dockerfile:1line requires this feature. - Create the Dockerfile
Place the example Dockerfile in the root of your project. Adjust the artifact name and runtime image as needed. - Build the Image
```bash # Build locally # Requires Docker Engine permissions # Replace demo-app with your image name docker build -t demo-app . ```Check the output for the
FROMlines and the finalENTRYPOINTto confirm the syntax is accepted. - Compare Image Sizes
```bash # Size of multi‑stage image docker images demo-app --format '{{.Size}}' ```Build a single‑stage version (e.g., using
openjdk:17-jdk-slimas the base) and run the same command. The multi‑stage image should be noticeably smaller. - Run the Container
```bash # Run the application docker run --rm -p 8080:8080 demo-app ```Verify that the endpoint (e.g.,
http://localhost:8080/health) returns the expected response. - Inspect Layers
```bash # Inspect the image layers docker image inspect demo-app ```Look for a single runtime layer containing only the JAR and any runtime dependencies.
- Integrate into CI
Update your CI pipeline to use the multi‑stage Dockerfile. The reduced image size will speed up build and deployment steps. - Monitor Runtime Security
Rundocker image inspect demo-appperiodically to ensure no unexpected packages appear in the final image.
By following these steps, you’ll reduce image size, lower deployment costs, and tighten your security posture without sacrificing functionality. Happy building!
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.