How to Shrink Docker Images with Multi‑Stage Builds: A Step‑by‑Step Guide
Learn why smaller Docker images boost build speed and deployment efficiency, then master Docker’s multi‑stage build technique—including basic concepts, a Go example, layer reduction, cache optimization, minimal base images, non‑root users, and build arguments—to produce lightweight, secure, and maintainable containers.
Why Multi‑Stage Builds Matter
In containerized development, a smaller image means faster builds and deployments. Traditional Dockerfiles often bundle build tools and dependencies into the final image, inflating size, reducing security, and slowing CI pipelines.
1. Traditional Build vs. Multi‑Stage Build
Typical single‑stage Dockerfiles compile, package, and install dependencies all in one image, leading to:
Redundant dependencies that bloat the image.
Lower security due to unnecessary tools.
Reduced build efficiency because each run reinstalls everything.
Multi‑stage builds split the process into separate stages, copying only the final artifacts into a minimal runtime image.
2. Basic Multi‑Stage Build Example (Go Web App)
Source Code (main.go)
package main
import (
"fmt"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, Docker Multi‑Stage Build!")
}
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}Dockerfile Using Multi‑Stage
FROM golang:1.21 AS builder
WORKDIR /app
COPY . .
RUN go build -o app
FROM alpine:latest
WORKDIR /root/
COPY --from=builder /app/app .
CMD ["./app"]The first stage ( builder) compiles the binary with the full Go toolchain. The second stage uses a tiny alpine:latest image (≈5 MB) and copies only the compiled binary, discarding the build environment.
3. Advanced Optimizations
3️⃣ Reduce Layers & Optimize Cache
Each RUN, COPY, or ADD creates a new layer. Combine commands to cut layers, e.g.:
RUN apt‑get update && apt‑get install -y \
curl \
git && rm -rf /var/lib/apt/lists/*Leverage Docker’s cache by copying immutable files (e.g., go.mod, go.sum) before the rest of the source:
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o app4️⃣ Use Smaller Base Images
Alpine : alpine:latest (~5 MB) is far smaller than ubuntu (~29 MB).
Distroless : gcr.io/distroless/static removes even more, leaving only the runtime.
FROM golang:1.21 AS builder
WORKDIR /app
COPY . .
RUN go build -o app
FROM gcr.io/distroless/static
COPY --from=builder /app/app /app
CMD ["/app"]This can shrink the final image to under 5 MB.
5️⃣ Run as Non‑Root
Running as root poses security risks. Add a non‑root user in the final stage:
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser6️⃣ Parameterize Builds with ARG
Make Dockerfiles reusable by passing variables:
ARG GO_VERSION=1.21
FROM golang:${GO_VERSION} AS builderBuild with a specific version:
docker build --build-arg GO_VERSION=1.20 -t myapp .4. Summary of Benefits
Reduced Image Size : Only runtime files remain, cutting megabytes.
Faster Builds : Layer caching and fewer layers speed up CI/CD pipelines.
Improved Security : Fewer packages and non‑root execution lower attack surface.
Better Maintainability : Build arguments and modular stages make Dockerfiles adaptable.
By adopting Docker multi‑stage builds, developers can keep the convenience of full‑featured build environments while delivering lean, secure, and fast‑to‑deploy container images.
Full-Stack DevOps & Kubernetes
Focused on sharing DevOps, Kubernetes, Linux, Docker, Istio, microservices, Spring Cloud, Python, Go, databases, Nginx, Tomcat, cloud computing, and related technologies.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
