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

If your Docker images are bloated and slow to deploy, multi-stage builds are the single most impactful optimization you can make in 2026. By separating your build environment from your runtime environment, you can reduce image sizes from gigabytes to mere megabytes — speeding up CI/CD pipelines, cutting cloud costs, and improving security. In this guide, we’ll walk through practical, real-world examples of multi-stage builds for Node.js, Python, and Go applications.

What Are Multi-Stage Builds?

Multi-stage builds let you use multiple FROM statements in a single Dockerfile. Each FROM starts a new build stage, and you can selectively copy artifacts from one stage to another — leaving behind build tools, source code, and dependencies that aren’t needed at runtime.

The result? Dramatically smaller, more secure production images.

Example 1: Node.js Application

Let’s start with a typical Node.js API. Without multi-stage builds, your image includes npm, dev dependencies, TypeScript compiler, and more — none of which are needed in production.

Before (Single Stage — ~1.2 GB)

FROM node:22
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["node", "dist/index.js"]

After (Multi-Stage — ~180 MB)

# Stage 1: Build
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 2: Production
FROM node:22-alpine AS production
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY --from=builder /app/dist ./dist
EXPOSE 3000
USER node
CMD ["node", "dist/index.js"]

Key improvements:

  • Alpine base — slim Linux distribution (~5 MB vs ~900 MB for full Node image)
  • npm ci — deterministic installs from lockfile
  • Production-only deps — dev dependencies stay in the builder stage
  • USER node — runs as non-root for better security

Example 2: Go Application

Go is where multi-stage builds truly shine, because Go compiles to a static binary. Your final image can be almost nothing.

# Stage 1: Build
FROM golang:1.23-alpine 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 /app/server ./cmd/server

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

This produces an image that’s typically 10-15 MB — just the binary and TLS certificates. The scratch base image is literally empty, which also means there’s almost no attack surface for vulnerabilities.

Build Flags Explained

  • CGO_ENABLED=0 — disables C bindings for a fully static binary
  • -ldflags='-s -w' — strips debug symbols, saving ~30% binary size
  • GOOS=linux — ensures Linux binary regardless of build host OS

Example 3: Python with Poetry

Python apps often carry Poetry, pip, compilers, and header files into production. Multi-stage builds fix this elegantly.

# Stage 1: Build dependencies
FROM python:3.13-slim AS builder
WORKDIR /app
RUN pip install poetry==2.1
COPY pyproject.toml poetry.lock ./
RUN poetry config virtualenvs.in-project true \
    && poetry install --only=main --no-root

# Stage 2: Runtime
FROM python:3.13-slim AS runtime
WORKDIR /app
COPY --from=builder /app/.venv ./.venv
COPY src/ ./src/
ENV PATH="/app/.venv/bin:$PATH"
USER 1000
CMD ["python", "-m", "src.main"]

Poetry and pip are gone from the final image. Only the virtual environment with production packages remains.

Advanced Technique: Cache Mounts

Docker BuildKit (enabled by default since Docker 23) supports cache mounts that persist across builds. This dramatically speeds up repeated builds:

# syntax=docker/dockerfile:1
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
COPY . .
RUN npm run build

The npm cache persists between builds, so unchanged packages aren’t re-downloaded. This works for pip, go mod, and apt too:

# Cache apt packages
RUN --mount=type=cache,target=/var/cache/apt apt-get update && apt-get install -y build-essential

# Cache Go modules
RUN --mount=type=cache,target=/go/pkg/mod go mod download

# Cache pip
RUN --mount=type=cache,target=/root/.cache/pip pip install -r requirements.txt

Security Benefits

Smaller images aren’t just about speed — they’re about security:

  • Fewer packages = fewer CVEs — a scratch-based Go image has zero OS-level vulnerabilities
  • No build tools in production — attackers can’t use compilers or package managers if they breach the container
  • Non-root users — always set USER in the final stage
  • No source code — only compiled artifacts reach production

Scan your images with docker scout quickview before and after — you’ll see a dramatic drop in vulnerabilities.

Quick Reference: Size Comparison

Here’s what you can expect from multi-stage builds:

  • Node.js: 1.2 GB → 180 MB (85% reduction)
  • Python: 900 MB → 200 MB (78% reduction)
  • Go: 800 MB → 12 MB (98% reduction)
  • Rust: 1.5 GB → 8 MB (99% reduction)

Common Pitfalls to Avoid

  1. Forgetting CA certificates — if your app makes HTTPS calls from a scratch image, copy certs from the builder
  2. Missing timezone data — copy /usr/share/zoneinfo if your app needs timezone support
  3. Not using .dockerignore — always exclude node_modules, .git, and other junk from the build context
  4. Copying too early — put COPY for source code after dependency installation to leverage layer caching

Wrapping Up

Multi-stage Docker builds are a must-have technique in 2026. They cost nothing to implement, require no infrastructure changes, and deliver immediate improvements in image size, build speed, and security. Start with your largest image, apply the patterns from this guide, and watch your deployment pipeline speed up overnight.

Comments

Leave a Reply

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

Privacy Policy · Contact · Sitemap

© 7Tech – Programming and Tech Tutorials