Running Django in production requires avoiding common Docker anti-patterns: running as root, rebuilding heavy C extensions (like `psycopg2`) on every change, and serving WSGI requests directly on insecure ports.
This architecture pairs Django behind Gunicorn, a secured PostgreSQL database volume, and Caddy for automated SSL, compression, and static asset handling.
Sets up healthchecks to ensure Gunicorn starts only after PostgreSQL is ready to receive queries.
version: '3.8'
services:
caddy:
image: caddy:2-alpine
container_name: django_proxy
restart: always
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
depends_on:
backend:
condition: service_healthy
backend:
build:
context: ./backend
dockerfile: Dockerfile
container_name: django_app
restart: always
expose:
- "8080"
environment:
- DEBUG=0
- DB_ENGINE=django.db.backends.postgresql
- DB_NAME=${DB_NAME:-django_prod}
- DB_USER=${DB_USER:-django_user}
- DB_PASSWORD=${DB_PASSWORD:-django_secret_pass}
- DB_HOST=database
- DB_PORT=5432
deploy:
resources:
limits:
cpus: '1.0'
memory: 1024M
depends_on:
database:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 15s
timeout: 5s
retries: 3
database:
image: postgres:15-alpine
container_name: django_postgres
restart: always
environment:
- POSTGRES_USER=${DB_USER:-django_user}
- POSTGRES_PASSWORD=${DB_PASSWORD:-django_secret_pass}
- POSTGRES_DB=${DB_NAME:-django_prod}
volumes:
- pg_data:/var/lib/postgresql/data
deploy:
resources:
limits:
cpus: '1.0'
memory: 1024M
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-django_user} -d ${DB_NAME:-django_prod}"]
interval: 10s
timeout: 5s
retries: 5
volumes:
caddy_data:
caddy_config:
pg_data:
Uses a multi-stage build that compiles Python wheels in a builder stage and copies clean wheels to the runner, cutting build times and container size by ~60%.
# --- Stage 1: Build Wheels ---
FROM python:3.10-slim AS builder
WORKDIR /app
COPY requirements.txt ./
RUN pip wheel --no-cache-dir --no-deps --wheel-dir /app/wheels -r requirements.txt
# --- Stage 2: Hardened Runtime ---
FROM python:3.10-slim
WORKDIR /app
# Security: Run as dedicated non-root user
RUN groupadd -g 1000 django && useradd -u 1000 -g django django
COPY --from=builder /app/wheels /wheels
COPY --from=builder /app/requirements.txt ./
RUN pip install --no-cache /wheels/* \
&& rm -rf /wheels
COPY . .
RUN chown -R django:django /app
USER django
EXPOSE 8080
CMD ["gunicorn", "wsgi:application", "--bind", "0.0.0.0:8080", "--workers", "3"]