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 clean removes the npm cache from the production layer
  • Non-root node user 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:

  • gcc and libpq-dev only in builder - not in final image
  • Pre-built wheels install without a compiler at runtime
  • libpq5 is the runtime-only PostgreSQL client library
  • --workers 4 runs 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=0 creates 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 .pyc files (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:

  1. Builder stage - everything needed to compile or bundle
  2. Production stage - the smallest base that can run the output
  3. Cache layers - manifests before source code
  4. Non-root user - security hardening
  5. HEALTHCHECK - observable health status
  6. Exec form CMD - correct signal handling

Adapt these to your project by:

  • Swapping the base image version to your current pinned version
  • Adjusting the CMD to your application's start command
  • Adding environment-specific ARG values
  • Customising the HEALTHCHECK endpoint to match your app's health route

Continue Learning

Explore Related Topics

Try the Tool

Related Resources