Skip to main content

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=0 produces a statically-linked binary — no libc needed
  • The leanest base image wins. alpine or even scratch for Go binaries