How to Optimize Docker Images with Multi-stage Builds
Learning how to optimize Docker images with multi-stage builds is essential for creating efficient, production-ready containers. This powerful Docker feature allows you to use multiple FROM statements in a single Dockerfile, enabling you to separate build dependencies from runtime requirements. You’ll significantly reduce image sizes, improve security, and create cleaner deployment artifacts.
Multi-stage builds solve a common problem in containerization: bloated images filled with build tools, source code, and dependencies that aren’t needed at runtime. Traditional approaches required complex scripts or multiple Dockerfiles to achieve lean production images. With multi-stage builds, you can compile your application in one stage and copy only the necessary artifacts to a minimal runtime image.
This tutorial covers the complete process of implementing multi-stage builds for various application types. You’ll learn to create efficient Dockerfiles, understand build contexts, and apply optimization techniques that can reduce image sizes by 80-90%. By the end, you’ll master this essential Docker skill for professional container deployments.
Prerequisites and Requirements for Multi-stage Docker Builds
Before you begin learning how to optimize Docker images with multi-stage builds, ensure you have the necessary tools and knowledge in place. You’ll need Docker version 17.05 or later installed on your system, as this feature wasn’t available in earlier versions.
Your system should have at least 4GB of available disk space for building and storing multiple image layers during the optimization process. Basic familiarity with Docker commands, Dockerfile syntax, and containerization concepts is assumed throughout this tutorial.
You should understand fundamental Docker operations like building images, running containers, and managing volumes. Knowledge of at least one programming language (Python, Node.js, Go, or Java) will help you follow the practical examples more effectively.
The estimated time to complete this tutorial is 45-60 minutes, depending on your internet connection speed for downloading base images. Ensure you have administrative privileges on your system to install packages and modify Docker configurations if needed.
Step-by-Step Guide to Optimize Docker Images with Multi-stage Builds
Related article: Setup Pivpn Server on Ubuntu and Connect on Windows
Let’s start with a practical example that demonstrates the power of multi-stage builds. We’ll create a Go application and show the dramatic difference in image sizes.
Step 1: Create a Sample Go Application
First, create a simple Go web server to use as our example application:
mkdir docker-multistage-demo
cd docker-multistage-demo
cat > main.go << 'EOF'
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r http.Request) {
fmt.Fprintf(w, "Hello from optimized Docker container!")
})
fmt.Println("Server starting on port 8080...")
http.ListenAndServe(":8080", nil)
}
EOF
This creates a minimal web server that responds with a greeting message. The application is small but represents typical build requirements.
Step 2: Create a Traditional Single-stage Dockerfile
Create a traditional Dockerfile to see the baseline image size:
cat > Dockerfile.single << 'EOF'
FROM golang:1.21
WORKDIR /app
COPY main.go .
RUN go build -o main main.go
EXPOSE 8080
CMD ["./main"]
EOF
Build this image and check its size:
docker build -f Dockerfile.single -t go-app-single .
docker images go-app-single
You’ll notice the image size is approximately 800MB-1GB because it includes the entire Go toolchain, which isn’t needed at runtime.
Step 3: Implement Multi-stage Build Optimization
Now create an optimized multi-stage Dockerfile that separates build and runtime environments:
cat > Dockerfile.multi << 'EOF'
# Build stage
FROM golang:1.21 AS builder
WORKDIR /app
COPY main.go .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main main.go
# Runtime stage
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/main .
EXPOSE 8080
CMD ["./main"]
EOF
The `AS builder` clause names the first stage, allowing us to reference it later. The `COPY –from=builder` instruction copies only the compiled binary from the build stage.
Step 4: Build and Compare Multi-stage Images
Build the multi-stage version and compare sizes:
docker build -f Dockerfile.multi -t go-app-multi .
docker images | grep go-app
You’ll see the multi-stage image is typically 10-15MB compared to the 800MB+ single-stage version. This represents a 95%+ size reduction while maintaining identical functionality.
Step 5: Advanced Multi-stage Optimization Techniques
For even better optimization, implement these advanced techniques in your Dockerfile:
cat > Dockerfile.optimized << 'EOF'
# Build stage with specific Go version
FROM golang:1.21-alpine AS builder
# Install build dependencies
RUN apk add --no-cache git
WORKDIR /app
# Copy go mod files first for better layer caching
COPY go.mod go.sum ./
RUN go mod download
# Copy source code
COPY . .
# Build with optimization flags
RUN CGO_ENABLED=0 GOOS=linux go build
-a -installsuffix cgo
-ldflags '-w -s'
-o main main.go
# Minimal runtime stage
FROM scratch
# Copy SSL certificates for HTTPS requests
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
# Copy binary from builder stage
COPY --from=builder /app/main /main
EXPOSE 8080
ENTRYPOINT ["/main"]
EOF
The `scratch` base image provides the absolute minimal runtime environment. The `-ldflags ‘-w -s’` flags strip debug information, further reducing binary size.
Step 6: Test Your Optimized Images
Verify both images work identically:
docker run -d -p 8080:8080 --name single go-app-single
docker run -d -p 8081:8080 --name multi go-app-multi
curl http://localhost:8080
curl http://localhost:8081
docker stop single multi
docker rm single multi
Both containers should respond identically, confirming your optimization maintains functionality while dramatically reducing resource usage.
Troubleshooting Common Multi-stage Build Issues
When implementing how to optimize Docker images with multi-stage builds, you might encounter several common issues. Understanding these problems and their solutions will save you significant debugging time.
Missing Dependencies in Runtime Stage
The most frequent issue occurs when the runtime image lacks required libraries or certificates. If your application fails with SSL errors, add certificates to your runtime stage:
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
For applications requiring shared libraries, use `ldd` to identify dependencies and copy them explicitly, or choose a base image like `alpine` instead of `scratch`.
Build Context Size Problems
Large build contexts slow down multi-stage builds significantly. Create a comprehensive `.dockerignore` file to exclude unnecessary files:
cat > .dockerignore << 'EOF'
.git
.gitignore
README.md
Dockerfile
.dockerignore
node_modules
.log
.DS_Store
EOF
Cross-platform Build Issues
When building for different architectures, specify target platforms explicitly:
docker buildx build --platform linux/amd64,linux/arm64 -t myapp:latest .
This ensures your optimized images work across different deployment environments without architecture-specific problems.
For Node.js applications, you might need to rebuild native modules in the runtime stage if using different base images between build and runtime stages.
Conclusion and Next Steps
You’ve successfully learned how to optimize Docker images with multi-stage builds, achieving dramatic size reductions while maintaining full application functionality. This technique is crucial for production deployments where image size directly impacts deployment speed, storage costs, and security surface area.
The multi-stage approach separates concerns cleanly, keeping build tools isolated from runtime environments. You can apply these principles to any application stack, from compiled languages like Go and Rust to interpreted languages like Python and Node.js. Consider exploring Docker’s official multi-stage documentation for advanced scenarios and BuildKit features for additional optimization capabilities.
Next steps include implementing automated builds in CI/CD pipelines, exploring Docker layer caching strategies, and investigating container security scanning tools that work particularly well with minimal runtime images. Your newly optimized containers will deploy faster, consume fewer resources, and provide better security through reduced attack surfaces.
