Skip to content
DevOps Apr 30, 2026 6 min read

Dockerizing a Java App the Right Way in 2026

Multi-stage builds, layered JARs, distroless images, and the JVM flags that make containers behave. A production Dockerfile you can actually copy.

D

DevCraftly Team

DevCraftly

Share
Dockerizing a Java App the Right Way in 2026

A naive FROM openjdk Dockerfile produces a 600MB image that rebuilds from scratch on every code change and ignores the container’s memory limits. Let’s fix all three problems.

Use multi-stage builds

Build with a full JDK, run with a slim JRE. The build tools never ship to production.

# ---- build stage ----
FROM eclipse-temurin:21-jdk AS build
WORKDIR /app
COPY . .
RUN ./mvnw -q -DskipTests package

# ---- runtime stage ----
FROM eclipse-temurin:21-jre AS runtime
WORKDIR /app
COPY --from=build /app/target/app.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

Layer your JAR for fast rebuilds

Spring Boot can split a JAR into layers (dependencies change rarely, your code changes constantly). Ordering COPY from least- to most-volatile means Docker reuses cached layers:

FROM eclipse-temurin:21-jre AS runtime
WORKDIR /app
COPY --from=build /app/target/dependencies/ ./
COPY --from=build /app/target/spring-boot-loader/ ./
COPY --from=build /app/target/snapshot-dependencies/ ./
COPY --from=build /app/target/application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]

Respect container memory limits

Modern JVMs are container-aware, but be explicit. Set a percentage of the container’s memory rather than a fixed -Xmx:

ENV JAVA_OPTS="-XX:MaxRAMPercentage=75 -XX:+UseG1GC"
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]

Without MaxRAMPercentage, a JVM in a 512MB container can still try to grab a heap sized for the host machine — and get OOM-killed.

Go distroless for the smallest, safest image

Distroless images contain only your app and the JRE — no shell, no package manager, a tiny attack surface:

FROM gcr.io/distroless/java21-debian12
COPY --from=build /app/target/app.jar /app.jar
ENTRYPOINT ["/app.jar"]

The checklist

  • ✅ Multi-stage build (JDK to build, JRE/distroless to run)
  • ✅ Layered copy for cache-friendly rebuilds
  • MaxRAMPercentage instead of fixed heap
  • ✅ Non-root user (distroless runs as nonroot by default)
  • ✅ A real .dockerignore (don’t ship target/, .git, node_modules)

Do these five things and your image drops from ~600MB to ~180MB, rebuilds in seconds, and behaves predictably under a memory limit.

#docker #java #devops #containers