Stackness
All moves

Shrinking Docker images with multi-stage builds

by Elif Yilmaz

Languages & Frameworks

Before I adopted multi-stage builds, my Go service images were bloated. I'd compile everything inside the container, and then ship the entire build layer to production. The image sat around 800 megabytes because it contained the Go toolchain, git, and every build dependency. That meant every deployment pulled that extra weight, and every running container exposed toolchain binaries that had no business being in production.

I restructured my Dockerfile to build in one stage and copy only the final binary into a fresh stage. The first stage does all the compilation work with all the heavy dependencies available. Then I use a FROM scratch or FROM alpine:latest for the final stage, and I COPY --from=builder /app/service /service. The resulting image drops to about 30 megabytes for a typical CLI tool or microservice.

Here's the concrete shift: my build stage runs RUN go build -o /app/service . with the full Go installation present, but the runtime stage starts from scratch and only contains that single statically-compiled binary. No source code, no build cache, no compiler. The attack surface shrinks because there is literally nothing left to exploit except the binary itself.

The caveat is that this approach works cleanly for statically-compiled binaries, which Go excels at, but falls apart if you need runtime dependencies or configuration files. If your service needs shared libraries or certificate bundles, you have to explicitly copy those into the final stage too, which adds back some size and complexity. It is not a universal solution for all container workflows.

FROM golang:1.25-alpine AS builder WORKDIR /src COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 go build -o /bin/api ./cmd/api FROM alpine:3.24 RUN apk add --no-cache ca-certificates COPY --from=builder /bin/api /bin/api ENTRYPOINT ["/bin/api"]

Tools

Shrinking Docker images with multi-stage builds | Stackness