BlogCI/CD

Multi-stage Docker builds that cut image size by 80%

Split build tooling from the runtime so your final image ships only the binary and its deps — smaller, faster, safer.

Jun 24, 2026·9 min readIntermediate

The fastest way to shrink an image and its attack surface is to stop shipping the toolchain that built it. A multi-stage build compiles in one stage and copies only the finished artifact into a tiny runtime stage — the compilers, headers, and package caches never make it into what you deploy.

bash — the payofflive
docker build -t app:single -f Dockerfile.single .
docker build -t app:multi -f Dockerfile.multi .
docker images | grep app
app single 1.02GB
app multi 184MB — 82% smaller

The problem: one fat stage

A single-stage Go or Node build carries the entire SDK, build deps, and cache into production. That is hundreds of megabytes an attacker can pivot through, and none of it runs your app.

The fix: build, then copy

Dockerfile.multi
# --- build stage ---
FROM golang:1.22 AS build
WORKDIR /src
COPY go.* ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /out/app ./cmd/app
# --- runtime stage ---
FROM gcr.io/distroless/static:nonroot
COPY --from=build /out/app /app
USER nonroot
ENTRYPOINT ["/app"]
What ends up in the image
Single stage
Go toolchain (~450MB)
apt caches
source + .git
shell + package manager
1.02GB total
Multi stage
static binary only
no shell, no apt
no source
runs as nonroot
184MB total
Only copy what you name
COPY --from=build pulls a single path out of the build stage; everything else there is discarded. That is the whole trick.

Push it further

Add a .dockerignore so node_modules and .git never enter the build context, pin the base image by digest, and keep the runtime on distroless or scratch.

Go deeper in a courseAdvanced container securityMinimal images, runtime hardening, and the escapes they stop.View course

Related posts