Shrinking Docker Images with BuildKit Multi‑Stage Builds
Learn how to cut Docker image size and build time by splitting a Dockerfile into separate compile and runtime stages using BuildKit.
05 Nov 2025, 15:07 UTC

Problem: Bloated Docker Images Slow Down Deployments
When you ship a container, the image size directly impacts pull time, storage cost, and cold‑start latency, especially for serverless or edge workloads. A typical Go binary built in a single stage can easily exceed 100 MB because the image contains the entire build toolchain, caches, and intermediate files.
Thesis: Use BuildKit Multi‑Stage Builds to Separate Compilation from Runtime
The goal is to keep only the final artifact in the runtime image, eliminating unnecessary layers and shrinking the image to under 10 MB. Docker BuildKit provides the --target flag and parallel execution, making this separation straightforward.
How BuildKit Multi‑Stage Works
BuildKit treats a Dockerfile as a series of stages. Each stage runs in its own isolated environment, can be executed in parallel, and benefits from aggressive layer caching. The syntax FROM --target=builder tells the builder to start from a previously defined stage named builder. The final image is produced from the stage referenced by --target=runtime (or simply the last stage if omitted).
Key requirements:
- Docker daemon must have BuildKit enabled (DOCKER_BUILDKIT=1 or daemon option).
- Docker client and server versions 20.10 or newer support the
--targetflag. - The Dockerfile must define at least two named stages (commonly
builderandruntime).
Worked Example: Minimal Go Binary Image
Below is a concise Dockerfile that demonstrates the pattern. It uses the official Golang image for the builder stage and scratch for the runtime stage.
# syntax=docker/dockerfile:1
# ---------- Builder stage ----------
FROM golang:1.22-alpine AS builder
WORKDIR /src
COPY . .
# Cache go.mod and go.sum separately for faster rebuilds
RUN go mod download
RUN go build -o /app/main .
# ---------- Runtime stage ----------
FROM scratch AS runtime
COPY --from=builder /app/main /app/main
ENTRYPOINT ["/app/main"]
Explanation of each part:
- syntax=docker/dockerfile:1 enables BuildKit features.
- The
AS builderline names the first stage. - All compilation steps run in the builder stage, where the Go toolchain and caches live.
AS runtimenames the final stage; it starts fromscratch, a zero‑size base.- The
COPY --from=buildercommand extracts only the compiled binary from the builder stage.
To build the image, run the following commands from the directory containing the Dockerfile.
- Enable BuildKit (if not already set globally):
export DOCKER_BUILDKIT=1. This is a one‑time environment variable for the shell; the daemon will inherit it. - Build the builder stage first (optional, but useful for debugging):
This produces an intermediate image that contains the full build environment.docker build --target=builder -t myapp:builder . - Build the final image:
The client contacts the daemon, which executes the builder stage, caches its layers, then creates the runtime image by copying the binary.docker build --target=runtime -t myapp:final .
Verification steps:
- Check daemon support:
Look for “BuildKit” in the Server section.docker version - Confirm the flag is available:
You should seedocker build --help | grep target--targetlisted. - Inspect final image size:
The size should be close to the binary (typically 5‑10 MB).docker image inspect myapp:final --format='{{.Size}}' - Compare build logs: run
and note the “Stage build” messages; faster builds and fewer intermediate layers indicate success.docker build --progress=plain .
Trade‑off: Readability and Determinism
While multi‑stage builds dramatically shrink images, they can make the Dockerfile harder to read, especially when many stages interact or when you need to pass environment variables between stages. Additionally, any step that depends on network access (e.g., go mod download) may introduce nondeterministic builds if the remote repository changes. Pinning versions and using --mount=type=cache can mitigate these risks, but the added complexity is a real maintenance cost.
Closing: Take Action and Measure the Gain
If your current images are larger than necessary or CI build times are high, try converting a single‑stage Dockerfile to a multi‑stage one using BuildKit. Start with a simple example like the Go binary above, then iterate. Measure the resulting image size and build duration; you should see a clear reduction in both. Remember to enable BuildKit on the daemon and verify the version requirement before integrating the pattern into CI pipelines.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.