Docker Multi-Stage Builds

February 13, 2026 | Docker Containers Security

Patterns for multiple languages.

Why Multi-Stage Builds Matter

A typical application Docker image built naively can be 1-2 GB — containing compilers, build tools, and source code that aren't needed at runtime. Multi-stage builds let you use one stage for building and another for running, producing images that are 10-50x smaller, faster to pull, and have a dramatically reduced attack surface.

The Problem: Bloated Images

# Bad: Single-stage build (800MB+)
FROM node:18
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["node", "dist/server.js"]

This image includes npm, the full Node.js development environment, all dev dependencies, source TypeScript files, and the node_modules folder — none of which are needed to run the compiled application.

Multi-Stage Solution

# Stage 1: Build
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production && cp -R node_modules prod_modules
RUN npm ci
COPY . .
RUN npm run build

# Stage 2: Production
FROM node:18-alpine AS runner
WORKDIR /app
COPY --from=builder /app/prod_modules ./node_modules
COPY --from=builder /app/dist ./dist
EXPOSE 3000
USER node
CMD ["node", "dist/server.js"]

Result: Image drops from 800MB to ~120MB.

Patterns by Language

.NET

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY *.csproj ./
RUN dotnet restore
COPY . .
RUN dotnet publish -c Release -o /app/publish

FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /app
COPY --from=build /app/publish .
USER 1000
ENTRYPOINT ["dotnet", "MyApp.dll"]

Go

FROM golang:1.21-alpine AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /app/server

FROM scratch
COPY --from=build /app/server /server
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
ENTRYPOINT ["/server"]

Go with scratch base image: final image is just 5-15MB!

Python

FROM python:3.11-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt

FROM python:3.11-slim
WORKDIR /app
COPY --from=builder /root/.local /root/.local
COPY . .
ENV PATH=/root/.local/bin:$PATH
USER 1000
CMD ["gunicorn", "app:app", "-b", "0.0.0.0:8000"]

Security Best Practices

  • Use distroless or alpine base images — Fewer packages means fewer CVEs
  • Run as non-root — Always add USER 1000 or USER node
  • Don't copy secrets — Use BuildKit secrets mounts for build-time credentials
  • .dockerignore — Exclude .git, node_modules, .env, and test files from build context
  • Pin base image digests — Use node:18-alpine@sha256:abc... for reproducible builds

Build Cache Optimization

Order COPY commands from least to most frequently changed:

  1. Dependency manifests (package.json, go.mod, *.csproj)
  2. Dependency installation (npm ci, go mod download, dotnet restore)
  3. Source code (COPY . .)
  4. Build command

This ensures Docker cache is used for dependency installation (the slowest step) unless dependencies actually change.

Image Size Comparison

LanguageSingle-StageMulti-StageReduction
Node.js800MB120MB85%
.NET1.2GB210MB83%
Go400MB10MB97%
Python900MB180MB80%

Eazy SaaS Tip: We include multi-stage Dockerfiles in every project we deliver. Combined with image scanning in CI/CD pipelines, this approach gives our clients secure, minimal images that deploy 5x faster and reduce ECR storage costs by 80%.