Separating a build stage from a runtime stage to shrink final image size dramatically, worked through a concrete Java Maven-build-to-slim-JRE example, with COPY --from= as the mechanism.
Published September 23, 2026
Building a Java application needs a JDK, Maven/Gradle, and the full dependency tree used only DURING compilation — none of which the application actually needs to RUN. A single-stage Dockerfile that builds and runs in the same image drags all of that build tooling into the final production image, often adding hundreds of megabytes of dead weight that exists purely as attack surface and slower pull times, never actually used at runtime.
# Stage 1: build — has the full JDK and Maven, produces a JAR
FROM maven:3.9-eclipse-temurin-21 AS build
WORKDIR /build
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn package -DskipTests
# Stage 2: runtime — only a slim JRE, none of the build tooling
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=build /build/target/app.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]
Each FROM starts a genuinely NEW, independent build stage — named here via AS build for the first one. COPY --from=build in the second stage reaches back into the FIRST stage's filesystem and copies out just the one artifact actually needed (the built JAR) — everything else from the build stage (the JDK, Maven, the full dependency cache, intermediate compiled classes) is simply discarded, never becoming part of the final image at all.
Single-stage (Maven + JDK + full app): ~600-800 MB
Multi-stage (slim JRE + JAR only): ~150-200 MB
This isn't a marginal optimization — a multi-stage build routinely cuts final image size by 3-4x for a typical Java service. Smaller images pull faster (meaningfully reducing container startup time during a deployment or autoscaling event — directly relevant to Kubernetes' rolling update speed in Deployments & Rollouts) and have less attack surface (fewer installed packages that could carry a known vulnerability, connecting to Health Checks & Production Best Practices' image-scanning discussion).
Stages don't need to be named (FROM x AS name) to work — an unnamed stage can be referenced by its numeric index (COPY --from=0 ...) — but naming them is standard practice for readability, especially once a Dockerfile has more than two stages (e.g. a separate stage for running tests, distinct from the build stage, copying nothing from it into the final image at all — its only purpose is to fail the build if tests fail).
Q: Can a multi-stage build have more than two stages, and why would it? A: Yes — a common three-stage pattern adds a dedicated TEST stage between build and runtime (running the test suite, with the build failing if tests fail) purely for CI feedback, contributing nothing to the final image; multi-stage builds are a general mechanism for isolating any BUILD-TIME concern (compiling, testing, linting) from the RUNTIME image, not limited to exactly two stages.
Q: Does the build stage's layer caching still work the same way as a single-stage build? A: Yes, identically — the same dependency-manifest-before-source-code ordering principle from Docker Fundamentals applies within each individual stage; multi-stage builds don't change layer-caching mechanics, they just scope which layers actually make it into the final image.
Q: Is there a runtime cost to having multiple stages, since Docker technically builds all of them? A: Build TIME cost yes (every stage referenced, directly or via COPY --from=, does get built), but zero runtime cost — the final image contains ONLY the last stage plus whatever was explicitly copied in via COPY --from=; earlier stages' layers are never pulled or run in production at all.
Q: Why not just use a JDK-based image at runtime instead of a separate JRE, to simplify to one stage? A: A JRE lacks the compiler and development tooling a JDK includes — for a compiled artifact (the JAR) that only needs to be EXECUTED, not recompiled, a JRE is sufficient and meaningfully smaller; using a full JDK at runtime is exactly the kind of unnecessary build-tooling bloat multi-stage builds exist to eliminate.