Skip to content
LatchkeyLatchkey home

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.

Applying this to your pipeline

  • Measure before changing. Most CI optimisation targets the wrong step because the slow one is assumed rather than timed.
  • Cache what is expensive to produce and cheap to validate, and key the cache to the exact tool version.
  • Fail fast: run the cheapest checks that can reject a change first, so an expensive job never starts on code that cannot pass.
  • Prefer determinism over speed when they conflict. A fast pipeline nobody trusts gets re-run, which is slower than a slow one that is believed.

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.

Frequently asked questions

What is What is a Multi-Stage Build? smaller Images, cleaner output?
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.

Related guides

References

Run this faster and cheaper on Latchkey managed runners - self-healing included. Start free → 30-day trial · No credit card