Docker Multi-Stage Builds: Optimize Your Container Images

Master Docker multi-stage builds to create smaller, more secure container images. Learn patterns for Node.js, Python, and Go applications.

Alex Chen
Alex Chen
December 15, 2024 10 min read
Docker Multi-Stage Builds: Optimize Your Container Images

Large Docker images are a common problem in containerized applications. They slow down deployments, consume more storage, and increase security attack surface. Multi-stage builds solve this by separating the build environment from the runtime environment.

What Are Multi-Stage Builds?

Multi-stage builds allow you to 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.

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

# Stage 2: Runtime
FROM node:20-alpine AS runtime
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["node", "dist/index.js"]

Node.js Application Example

Here’s a production-ready multi-stage Dockerfile for a Node.js application:

# ================================
# Stage 1: Dependencies
# ================================
FROM node:20-alpine AS deps
WORKDIR /app

# Install only production dependencies
COPY package*.json ./
RUN npm ci --only=production && \
    npm cache clean --force

# ================================
# Stage 2: Builder
# ================================
FROM node:20-alpine AS builder
WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build && \
    npm run test

# ================================
# Stage 3: Runtime
# ================================
FROM node:20-alpine AS runtime

# Security: Create non-root user
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodejs -u 1001

WORKDIR /app

# Copy only what's needed
COPY --from=deps --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
COPY --from=builder --chown=nodejs:nodejs /app/package.json ./

USER nodejs

ENV NODE_ENV=production
EXPOSE 3000

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1

CMD ["node", "dist/index.js"]

Size Comparison

Image TypeSize
Single-stage (with devDependencies)~850 MB
Multi-stage (production only)~180 MB
Multi-stage (with distroless)~120 MB

Python Application with Virtual Environment

# ================================
# Stage 1: Builder
# ================================
FROM python:3.12-slim AS builder

WORKDIR /app

# Install build dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
    build-essential \
    && rm -rf /var/lib/apt/lists/*

# Create virtual environment
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

# Install Python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# ================================
# Stage 2: Runtime
# ================================
FROM python:3.12-slim AS runtime

# Security: Create non-root user
RUN useradd --create-home --shell /bin/bash appuser

WORKDIR /app

# Copy virtual environment from builder
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

# Copy application code
COPY --chown=appuser:appuser . .

USER appuser

EXPOSE 8000

CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:create_app()"]

Go Application: Smallest Possible Image

Go’s static binaries are perfect for ultra-small images:

# ================================
# Stage 1: Builder
# ================================
FROM golang:1.22-alpine AS builder

WORKDIR /app

# Download dependencies
COPY go.mod go.sum ./
RUN go mod download

# Build static binary
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build \
    -ldflags="-w -s" \
    -o /app/server ./cmd/server

# ================================
# Stage 2: Runtime (scratch)
# ================================
FROM scratch

# Copy CA certificates for HTTPS
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/

# Copy binary
COPY --from=builder /app/server /server

EXPOSE 8080

ENTRYPOINT ["/server"]

Result: A ~10 MB image containing only your binary!

Vue.js/React Static Site

# ================================
# Stage 1: Builder
# ================================
FROM node:20-alpine AS builder

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

# ================================
# Stage 2: Production (nginx)
# ================================
FROM nginx:alpine AS production

# Remove default nginx config
RUN rm /etc/nginx/conf.d/default.conf

# Copy custom nginx config
COPY nginx.conf /etc/nginx/conf.d/

# Copy built assets
COPY --from=builder /app/dist /usr/share/nginx/html

# Security headers and compression
RUN echo 'server_tokens off;' >> /etc/nginx/nginx.conf

EXPOSE 80

HEALTHCHECK --interval=30s --timeout=3s \
  CMD wget --quiet --tries=1 --spider http://localhost/ || exit 1

CMD ["nginx", "-g", "daemon off;"]

Advanced Patterns

Build Arguments for Flexibility

ARG NODE_VERSION=20
ARG ALPINE_VERSION=3.19

FROM node:${NODE_VERSION}-alpine${ALPINE_VERSION} AS builder
# ...

FROM node:${NODE_VERSION}-alpine${ALPINE_VERSION} AS runtime
# ...
# Build with custom versions
docker build --build-arg NODE_VERSION=18 -t myapp .

Caching Dependencies Effectively

FROM node:20-alpine AS deps
WORKDIR /app

# Copy only package files first (better caching)
COPY package.json package-lock.json ./

# This layer is cached unless package*.json changes
RUN npm ci

# Now copy source code (changes more frequently)
COPY . .
RUN npm run build

Conditional Stages for Development

# ================================
# Base stage
# ================================
FROM node:20-alpine AS base
WORKDIR /app
COPY package*.json ./

# ================================
# Development stage
# ================================
FROM base AS development
RUN npm install
COPY . .
CMD ["npm", "run", "dev"]

# ================================
# Production build
# ================================
FROM base AS builder
RUN npm ci
COPY . .
RUN npm run build

# ================================
# Production runtime
# ================================
FROM node:20-alpine AS production
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["node", "dist/index.js"]
# Build specific stage
docker build --target development -t myapp:dev .
docker build --target production -t myapp:prod .

Security Best Practices

1. Use Non-Root Users

RUN addgroup -g 1001 -S appgroup && \
    adduser -S appuser -u 1001 -G appgroup

USER appuser

2. Minimize Attack Surface

# Use distroless for minimal images
FROM gcr.io/distroless/nodejs20-debian12

# Or scratch for static binaries
FROM scratch

3. Scan for Vulnerabilities

# Use Docker Scout or Trivy
docker scout cves myapp:latest
trivy image myapp:latest

Performance Tips

1. Order Layers by Change Frequency

# Least frequently changed β†’ Most frequently changed
COPY package.json ./          # Changes rarely
RUN npm ci                    # Cached until package.json changes
COPY src/ ./src/             # Changes often
COPY public/ ./public/       # Changes sometimes

2. Use .dockerignore

# .dockerignore
node_modules
.git
.gitignore
*.md
.env*
Dockerfile*
docker-compose*
.github
coverage
.nyc_output

3. Parallelize Independent Stages

FROM node:20 AS frontend-builder
COPY frontend/ .
RUN npm run build

FROM golang:1.22 AS backend-builder
COPY backend/ .
RUN go build -o server

FROM alpine:3.19
COPY --from=frontend-builder /app/dist /www
COPY --from=backend-builder /go/server /server

Conclusion

Multi-stage builds are essential for production Docker images:

  • Smaller images: Only include runtime dependencies
  • Faster deployments: Less data to transfer
  • Better security: Reduced attack surface
  • Cleaner Dockerfiles: Logical separation of concerns

Start optimizing your images today and see the difference in your deployment pipeline!


What’s your Docker optimization secret? Share in the comments!

Advertisement

In-Article Ad

Dev Mode

Share this article

Alex Chen

Alex Chen

Senior Full-Stack Developer

I'm a passionate full-stack developer with 10+ years of experience building scalable web applications. I write about Vue.js, Node.js, PostgreSQL, and modern DevOps practices.

Enjoyed this article?

Subscribe to get more tech content delivered to your inbox.

Related Articles