The Problem with Naive Docker Images
# Don't do this
FROM node:20
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
CMD ["node", "dist/server.js"]
This image weighs in around 1.2GB. It includes:
- Full Node.js runtime + dev tools
- All devDependencies (TypeScript, ESLint, Jest, etc.)
- Source files
- Build artifacts
- npm cache
Your production server needs exactly none of those except the compiled output and production dependencies.
Multi-Stage Builds
# Stage 1: Dependencies
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
# Stage 2: Builder
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN 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"]
Result: ~120MB instead of 1.2GB. 90% reduction.
How It Works
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.
Real-World Examples
Next.js App
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN 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"]
Go Binary
# 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 scratch = zero base image. Final image is just your binary. Typically 10-20MB.
Python FastAPI
FROM python:3.12-slim AS builder
WORKDIR /app
RUN pip 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 pip install --no-cache-dir -r requirements.txt
COPY src/ ./src/
EXPOSE 8000
CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]
Layer Caching Optimization
# Bad: invalidates cache on ANY file change
COPY . .
RUN npm install
# Good: npm install only re-runs when package.json changes
COPY package*.json ./
RUN npm install
COPY . .
Docker caches each layer. If the input files haven't changed, it reuses the cached layer. Put slow, infrequent operations first.
Build Arguments for Dynamic Stages
FROM node:20-alpine AS base
FROM base AS development
RUN npm install
CMD ["npm", "run", "dev"]
FROM base AS production
RUN npm ci --only=production && npm run build
CMD ["node", "dist/server.js"]
# Build specific target
docker build --target development -t myapp:dev .
docker build --target production -t myapp:prod .
Size Comparison
# Check your image sizes
docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}"
# Dive tool for layer analysis
docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock \
wagoodman/dive:latest myapp:prod
Typical results after multi-stage:
- Node.js app: 1.2GB → 150MB
- Go service: 800MB → 15MB
- Python app: 900MB → 200MB
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.
Top comments (0)