# What Is a Multi-Stage Build? Smaller Images, Cleaner Output

> A multi-stage build uses several FROM stages so build tools stay out of the final image. Learn how it works, with an example, and why it slashes image size.

Source: https://latchkey.dev/learn/ci-explained/what-is-a-multi-stage-build  
Updated: 2026-06-26

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.

## FAQ

### 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.

---

Latchkey runs CI/CD that repairs its own failures. Agent entry points: https://latchkey.dev/agent.txt, https://latchkey.dev/openapi.json, https://latchkey.dev/llms.txt
