Docker Multi-Stage Builds in 2026: Slash Your Container Image Size by 90%

If your Docker images are bloated with build tools, compilers, and dependencies your app doesn’t need at runtime, you’re wasting bandwidth, slowing deployments, and increasing your attack surface. Docker multi-stage builds solve this elegantly — letting you use one stage to build and another to run, producing minimal production images. In this guide, we’ll walk through practical multi-stage patterns that can cut your image size by 90% or more.

Why Image Size Matters More Than Ever

In 2026, with edge deployments, serverless containers, and auto-scaling Kubernetes clusters, every megabyte counts. Smaller images mean:

  • Faster cold starts — critical for serverless and scale-to-zero workloads
  • Lower bandwidth costs — especially across multi-region registries
  • Reduced attack surface — fewer packages means fewer CVEs
  • Quicker CI/CD pipelines — less time pushing and pulling layers

The Problem: Single-Stage Bloat

Here’s a typical single-stage Dockerfile for a Go application:

# ❌ Single-stage: ~1.1GB
FROM golang:1.23
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o server ./cmd/server
EXPOSE 8080
CMD ["./server"]

This image includes the entire Go toolchain, build cache, and source code — none of which are needed at runtime. The result? A 1.1GB image for a binary that’s only 15MB.

The Fix: Multi-Stage Builds

Multi-stage builds let you define multiple FROM instructions. Each one starts a new stage, and you can selectively copy artifacts between them:

# ✅ Multi-stage: ~12MB
# Stage 1: Build
FROM golang:1.23 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags='-s -w' -o server ./cmd/server

# Stage 2: Run
FROM scratch
COPY --from=builder /app/server /server
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
EXPOSE 8080
ENTRYPOINT ["/server"]

We went from 1.1GB to 12MB — a 99% reduction. The scratch base image is literally empty, containing only what we copy into it.

Key Techniques Used

  • CGO_ENABLED=0 — produces a statically linked binary (no glibc dependency)
  • -ldflags='-s -w' — strips debug symbols and DWARF info
  • COPY --from=builder — cherry-picks only the compiled binary and TLS certs

Multi-Stage for Node.js Applications

Node.js apps benefit enormously from multi-stage builds by separating dependency installation from the runtime:

# Stage 1: Install dependencies
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev

# Stage 2: Build (if using TypeScript/bundler)
FROM node:22-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 3: Production
FROM node:22-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY package.json ./
EXPOSE 3000
CMD ["node", "dist/index.js"]

This three-stage approach ensures devDependencies (TypeScript, testing tools, linters) never make it into production. Typical savings: 800MB → 150MB.

Multi-Stage for Python Applications

Python is trickier because of C extension compilation, but the pattern still works great:

# Stage 1: Build wheels
FROM python:3.13-slim AS builder
RUN apt-get update && apt-get install -y gcc libffi-dev
WORKDIR /app
COPY requirements.txt .
RUN pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt

# Stage 2: Install and run
FROM python:3.13-slim
WORKDIR /app
COPY --from=builder /wheels /wheels
RUN pip install --no-cache-dir --no-index --find-links=/wheels /wheels/* && rm -rf /wheels
COPY . .
EXPOSE 8000
CMD ["python", "-m", "uvicorn", "main:app", "--host", "0.0.0.0"]

The build stage compiles C extensions with gcc, but the runtime stage doesn’t need the compiler at all. Savings: ~40% smaller and no compiler in production.

Advanced Pattern: Build Cache Optimization

Combine multi-stage builds with Docker’s cache mounts for blazing-fast rebuilds:

# syntax=docker/dockerfile:1
FROM rust:1.82 AS builder
WORKDIR /app
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo 'fn main(){}' > src/main.rs
RUN --mount=type=cache,target=/usr/local/cargo/registry \
    --mount=type=cache,target=/app/target \
    cargo build --release
COPY src ./src
RUN --mount=type=cache,target=/usr/local/cargo/registry \
    --mount=type=cache,target=/app/target \
    cargo build --release && cp target/release/myapp /myapp

FROM gcr.io/distroless/cc-debian12
COPY --from=builder /myapp /
ENTRYPOINT ["/myapp"]

The --mount=type=cache directive preserves the cargo registry and build artifacts between builds, making incremental compilation nearly instant while still producing a tiny runtime image.

Choosing Your Runtime Base Image

Your choice of final stage base image has a huge impact:

  • scratch — 0MB, for static binaries only (Go, Rust)
  • distroless — ~2-20MB, includes libc but no shell or package manager
  • alpine — ~7MB, includes musl libc, shell, and apk
  • *-slim — ~50-80MB, Debian-based with minimal packages

Rule of thumb: use the smallest base that your application can run on. If you need a shell for debugging, use Alpine. If you don’t, go distroless or scratch.

Debugging Multi-Stage Builds

You can build and stop at any stage using --target:

# Build only the builder stage
docker build --target builder -t myapp:debug .

# Shell into it to inspect
docker run -it myapp:debug /bin/sh

This is invaluable for debugging build failures without waiting for the full pipeline.

Security Scanning: Before and After

Run a quick scan to see the CVE difference:

# Scan the bloated image
docker scout cves myapp:single-stage
# Result: 142 vulnerabilities (23 critical)

# Scan the multi-stage image
docker scout cves myapp:multi-stage
# Result: 0 vulnerabilities

Fewer packages literally means fewer things that can be exploited. A scratch-based image with a static binary has zero OS-level CVEs by definition.

Quick Checklist

  • ✅ Separate build and runtime stages
  • ✅ Use --omit=dev or equivalent to exclude dev dependencies
  • ✅ Choose the smallest viable base image for your final stage
  • ✅ Copy only the artifacts you need with COPY --from=
  • ✅ Strip binaries where possible (-ldflags='-s -w', strip)
  • ✅ Use cache mounts for faster rebuilds
  • ✅ Scan your final image for CVEs

Conclusion

Docker multi-stage builds are one of the highest-impact, lowest-effort optimizations you can make to your containerized applications. Whether you’re shipping Go microservices, Node.js APIs, or Python ML models, the pattern is the same: build heavy, ship light. Start applying these patterns today — your CI pipeline, cloud bill, and security team will thank you.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

Privacy Policy · Contact · Sitemap

© 7Tech – Programming and Tech Tutorials