Docker packages an application and all its dependencies — Python version, libraries, system packages — into an immutable image that runs identically on any machine with Docker installed. The developer laptop, the CI server, and the production VPS all run the exact same image. This eliminates an entire category of deployment bugs: dependency version mismatches, missing system libraries, and OS-specific behaviour. For the blog application, we need three images: FastAPI, the React Vite build (served by Nginx), and the PostgreSQL database (official image, no Dockerfile needed).
FastAPI Dockerfile
# Dockerfile.backend — multi-stage build
# Stage 1: build dependencies (creates a clean wheel cache)
# Stage 2: production runtime (only copies what is needed)
# Dockerfile.backend
FROM python:3.12-slim AS builder
WORKDIR /build
COPY requirements.txt .
RUN pip install --upgrade pip \
&& pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt
# ── Production stage ───────────────────────────────────────────────────────────
FROM python:3.12-slim AS production
# Non-root user for security
RUN addgroup --system app && adduser --system --ingroup app app
WORKDIR /app
COPY --from=builder /wheels /wheels
RUN pip install --no-cache-dir --no-index --find-links /wheels /wheels/*.whl \
&& rm -rf /wheels
# Copy application code
COPY --chown=app:app . .
USER app
# Health check endpoint
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/api/health')"
EXPOSE 8000
CMD ["uvicorn", "app.main:app", \
"--host", "0.0.0.0", \
"--port", "8000", \
"--workers", "2", \
"--no-access-log"]
USER app) is a security best practice. If an attacker exploits a vulnerability in the application, they are limited to the permissions of the app user — they cannot install packages, write to system directories, or escalate to root. The default Docker behaviour runs as root, which means a compromised container can potentially escape to the host with root privileges. Always run production containers as a non-root user.COPY . . without a .dockerignore file — you will inadvertently copy .env files with production secrets, .git history, node_modules, test fixtures, and other files that bloat the image or leak sensitive data. Create a .dockerignore that excludes: .env*, .git, __pycache__, *.pyc, .pytest_cache, htmlcov, node_modules, and any other development-only artefacts.React + Nginx Dockerfile
# Dockerfile.frontend — Vite build → Nginx serve
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --prefer-offline
COPY . .
ARG VITE_API_BASE_URL=/api
ENV VITE_API_BASE_URL=$VITE_API_BASE_URL
RUN npm run build # outputs to /app/dist
# ── Nginx production stage ─────────────────────────────────────────────────────
FROM nginx:1.27-alpine AS production
# Copy Vite build output to Nginx's default serve directory
COPY --from=builder /app/dist /usr/share/nginx/html
# Copy custom Nginx config (see next lesson)
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
docker-compose.yml
# docker-compose.yml — production stack
services:
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD", "pg_isready", "-U", "${POSTGRES_USER}"]
interval: 10s
retries: 5
redis:
image: redis:7-alpine
restart: unless-stopped
command: redis-server --maxmemory 128mb --maxmemory-policy allkeys-lru
backend:
build:
context: .
dockerfile: Dockerfile.backend
restart: unless-stopped
depends_on:
db:
condition: service_healthy
env_file: .env.production
environment:
DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db/${POSTGRES_DB}
REDIS_URL: redis://redis:6379
frontend:
build:
context: ./frontend
dockerfile: Dockerfile.frontend
args:
VITE_API_BASE_URL: /api
restart: unless-stopped
depends_on: [backend]
nginx:
image: nginx:1.27-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
- ./certbot/conf:/etc/letsencrypt:ro
- ./certbot/www:/var/www/certbot:ro
depends_on: [frontend, backend]
volumes:
postgres_data:
Common Mistakes
Mistake 1 — No .dockerignore (secrets and bloat in image)
❌ Wrong — COPY . . without .dockerignore copies .env files with real passwords.
✅ Correct — create .dockerignore with at minimum: .env*, .git, node_modules, __pycache__.
Mistake 2 — Running as root in production container
❌ Wrong — default Docker user is root.
✅ Correct — create a non-root user and switch with USER app before CMD.