Tools: Complete Guide to Docker Multi-Stage Builds: Slash Your Image Size by 90%

Tools: Complete Guide to Docker Multi-Stage Builds: Slash Your Image Size by 90%

The Problem with Naive Docker Images

Multi-Stage Builds

How It Works

Real-World Examples

Next.js App

Go Binary

Python FastAPI

Layer Caching Optimization

Build Arguments for Dynamic Stages

Size Comparison This image weighs in around 1.2GB. It includes: Your production server needs exactly none of those except the compiled output and production dependencies. Result: ~120MB instead of 1.2GB. 90% reduction. Each FROM instruction creates a new stage. The final image only contains files explicitly copied from previous stages via COPY --from=<stage>. Docker throws away intermediate stages—they exist only to produce artifacts. FROM scratch = zero base image. Final image is just your binary. Typically 10-20MB. Docker caches each layer. If the input files haven't changed, it reuses the cached layer. Put slow, infrequent operations first. Typical results after multi-stage: Smaller images = faster pulls = faster deploys = lower storage costs. Building production microservices? The Whoff Agents AI SaaS Starter Kit includes optimized Dockerfiles for Node.js and Next.js apps. Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Command

Copy

# Don't do this FROM node:20 WORKDIR /app COPY . . RUN -weight: 500;">npm -weight: 500;">install RUN -weight: 500;">npm run build CMD ["node", "dist/server.js"] # Don't do this FROM node:20 WORKDIR /app COPY . . RUN -weight: 500;">npm -weight: 500;">install RUN -weight: 500;">npm run build CMD ["node", "dist/server.js"] # Don't do this FROM node:20 WORKDIR /app COPY . . RUN -weight: 500;">npm -weight: 500;">install RUN -weight: 500;">npm run build CMD ["node", "dist/server.js"] # Stage 1: Dependencies FROM node:20-alpine AS deps WORKDIR /app COPY package*.json ./ RUN -weight: 500;">npm ci --only=production # Stage 2: Builder FROM node:20-alpine AS builder WORKDIR /app COPY package*.json ./ RUN -weight: 500;">npm ci COPY . . RUN -weight: 500;">npm run build # Stage 3: Runner (final image) FROM node:20-alpine AS runner WORKDIR /app # Non-root user for security RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs # Copy only what's needed COPY --from=deps /app/node_modules ./node_modules COPY --from=builder /app/dist ./dist COPY --from=builder /app/package.json ./package.json USER nextjs EXPOSE 3000 CMD ["node", "dist/server.js"] # Stage 1: Dependencies FROM node:20-alpine AS deps WORKDIR /app COPY package*.json ./ RUN -weight: 500;">npm ci --only=production # Stage 2: Builder FROM node:20-alpine AS builder WORKDIR /app COPY package*.json ./ RUN -weight: 500;">npm ci COPY . . RUN -weight: 500;">npm run build # Stage 3: Runner (final image) FROM node:20-alpine AS runner WORKDIR /app # Non-root user for security RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs # Copy only what's needed COPY --from=deps /app/node_modules ./node_modules COPY --from=builder /app/dist ./dist COPY --from=builder /app/package.json ./package.json USER nextjs EXPOSE 3000 CMD ["node", "dist/server.js"] # Stage 1: Dependencies FROM node:20-alpine AS deps WORKDIR /app COPY package*.json ./ RUN -weight: 500;">npm ci --only=production # Stage 2: Builder FROM node:20-alpine AS builder WORKDIR /app COPY package*.json ./ RUN -weight: 500;">npm ci COPY . . RUN -weight: 500;">npm run build # Stage 3: Runner (final image) FROM node:20-alpine AS runner WORKDIR /app # Non-root user for security RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs # Copy only what's needed COPY --from=deps /app/node_modules ./node_modules COPY --from=builder /app/dist ./dist COPY --from=builder /app/package.json ./package.json USER nextjs EXPOSE 3000 CMD ["node", "dist/server.js"] FROM node:20-alpine AS deps WORKDIR /app COPY package*.json ./ RUN -weight: 500;">npm ci FROM node:20-alpine AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . RUN -weight: 500;">npm run build FROM node:20-alpine AS runner WORKDIR /app ENV NODE_ENV=production RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs COPY --from=builder /app/public ./public COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static USER nextjs EXPOSE 3000 ENV PORT=3000 CMD ["node", "server.js"] FROM node:20-alpine AS deps WORKDIR /app COPY package*.json ./ RUN -weight: 500;">npm ci FROM node:20-alpine AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . RUN -weight: 500;">npm run build FROM node:20-alpine AS runner WORKDIR /app ENV NODE_ENV=production RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs COPY --from=builder /app/public ./public COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static USER nextjs EXPOSE 3000 ENV PORT=3000 CMD ["node", "server.js"] FROM node:20-alpine AS deps WORKDIR /app COPY package*.json ./ RUN -weight: 500;">npm ci FROM node:20-alpine AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . RUN -weight: 500;">npm run build FROM node:20-alpine AS runner WORKDIR /app ENV NODE_ENV=production RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs COPY --from=builder /app/public ./public COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static USER nextjs EXPOSE 3000 ENV PORT=3000 CMD ["node", "server.js"] # Builder: full Go toolchain FROM golang:1.22 AS builder WORKDIR /app COPY go.* ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 GOOS=linux go build -o server ./cmd/server # Runner: nearly nothing FROM scratch AS runner COPY --from=builder /app/server /server COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ EXPOSE 8080 CMD ["/server"] # Builder: full Go toolchain FROM golang:1.22 AS builder WORKDIR /app COPY go.* ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 GOOS=linux go build -o server ./cmd/server # Runner: nearly nothing FROM scratch AS runner COPY --from=builder /app/server /server COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ EXPOSE 8080 CMD ["/server"] # Builder: full Go toolchain FROM golang:1.22 AS builder WORKDIR /app COPY go.* ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 GOOS=linux go build -o server ./cmd/server # Runner: nearly nothing FROM scratch AS runner COPY --from=builder /app/server /server COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ EXPOSE 8080 CMD ["/server"] FROM python:3.12-slim AS builder WORKDIR /app RUN -weight: 500;">pip -weight: 500;">install poetry COPY pyproject.toml poetry.lock ./ RUN poetry export -f requirements.txt --output requirements.txt --without-hashes FROM python:3.12-slim AS runner WORKDIR /app COPY --from=builder /app/requirements.txt . RUN -weight: 500;">pip -weight: 500;">install --no-cache-dir -r requirements.txt COPY src/ ./src/ EXPOSE 8000 CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"] FROM python:3.12-slim AS builder WORKDIR /app RUN -weight: 500;">pip -weight: 500;">install poetry COPY pyproject.toml poetry.lock ./ RUN poetry export -f requirements.txt --output requirements.txt --without-hashes FROM python:3.12-slim AS runner WORKDIR /app COPY --from=builder /app/requirements.txt . RUN -weight: 500;">pip -weight: 500;">install --no-cache-dir -r requirements.txt COPY src/ ./src/ EXPOSE 8000 CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"] FROM python:3.12-slim AS builder WORKDIR /app RUN -weight: 500;">pip -weight: 500;">install poetry COPY pyproject.toml poetry.lock ./ RUN poetry export -f requirements.txt --output requirements.txt --without-hashes FROM python:3.12-slim AS runner WORKDIR /app COPY --from=builder /app/requirements.txt . RUN -weight: 500;">pip -weight: 500;">install --no-cache-dir -r requirements.txt COPY src/ ./src/ EXPOSE 8000 CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"] # Bad: invalidates cache on ANY file change COPY . . RUN -weight: 500;">npm -weight: 500;">install # Good: -weight: 500;">npm -weight: 500;">install only re-runs when package.json changes COPY package*.json ./ RUN -weight: 500;">npm -weight: 500;">install COPY . . # Bad: invalidates cache on ANY file change COPY . . RUN -weight: 500;">npm -weight: 500;">install # Good: -weight: 500;">npm -weight: 500;">install only re-runs when package.json changes COPY package*.json ./ RUN -weight: 500;">npm -weight: 500;">install COPY . . # Bad: invalidates cache on ANY file change COPY . . RUN -weight: 500;">npm -weight: 500;">install # Good: -weight: 500;">npm -weight: 500;">install only re-runs when package.json changes COPY package*.json ./ RUN -weight: 500;">npm -weight: 500;">install COPY . . FROM node:20-alpine AS base FROM base AS development RUN -weight: 500;">npm -weight: 500;">install CMD ["-weight: 500;">npm", "run", "dev"] FROM base AS production RUN -weight: 500;">npm ci --only=production && -weight: 500;">npm run build CMD ["node", "dist/server.js"] FROM node:20-alpine AS base FROM base AS development RUN -weight: 500;">npm -weight: 500;">install CMD ["-weight: 500;">npm", "run", "dev"] FROM base AS production RUN -weight: 500;">npm ci --only=production && -weight: 500;">npm run build CMD ["node", "dist/server.js"] FROM node:20-alpine AS base FROM base AS development RUN -weight: 500;">npm -weight: 500;">install CMD ["-weight: 500;">npm", "run", "dev"] FROM base AS production RUN -weight: 500;">npm ci --only=production && -weight: 500;">npm run build CMD ["node", "dist/server.js"] # Build specific target -weight: 500;">docker build --target development -t myapp:dev . -weight: 500;">docker build --target production -t myapp:prod . # Build specific target -weight: 500;">docker build --target development -t myapp:dev . -weight: 500;">docker build --target production -t myapp:prod . # Build specific target -weight: 500;">docker build --target development -t myapp:dev . -weight: 500;">docker build --target production -t myapp:prod . # Check your image sizes -weight: 500;">docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}" # Dive tool for layer analysis -weight: 500;">docker run --rm -it -v /var/run/-weight: 500;">docker.sock:/var/run/-weight: 500;">docker.sock \ wagoodman/dive:latest myapp:prod # Check your image sizes -weight: 500;">docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}" # Dive tool for layer analysis -weight: 500;">docker run --rm -it -v /var/run/-weight: 500;">docker.sock:/var/run/-weight: 500;">docker.sock \ wagoodman/dive:latest myapp:prod # Check your image sizes -weight: 500;">docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}" # Dive tool for layer analysis -weight: 500;">docker run --rm -it -v /var/run/-weight: 500;">docker.sock:/var/run/-weight: 500;">docker.sock \ wagoodman/dive:latest myapp:prod - Full Node.js runtime + dev tools - All devDependencies (TypeScript, ESLint, Jest, etc.) - Source files - Build artifacts - Node.js app: 1.2GB → 150MB - Go -weight: 500;">service: 800MB → 15MB - Python app: 900MB → 200MB