Docker Multi-Stage Builds: Minimal Images for Next.js & FastAPI
How to create production-ready Docker images under 150MB for Next.js (standalone output) and FastAPI. Multi-stage builds, layer caching strategies, non-root users, and health checks.
Why Multi-Stage Builds?
A naive Node.js Docker image includes the full node_modules directory, build tools, TypeScript compiler, and dev dependencies — easily 1–2GB. A multi-stage build discards everything you don't need at runtime.
For Next.js with standalone output: the final image is under 150MB and runs with zero dev dependencies.
Next.js Multi-Stage Dockerfile
# ── Stage 1: Install dependencies ──────────────────────
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci# ── Stage 2: Build ────────────────────────────────────── FROM node:22-alpine AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . ENV NEXT_TELEMETRY_DISABLED=1 RUN npm run build
# ── Stage 3: Production runtime ───────────────────────── FROM node:22-alpine AS runner ENV NODE_ENV=production ENV NEXT_TELEMETRY_DISABLED=1
# Non-root user for security RUN addgroup --system --gid 1001 nodejs && \ 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 CMD ["node", "server.js"] ```
Required in next.config.ts:
``typescript
const nextConfig = { output: 'standalone' }
``
FastAPI Multi-Stage Dockerfile
# ── Stage 1: Build dependencies ─────────────────────────
FROM python:3.12-slim AS builder
RUN pip install uv
WORKDIR /app
COPY requirements.txt .
RUN uv pip install --system -r requirements.txt# ── Stage 2: Runtime ──────────────────────────────────── FROM python:3.12-slim AS runner WORKDIR /app
# Copy only installed packages, not pip or uv COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages COPY --from=builder /usr/local/bin/uvicorn /usr/local/bin/uvicorn COPY ./app ./app
# Non-root user RUN useradd --system --uid 1001 fastapi USER fastapi
EXPOSE 8000 HEALTHCHECK --interval=30s --timeout=5s \ CMD curl -f http://localhost:8000/health || exit 1
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] ```
Layer Caching Strategy
Order your COPY instructions from least-changed to most-changed:
# GOOD — package.json changes rarely; source changes every build
COPY package.json package-lock.json ./
RUN npm ci # Cached unless package.json changes
COPY . . # Always re-runs (source files change)
RUN npm run build# BAD — copies everything first, busting the npm ci cache every build COPY . . RUN npm ci RUN npm run build ```
Docker Compose for Local Development
services:
web:
build: ./frontend
ports: ["3000:3000"]
env_file: .env.local
depends_on: [api, redis]api: build: ./backend ports: ["8000:8000"] env_file: .env.local volumes: - ./backend:/app # Hot reload in dev depends_on: [postgres, redis]
postgres: image: postgres:16-alpine volumes: [postgres_data:/var/lib/postgresql/data] environment: POSTGRES_DB: app POSTGRES_PASSWORD: secret
redis: image: redis:7-alpine
volumes: postgres_data: ```