Zero to Minimal: Advanced Docker Multi-Stage Optimization Guide
How to shrink container images from 1.2GB to 35MB using distroless, Alpine, layer caching, and buildkit secrets.
Bloated Docker images degrade developer velocity and broaden your attack surface. A standard Node.js or Go image containing build compilers, development headers, package managers, and package caches frequently exceeds 1GB.
By adopting multi-stage builds and distroless runtime bases, you can deliver sub-50MB production containers with zero CVE baggage.
Anatomy of an Optimized Multi-Stage Dockerfile
Consider this Go microservice build:
# Stage 1: Build & compile
FROM golang:1.23-alpine AS builder
WORKDIR /src
RUN apk add --no-cache git ca-certificates
# Cache dependencies independently
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
-ldflags="-s -w -extldflags '-static'" \
-o /bin/server ./cmd/api
# Stage 2: Scratch minimal runtime
FROM gcr.io/distroless/static-debian12:nonroot
USER nonroot:nonroot
COPY --from=builder /bin/server /server
EXPOSE 8080
ENTRYPOINT ["/server"]
Key Optimization Techniques
- Leverage BuildKit Mount Caching: Use
--mount=type=cache,target=/root/.cache/go-buildto retain compilation artifacts between CI pipeline runs. - Order Layer Cache by Volatility: Copy dependencies (
package.json,go.mod,requirements.txt) first and run installation before copying application source code. - Drop Package Managers in Final Layers: Production runtimes do not need
apt,apk,curl, orbash. Stripping binaries prevents shell injection exploits from pivoting.