Dilip Singh logo
All posts
InfrastructureIntermediate2025-05-05·7 min read

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

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

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:

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

yaml
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: ```

DS
Dilip Singh
Lead Software Architect · Hureka Technologies

14+ years building enterprise software and AI systems. Architecting multi-agent AI platforms, RAG pipelines, voice AI, and high-performance SaaS for global clients.