What Is a Multi-Stage Build? Smaller Images, Cleaner Output
A multi-stage build compiles in one throwaway stage and copies only the finished artifact into a small final stage - so your image ships output, not toolchains.
Build tools, compilers, and dev dependencies are needed to produce an artifact but not to run it. A multi-stage Dockerfile lets you use a heavy builder stage and then start a fresh, minimal final stage that contains only what you ship. The result is a dramatically smaller and safer image.
The core idea
You declare multiple FROM stages in one Dockerfile. Earlier stages do the heavy lifting; the final stage starts clean and uses COPY --from=<stage> to pull in just the compiled output. Everything left behind in earlier stages is discarded.
A Go example
Stage one:
FROM golang:1.22 AS build
WORKDIR /src
COPY . .
RUN go build -o app .
Stage two:
FROM gcr.io/distroless/static
COPY --from=build /src/app /app
ENTRYPOINT ["/app"]
The final image has the binary and nothing else.
Why it shrinks images so much
The Go toolchain image is hundreds of megabytes; the final distroless image is a few. Because only the COPY --from artifact lands in the final stage, all of that build weight never ships.
Other wins
- Fewer packages means fewer vulnerabilities in the running image.
- Secrets used during build can stay in the builder stage.
- You can target multiple final stages (e.g. test vs prod) from one file.
Multi-stage builds in CI
Multi-stage builds cache each stage, so unchanged builder steps are skipped. On managed runners with warm caches, the expensive compile stage is reused across jobs, and only the final copy reruns.
Key takeaways
- Multi-stage builds keep build tools out of the final image.
- COPY --from pulls only the finished artifact into a clean final stage.
- The result is far smaller, safer images with no toolchain weight.