Back to DevOpsForge Visual Builder
Production Architecture

Django Docker Compose with PostgreSQL & Gunicorn

Updated September 2026 Python 3.10+ / 3.11 Multi-stage Wheel Caching

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.

Customize this Django Stack Visually

Tweak ports, add Redis caching, or toggle S3 cloud backups with zero manual YAML typing.

1. Production `docker-compose.yml`

Sets up healthchecks to ensure Gunicorn starts only after PostgreSQL is ready to receive queries.

docker-compose.yml
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:

2. Optimized Multi-Stage Python `Dockerfile`

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%.

backend/Dockerfile
# --- 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"]