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.
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.
docker build -t app:single -f Dockerfile.single .docker build -t app:multi -f Dockerfile.multi .docker images | grep appapp single 1.02GBapp multi 184MB — 82% smallerThe 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 buildWORKDIR /srcCOPY go.* ./RUN go mod downloadCOPY . .RUN CGO_ENABLED=0 go build -o /out/app ./cmd/app# --- runtime stage ---FROM gcr.io/distroless/static:nonrootCOPY --from=build /out/app /appUSER nonrootENTRYPOINT ["/app"]
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.