Mert Tosun
← Posts
Shrinking Docker Images: Multi-stage Builds and Distroless Techniques

Shrinking Docker Images: Multi-stage Builds and Distroless Techniques

Mert TosunDevOps

Large container images cost storage, pull time, and attack surface (CVEs). The most effective pattern to shrink a Docker image is a multi-stage build; pairing it with a distroless or minimal runtime base is the second lever.

What is multi-stage?

The first stage contains compilers and dev tools; the final stage copies only what you need to run. The final image does not ship git, build-essential, or dev node_modules.

Go example

FROM golang:1.22-bookworm AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /app/server ./cmd/server

FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /app/server /server
USER nonroot:nonroot
ENTRYPOINT ["/server"]

CGO_ENABLED=0 helps produce a static binary suitable for distroless (project-dependent).

Node.js sketch

FROM node:20-bookworm AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev

FROM node:20-bookworm AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

FROM gcr.io/distroless/nodejs20-debian12
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules

Distroless has no shell — great for prod, harder to exec debug; keep a separate debug target if needed.


Distroless vs Alpine

Base Size Notes
distroless Very small No package manager, smaller surface
alpine Small musl quirks for some native deps
debian-slim Medium Easier debugging

Alpine is tempting; glibc-expecting binaries can surprise you. Static Go builds + distroless are a common combo.


Layer caching

Put COPY go.mod go.sum and RUN go mod download early so dependency layers rarely invalidate. Same idea for package.json + npm ci.


Security

Smaller images mean fewer packages and fewer CVEs. Still rebuild bases regularly and run scanners (docker scout, registry scans) in CI.


Summary

  • Multi-stage builds keep toolchains out of the final image.
  • Distroless or minimal bases plus static binaries shrink Docker images and attack surface.
  • Be careful with Alpine; distroless/static + static Go is a proven default.

Pair with a lean data path — e.g. PgBouncer connection pooling — for end-to-end efficiency.