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.
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
- ✅
MaxRAMPercentageinstead of fixed heap - ✅ Non-root user (distroless runs as nonroot by default)
- ✅ A real
.dockerignore(don’t shiptarget/,.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.
Related articles

Building a Full SaaS Application with NestJS, React, PostgreSQL and Docker
A step-by-step, production-grade guide: architecture, multi-tenant database design, JWT auth, NestJS APIs, a React frontend, Docker, CI/CD with GitHub Actions, scalability, and the best practices that hold up in production.

MCP Servers Explained: The Future of AI Tool Integration
What the Model Context Protocol (MCP) is, how MCP servers work, why it beats bespoke API glue, a hands-on server example, the growing ecosystem, security considerations, and where it's all heading.

Why AI Coding Agents Will Change Software Development in 2026
What AI coding agents are, how they differ from autocomplete assistants, the tools that matter in 2026, real use cases, the productivity math, security risks, and how to fold agents into your daily workflow without regrets.
Have a project or an idea?
We don't just write about software — we build it. Tell us what you're working on and we'll get back within 1–2 business days.