Skip to main content
graphwiz.ai
← Back to Posts

Docker Multi-Stage Builds: Optimizing Production Images

DevOpsDocker
dockercontainersoptimizationsecurity

Docker Multi-Stage Builds: Optimizing Production Images

Multi-stage builds are the single most impactful optimization for Docker images. They separate build dependencies from runtime, producing minimal, secure production images.

The Problem

A typical Node.js Dockerfile:

FROM node:18
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
CMD ["npm", "start"]

Result: 1.2 GB image containing:

  • Full Node.js toolchain
  • npm cache
  • devDependencies
  • TypeScript compiler
  • Source maps
  • Build artifacts

None of this is needed at runtime.

Multi-Stage Solution

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

# Stage 2: Production
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
USER node
CMD ["node", "dist/index.js"]

Result: 180 MB image - 85% smaller.

Pattern Library

Go Application

# Build stage
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.* ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o main .

# Runtime stage
FROM scratch
COPY --from=builder /app/main /main
ENTRYPOINT ["/main"]

Result: ~10 MB image

React Application

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

# Runtime stage (nginx)
FROM nginx:alpine
COPY --from=builder /app/build /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Python with Virtual Environment

# Build stage
FROM python:3.11-slim AS builder
WORKDIR /app
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Runtime stage
FROM python:3.11-slim
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY . .
CMD ["python", "app.py"]

Security Benefits

Fewer Vulnerabilities

# Large image
docker scout quickview myapp:v1
# 47 CVEs detected

# Multi-stage image
docker scout quickview myapp:v2
# 3 CVEs detected

No Build Tools

Attackers can't compile exploits if there's no compiler:

# Runtime has no gcc, make, or build tools
FROM alpine:3.18
# Only what's needed to run

Best Practices

Use Specific Base Images

# Bad: floating tag
FROM node:18

# Good: pinned digest
FROM node:18.19.0-alpine3.19@sha256:abc123...

Minimize Layers

# Bad: multiple layers
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get clean

# Good: single layer
RUN apt-get update && \
    apt-get install -y --no-install-recommends curl && \
    rm -rf /var/lib/apt/lists/*

Use .dockerignore

node_modules
npm-debug.log
Dockerfile
.dockerignore
.git
.gitignore
README.md
.env
coverage
.nyc_output

Non-Root User

FROM node:18-alpine
RUN addgroup -g 1001 -S appgroup && \
    adduser -u 1001 -S appuser -G appgroup
USER appuser

Size Comparison

Approach Size Security
Single stage 1.2 GB 47 CVEs
Multi-stage 180 MB 3 CVEs
Multi-stage + distroless 80 MB 0 CVEs

Conclusion

Multi-stage builds should be the default for all production Dockerfiles. They reduce size, improve security, and enforce clean separation between build and runtime environments.