Real-World Examples
Complete, production-ready Dockerfiles for Node.js, Python, Go, React, and more - annotated with the reasoning behind every decision.
Real-World Examples
Theory is useful, but seeing a complete, production-ready Dockerfile - and understanding every decision - is what makes the knowledge stick. Each example below is annotated and ready to adapt.
Node.js API (TypeScript)
A TypeScript API compiled to JavaScript, running in production with only the built output and production dependencies.
# ── Build Stage ───────────────────────────────────────────────
FROM node:20-alpine AS builder
WORKDIR /app
# Copy dependency manifests first (cache layer)
COPY package*.json tsconfig.json ./
# Install ALL deps including devDependencies (needed for tsc)
RUN npm ci
# Copy source and compile
COPY src ./src
RUN npm run build # Outputs to /app/dist
# ── Production Stage ─────────────────────────────────────────
FROM node:20-alpine
WORKDIR /app
# Only production dependencies
COPY package*.json ./
RUN npm ci --omit=dev && \
npm cache clean --force
# Copy compiled output from builder
COPY --from=builder /app/dist ./dist
# OCI metadata
LABEL org.opencontainers.image.source="https://github.com/org/api"
# Non-root user (built into node image)
RUN chown -R node:node /app
USER node
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD wget -qO- http://localhost:3000/health || exit 1
CMD ["node", "dist/server.js"]
What this achieves:
- Build tools (TypeScript compiler, dev deps) stay in the builder stage only
- Final image has: node runtime + production deps + compiled JS only
npm cache cleanremoves the npm cache from the production layer- Non-root
nodeuser for runtime security
Python FastAPI
A Python web API with compiled extension packages and a non-root user.
# ── Build Stage (compiles C extensions) ──────────────────────
FROM python:3.12-slim AS builder
# Install build tools
RUN apt-get update && \
apt-get install -y --no-install-recommends gcc libpq-dev && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Build wheels for all dependencies
COPY requirements.txt .
RUN pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt
# ── Production Stage ─────────────────────────────────────────
FROM python:3.12-slim
# Runtime system library only (no build tools)
RUN apt-get update && \
apt-get install -y --no-install-recommends libpq5 && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Create non-root user
RUN useradd -m -r appuser && chown appuser /app
# Install pre-built wheels (no compiler needed)
COPY --from=builder /wheels /wheels
RUN pip install --no-cache /wheels/* && rm -rf /wheels
# Copy application source
COPY --chown=appuser:appuser . .
USER appuser
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
What this achieves:
gccandlibpq-devonly in builder - not in final image- Pre-built wheels install without a compiler at runtime
libpq5is the runtime-only PostgreSQL client library--workers 4runs multiple uvicorn worker processes
Go (Static Binary)
Go compiles to a single static binary, enabling the smallest possible runtime image.
# ── Build Stage ───────────────────────────────────────────────
FROM golang:1.22-alpine AS builder
# Install git for module fetching (if needed)
RUN apk add --no-cache git ca-certificates tzdata
WORKDIR /app
# Download dependencies first (cached)
COPY go.mod go.sum ./
RUN go mod download
# Build static binary
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -ldflags="-w -s" -o server .
# Verify the binary
RUN ./server --version
# ── Production Stage ─────────────────────────────────────────
FROM scratch
# Copy SSL certs (needed for HTTPS calls)
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
# Copy timezone data
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
WORKDIR /app
COPY --from=builder /app/server .
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD ["/app/server", "healthcheck"]
ENTRYPOINT ["/app/server"]
What this achieves:
CGO_ENABLED=0creates a fully static binary with no libc dependency-ldflags="-w -s"strips debug info - reduces binary size ~30%- Final image:
FROM scratch- just the binary, ~10MB total - CA certificates and timezone data copied from the builder (commonly needed)
React Frontend (Nginx)
Build the React app with Node.js, serve the static output with Nginx.
# ── Build Stage ───────────────────────────────────────────────
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
# Build args for environment-specific config
ARG VITE_API_URL=https://api.example.com
ARG VITE_APP_ENV=production
ENV VITE_API_URL=${VITE_API_URL}
ENV VITE_APP_ENV=${VITE_APP_ENV}
RUN npm run build # Outputs to /app/dist
# ── Production Stage ─────────────────────────────────────────
FROM nginx:1.25-alpine
# Remove default nginx config
RUN rm /etc/nginx/conf.d/default.conf
# Add custom nginx config
COPY nginx.conf /etc/nginx/conf.d/app.conf
# Copy built static files
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD wget -qO- http://localhost/health || exit 1
CMD ["nginx", "-g", "daemon off;"]
A minimal nginx.conf for a React SPA (handles client-side routing):
server {
listen 80;
root /usr/share/nginx/html;
index index.html;
# All routes fall back to index.html (SPA routing)
location / {
try_files $uri $uri/ /index.html;
}
# Health check endpoint
location /health {
return 200 'OK';
add_header Content-Type text/plain;
}
# Cache static assets aggressively
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
What this achieves:
- Build args inject API URL at build time (baked into the JS bundle)
- Final image: Nginx + static files only - no Node.js, no source code
- SPA routing handled by Nginx fallback to
index.html
Django + Gunicorn
FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1
RUN apt-get update && \
apt-get install -y --no-install-recommends libpq5 && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
RUN useradd -m -r django && chown django /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY --chown=django:django . .
# Collect static files at build time
RUN python manage.py collectstatic --noinput
USER django
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health/')"
CMD ["gunicorn", "myproject.wsgi:application", \
"--bind", "0.0.0.0:8000", \
"--workers", "4", \
"--timeout", "120"]
Key env vars for Python:
PYTHONDONTWRITEBYTECODE=1- no.pycfiles (saves space in the image)PYTHONUNBUFFERED=1- stdout/stderr flushed immediately (critical for logs)PIP_NO_CACHE_DIR=1- no pip cache (saves space)
Using These Examples
Each example follows the same pattern:
- Builder stage - everything needed to compile or bundle
- Production stage - the smallest base that can run the output
- Cache layers - manifests before source code
- Non-root user - security hardening
- HEALTHCHECK - observable health status
- Exec form CMD - correct signal handling
Adapt these to your project by:
- Swapping the base image version to your current pinned version
- Adjusting the
CMDto your application's start command - Adding environment-specific
ARGvalues - Customising the
HEALTHCHECKendpoint to match your app's health route