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
# 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