Docker Multi-Stage Builds: Shrinking Your Image 40x
Is your Go app's Docker image 900 MB? A hands-on guide to cutting it down to ~20 MB with multi-stage builds, while improving security and speeding up deploys.

The problem
Many Dockerfiles look like this:
FROM golang:1.23
WORKDIR /app
COPY . .
RUN go build -o server .
CMD ["./server"]
It works. But the final image still contains the entire Go toolchain, build cache, source code and unnecessary utilities. It can easily exceed 900 MB. That means:
- Slow deploys: hundreds of megabytes are pushed to the registry and pulled to the server every time.
- Larger attack surface: every extra package in the image is a potential vulnerability.
- Wasted resources: disk, bandwidth and CI time.
The fix: multi-stage builds
The idea is simple: build the app in one stage, and only run the finished binary in another.
# Stage 1: build
FROM golang:1.23 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o server .
# Stage 2: runtime
FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/server /server
USER nonroot:nonroot
ENTRYPOINT ["/server"]
Result: an image of roughly 15–25 MB.
What changed?
AS builder: we name the first stage so we can copy files out of it later.go.modis copied separately: dependencies change less often than code, so Docker caches this layer and builds get faster.CGO_ENABLED=0: produces a static binary with no dependency on external C libraries.-ldflags="-s -w": strips debug information, making the binary smaller.distroless: no shell, no package manager. Just your app.USER nonroot: the container runs without root privileges.
Note: distroless has no shell, so
docker exec -it ... shwon't work. For debugging, use thestatic-debian12:debugvariant.
Does it work for Python too?
Yes. Build wheels in the builder stage and use a slim image at runtime:
FROM python:3.12 AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt
FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /wheels /wheels
RUN pip install --no-cache-dir /wheels/* && rm -rf /wheels
COPY . .
CMD ["python", "main.py"]
The gains aren't as dramatic as with Go. Still, compilers and build tools stay out of the final image, which helps both size and security.
Takeaway
Multi-stage builds are one of the easiest optimizations out there: five minutes spent on your Dockerfile pays off in speed and security on every deploy. If your project still has a single FROM, try it today and compare the output of docker images.