Multi-stage builds, in depth
Leave every compiler and package manager behind.
Every tool in your build stage is attack surface in production you never wanted. A compiler can build a payload; a package manager can fetch one; a shell can run one; leftover source and credentials can leak. The multi-stage build is the mechanism that severs the build environment from the runtime environment: do all the messy work in a builder stage, then copy only the finished artifact into a clean, minimal final stage that ships nothing else.
FROM golang:1.23 AS build # fat: compiler, git, shell, ~800MBWORKDIR /srcCOPY go.mod go.sum ./RUN go mod downloadCOPY . .RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /out/server ./cmd/serverFROM gcr.io/distroless/static:nonroot # ~2MB: no shell, no pkg mgr, non-rootCOPY --from=build /out/server /serverUSER nonrootENTRYPOINT ["/server"]
Copy the artifact, never the stage
The discipline that makes it work is copying the narrowest possible thing with COPY --from — one binary, one directory of assets — never COPY --from=build /, which drags the whole toolchain back and defeats the point. Name stages with AS and you can also build a debug or test target from the same file with --target, so one Dockerfile yields a tiny production image and a fuller debugging image that share an identical build.
$ docker build -t svc:1.0 .$ docker images svcREPOSITORY TAG SIZEsvc 1.0 6.4MB # the 800MB builder never shipped$ docker build --target build -t svc:debug . # a full-toolchain image when you need one
Build-time secrets stay out of layers
Discarded builder stages never reach the registry, but do not rely on that for secrets — a private key or token COPY-ed into a builder still lives in that stage’s layers during the build and can leak through cache. Use BuildKit’s secret mounts, which expose a credential to a single RUN without ever writing it to a layer.
# syntax=docker/dockerfile:1RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \npm ci # the token is present for this RUN only, never in any layer# build with: docker build --secret id=npmrc,src=$HOME/.npmrc .