Shrink Your Docker Images: A Practical Guide to Multi-Stage Builds
Multi-stage Docker builds let you separate build-time dependencies from runtime artifacts, drastically reducing image size and attack surface. We walk through a practical example with Go and show how to verify the results.
10 Jul 2026, 13:38 UTC

The Problem: Bloated Production Images
When you build a Docker image with all your build tools, dependencies, and source code included, you end up with images that are megabytes or even gigabytes larger than they need to be. This isn't just a storage issue—larger images mean slower deployments, longer pull times, and a bigger attack surface for potential security vulnerabilities.
Consider a typical Node.js application. A straightforward Dockerfile might install npm packages, run a build script, and include the entire source directory. The result? An image that carries around development dependencies it never needs at runtime.
The Solution: Multi-Stage Builds
Docker's multi-stage builds let you use multiple FROM statements in a single Dockerfile. Each stage can build upon the previous one, but you only copy what you need into the final production image. This means compilers, package managers, and build scripts stay in earlier stages while only the compiled application makes it to production.
The key insight is that you're not limited to one image per Dockerfile. You can have a builder stage that compiles your code, then a minimal runtime stage that only contains the final artifacts.
How It Works: A Real Example
Let's look at a concrete example with a Go application. First, here's a traditional Dockerfile that creates a bloated image:
FROM golang:1.21-alpine
WORKDIR /app
COPY . .
RUN go mod download && go build -o main .
EXPOSE 8080
CMD ["./main"]
This image includes the entire Go toolchain, source files, and build cache. Now, here's the multi-stage version:
# Stage 1: Builder
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o main .
# Stage 2: Runtime
FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/main .
EXPOSE 8080
CMD ["./main"]
The difference is striking. The final image only contains the compiled binary and a minimal Alpine Linux base. No compilers, no source code, no build tools.
Verification: Checking Your Work
After implementing multi-stage builds, verify the results:
- Compare sizes: Run
docker imagesbefore and after to see the size reduction - Inspect layers: Use
docker history <image_id>to confirm build tools are absent from the final image - Test runtime: Try running build commands like
gccorgoinside the production container to ensure they're missing
For our Go example, you should see the final image drop from ~500MB to under 10MB.
Trade-offs and Considerations
While multi-stage builds are powerful, they come with trade-offs. Over-segmenting your Dockerfile into too many stages can make it harder to maintain, especially for teams with junior developers. The caching behavior also changes—modifications in early stages invalidate all subsequent stages, which can slow down builds if not structured carefully.
Be especially careful about what you copy. Using COPY --from=builder /app . might accidentally include source files or secrets that shouldn't be in production. Always specify exact paths to the artifacts you need.
Take Action Today
Start by identifying your largest Docker images—likely those built from compiled languages like Go, Rust, or Java. Apply the multi-stage pattern: create a builder stage with all your build tools, then a minimal runtime stage that only copies the final artifacts. Verify the results with docker images and docker history, and you'll see immediate improvements in image size and security posture.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.