TechLead
Lesson 8 of 18
5 min read
Docker & DevOps

Multi-Stage Builds

Optimize Docker images with multi-stage builds to reduce image size and improve security

What Are Multi-Stage Builds?

Multi-stage builds let you use multiple FROM statements in a single Dockerfile. Each FROM creates a new build stage, and you can selectively copy artifacts from one stage to another. This dramatically reduces final image size by excluding build tools and dependencies.

The Problem: Large Images

# ❌ Single-stage build β€” includes build tools in production
FROM node:20

WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Final image includes: source code, devDependencies, build tools
# Size: ~1.2 GB 😱
CMD ["npm", "start"]

The Solution: Multi-Stage Build

# βœ… Multi-stage build β€” clean production image
# Stage 1: Build
FROM node:20-alpine AS builder

WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 2: Production
FROM node:20-alpine AS production

WORKDIR /app

# Copy only what we need from the builder stage
COPY --from=builder /app/package*.json ./
RUN npm ci --production
COPY --from=builder /app/dist ./dist

ENV NODE_ENV=production
EXPOSE 3000
CMD ["node", "dist/server.js"]

# Final image: no source code, no devDeps, no build tools
# Size: ~150 MB βœ…

React/Next.js Production Build

# Stage 1: Install dependencies
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci

# Stage 2: Build the application
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

# Stage 3: Production image
FROM node:20-alpine AS runner
WORKDIR /app

ENV NODE_ENV=production

# Create a non-root user
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

# Copy built assets
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static

USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]

Go Application β€” Extreme Size Reduction

# Stage 1: Build
FROM golang:1.22-alpine AS builder

WORKDIR /app
COPY go.* ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o server .

# Stage 2: Minimal production 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"]

# Final image: ~10 MB! (vs ~300 MB with golang base)

Python Application

# Stage 1: Build dependencies
FROM python:3.12-slim AS builder

WORKDIR /app
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt

# Stage 2: Production
FROM python:3.12-slim

WORKDIR /app

# Copy installed packages from builder
COPY --from=builder /root/.local /root/.local
COPY . .

ENV PATH=/root/.local/bin:$PATH
EXPOSE 8000
CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:8000"]

Building Specific Stages

# Build only up to a specific stage
docker build --target builder -t myapp:build .

# Build the production stage (default: last stage)
docker build -t myapp:prod .

# Use in CI/CD to run tests in the build stage
docker build --target test -t myapp:test .

Size Comparison

ApproachTypical Size
Single-stage Node.js~1.2 GB
Multi-stage Node.js (Alpine)~150 MB
Single-stage Go~300 MB
Multi-stage Go (scratch)~10 MB

Continue Learning