
Docker Images with Multi-Stage Builds
- 4 minsDocker Images with Multi-Stage Builds
If you’ve ever struggled with oversized Docker images that take forever to ship or open up security holes, then you’ll appreciate what we’re covering today: Multi-Stage Docker Builds — a smarter, cleaner, and more scalable approach to building container images.
Overview
In this tutorial, you’ll learn:
- What is a multi-stage Docker build?
- Optimizing a Node.js image
- Trimming down a Golang image
- Size & performance impact
- Optimization tips
- Best practices
🔗 Official Docker Multi-Stage Build Docs
What Is a Multi-Stage Build?
A multi-stage build uses multiple FROM
layers in one Dockerfile. Each stage does a specific job — like compiling code or installing dependencies — and only the necessary artifacts are passed into the final image.
Goal: Keep your production image small, clean, and dependency-free.
Example 1: Node.js App
Traditional Dockerfile (Not Ideal)
Create a file: Dockerfile.bloat
FROM node:18
WORKDIR /app
COPY . .
RUN npm install && npm run build
CMD ["npm", "start"]
Problems:
- Includes dev dependencies and source code.
- Sluggish performance due to a larger image.
- More layers = more security concerns.
You can see my image is 1.09GB
Multi-Stage Version
Create a new file: Dockerfile
# First stage: build
FROM node:18 AS builder
WORKDIR /app
# Copy only package files first for better caching
COPY package*.json ./
# Install all dependencies
RUN npm install
# Copy the rest of the application
COPY . .
# Run the build script
RUN npm run build
# Second stage: production image
FROM node:18-slim
WORKDIR /app
# Copy built app and node_modules from builder
COPY --from=builder /app ./
# Use npm start, same as in the traditional version
CMD ["npm", "start"]
Result:
- Production image has only what’s required to run.
- Faster container start times.
- Lower attack surface.
The image is 192MB
Quick Comparison
Run:
docker images | grep -E 'nodeapp'
Example 2: Go Application
Bloated Version
Create Dockerfile.golang.bloat
:
FROM golang:1.21
WORKDIR /app
COPY . .
RUN go build -o app
CMD ["./app"]
Issues:
- Over 700MB in size.
- Retains Go compiler and source code.
Optimized Multi-Stage Dockerfile
FROM golang:1.21 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o app
FROM alpine:latest
WORKDIR /root/
COPY --from=builder /app/app .
CMD ["./app"]
Image Size: From ~700MB ➜ ~20MB (can drop under 5MB with scratch
)
Why Multi-Stage Wins:
- Reduces build time with Docker cache.
- Smaller push/pull size.
- Keeps images tidy and focused.
- Simplifies debugging and maintenance.
Pro Tips for Go Builds
Tip | Description |
---|---|
CGO_ENABLED=0 | Builds a static binary, no C libraries |
GOOS , GOARCH | Cross-compile for Linux, Windows, ARM, etc. |
FROM scratch | Minimal image with just the binary |
distroless base | Secure images with no package manager |
Best Practices
- Use base images like
alpine
ornode:slim
- Use
.dockerignore
to skip unnecessary files (.git
,docs
,node_modules
) - Separate build, test, and production steps into clear stages
- Always minimize what you copy into the final stage
Example .dockerignore
:
.git
node_modules
*.md
tests/
Final Thoughts
Multi-stage builds are a simple yet powerful way to:
- Reduce Docker image sizes
- Enhance security
- Speed up deployment pipelines
🔗 Explore more in the official Docker docs
Thanks for reading!
—
Guneycan Sanli