Shaving 200MB Off Our Docker Image With Multi-Stage Builds
1 min read
The before
A single-stage Dockerfile that started from golang:1.22:
FROM golang:1.22
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o server .
CMD ["./server"]
Result: 1.2GB image. The Go compiler, OS headers, and package cache all shipped to production.
The after
# Build stage
FROM golang:1.22-alpine AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /server .
# Runtime stage
FROM alpine:3.20
RUN apk add --no-cache ca-certificates tzdata
COPY --from=builder /server /server
EXPOSE 8080
CMD ["/server"]
Result: 22MB. Two stages, zero unnecessary layers.
What I learned
-ldflags="-s -w"strips debug symbols (saves ~40% on binary size)CGO_ENABLED=0produces a statically-linked binary — no libc needed- The leanest base image wins.
alpineor evenscratchfor Go binaries