Files
thrillwiki_django_no_react/architecture/deployment-guide.md
pacnpal edcd8f2076 Add secret management guide, client-side performance monitoring, and search accessibility enhancements
- Introduced a comprehensive Secret Management Guide detailing best practices, secret classification, development setup, production management, rotation procedures, and emergency protocols.
- Implemented a client-side performance monitoring script to track various metrics including page load performance, paint metrics, layout shifts, and memory usage.
- Enhanced search accessibility with keyboard navigation support for search results, ensuring compliance with WCAG standards and improving user experience.
2025-12-23 16:41:42 -05:00

14 KiB

ThrillWiki Deployment Guide

This document outlines deployment strategies, build processes, and infrastructure considerations for the ThrillWiki Django + HTMX application.

Architecture Overview

ThrillWiki is a Django monolith with HTMX for dynamic interactivity. There is no separate frontend build process - templates and static assets are served directly by Django.

graph TB
    A[Source Code] --> B[Django Application]
    B --> C[Static Files Collection]
    C --> D[Docker Container]
    D --> E[Production Deployment]

    subgraph "Django Application"
        B1[Python Dependencies]
        B2[Database Migrations]
        B3[HTMX Templates]
    end

Development Environment

Prerequisites

  • Python 3.13+ with UV package manager
  • PostgreSQL 14+ with PostGIS extension
  • Redis 6+ (for caching and sessions)

Local Development Setup

# Clone repository
git clone <repository-url>
cd thrillwiki

# Install dependencies
cd backend
uv sync --frozen

# Configure environment
cp .env.example .env
# Edit .env with your settings

# Database setup
uv run manage.py migrate
uv run manage.py collectstatic --noinput

# Start development server
uv run manage.py runserver

Build Strategies

Multi-stage Dockerfile

# backend/Dockerfile
FROM python:3.13-slim as builder

WORKDIR /app

# Install system dependencies for GeoDjango
RUN apt-get update && apt-get install -y \
    binutils libproj-dev gdal-bin libgdal-dev \
    libpq-dev gcc \
    && rm -rf /var/lib/apt/lists/*

# Install UV
RUN pip install uv

# Copy dependency files
COPY pyproject.toml uv.lock ./

# Install dependencies
RUN uv sync --frozen --no-dev

FROM python:3.13-slim as runtime

WORKDIR /app

# Install runtime dependencies for GeoDjango
RUN apt-get update && apt-get install -y \
    libpq5 gdal-bin libgdal32 libgeos-c1v5 libproj25 \
    && rm -rf /var/lib/apt/lists/*

# Copy virtual environment from builder
COPY --from=builder /app/.venv /app/.venv
ENV PATH="/app/.venv/bin:$PATH"

# Copy application code
COPY . .

# Collect static files
RUN python manage.py collectstatic --noinput

# Create logs directory
RUN mkdir -p logs

EXPOSE 8000

# Run with gunicorn
CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "4"]

Docker Compose for Development

# docker-compose.dev.yml
version: '3.8'

services:
  db:
    image: postgis/postgis:15-3.3
    environment:
      POSTGRES_DB: thrillwiki
      POSTGRES_USER: thrillwiki
      POSTGRES_PASSWORD: password
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

  web:
    build:
      context: ./backend
      dockerfile: Dockerfile.dev
    ports:
      - "8000:8000"
    volumes:
      - ./backend:/app
      - ./shared/media:/app/media
    environment:
      - DEBUG=1
      - DATABASE_URL=postgis://thrillwiki:password@db:5432/thrillwiki
      - REDIS_URL=redis://redis:6379/0
    depends_on:
      - db
      - redis
    command: python manage.py runserver 0.0.0.0:8000

  celery:
    build:
      context: ./backend
      dockerfile: Dockerfile.dev
    volumes:
      - ./backend:/app
    environment:
      - DATABASE_URL=postgis://thrillwiki:password@db:5432/thrillwiki
      - REDIS_URL=redis://redis:6379/0
    depends_on:
      - db
      - redis
    command: celery -A config.celery worker -l info

volumes:
  postgres_data:

Docker Compose for Production

# docker-compose.prod.yml
version: '3.8'

services:
  db:
    image: postgis/postgis:15-3.3
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    restart: unless-stopped

  redis:
    image: redis:7-alpine
    restart: unless-stopped

  web:
    build:
      context: ./backend
      dockerfile: Dockerfile
    environment:
      - DEBUG=0
      - DATABASE_URL=${DATABASE_URL}
      - REDIS_URL=${REDIS_URL}
      - SECRET_KEY=${SECRET_KEY}
      - ALLOWED_HOSTS=${ALLOWED_HOSTS}
    volumes:
      - ./shared/media:/app/media
      - static_files:/app/staticfiles
    depends_on:
      - db
      - redis
    restart: unless-stopped

  celery:
    build:
      context: ./backend
      dockerfile: Dockerfile
    environment:
      - DATABASE_URL=${DATABASE_URL}
      - REDIS_URL=${REDIS_URL}
      - SECRET_KEY=${SECRET_KEY}
    depends_on:
      - db
      - redis
    command: celery -A config.celery worker -l info
    restart: unless-stopped

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf
      - ./nginx/ssl:/etc/nginx/ssl
      - static_files:/usr/share/nginx/html/static
      - ./shared/media:/usr/share/nginx/html/media
    depends_on:
      - web
    restart: unless-stopped

volumes:
  postgres_data:
  static_files:

Nginx Configuration

# nginx/nginx.conf
upstream django {
    server web:8000;
}

server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name yourdomain.com www.yourdomain.com;

    ssl_certificate /etc/nginx/ssl/fullchain.pem;
    ssl_certificate_key /etc/nginx/ssl/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
    ssl_prefer_server_ciphers off;

    # Security headers
    add_header X-Frame-Options "DENY" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    # Static files
    location /static/ {
        alias /usr/share/nginx/html/static/;
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    # Media files
    location /media/ {
        alias /usr/share/nginx/html/media/;
        expires 1M;
        add_header Cache-Control "public";
    }

    # Django application
    location / {
        proxy_pass http://django;
        proxy_set_header Host $http_host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # HTMX considerations
        proxy_set_header HX-Request $http_hx_request;
        proxy_set_header HX-Current-URL $http_hx_current_url;
    }

    # Health check endpoint
    location /api/v1/health/simple/ {
        proxy_pass http://django;
        proxy_set_header Host $http_host;
        access_log off;
    }
}

CI/CD Pipeline

GitHub Actions Workflow

# .github/workflows/deploy.yml
name: Deploy ThrillWiki

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest

    services:
      postgres:
        image: postgis/postgis:15-3.3
        env:
          POSTGRES_PASSWORD: postgres
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 5432:5432

      redis:
        image: redis:7-alpine
        ports:
          - 6379:6379

    steps:
    - uses: actions/checkout@v4

    - name: Set up Python
      uses: actions/setup-python@v5
      with:
        python-version: '3.13'

    - name: Install UV
      run: pip install uv

    - name: Cache dependencies
      uses: actions/cache@v4
      with:
        path: ~/.cache/uv
        key: ${{ runner.os }}-uv-${{ hashFiles('backend/uv.lock') }}

    - name: Install dependencies
      run: |
        cd backend
        uv sync --frozen

    - name: Run tests
      run: |
        cd backend
        uv run manage.py test
      env:
        DATABASE_URL: postgis://postgres:postgres@localhost:5432/postgres
        REDIS_URL: redis://localhost:6379/0
        SECRET_KEY: test-secret-key
        DEBUG: "1"

    - name: Run linting
      run: |
        cd backend
        uv run ruff check .
        uv run black --check .

  build:
    needs: test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'

    steps:
    - uses: actions/checkout@v4

    - name: Build Docker image
      run: |
        docker build -t thrillwiki-web ./backend

    - name: Push to registry
      run: |
        # Push to your container registry
        # docker push your-registry/thrillwiki-web:${{ github.sha }}

  deploy:
    needs: build
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'

    steps:
    - name: Deploy to production
      run: |
        # Deploy using your preferred method
        # SSH, Kubernetes, AWS ECS, etc.

Environment Configuration

Required Environment Variables

# Django Settings
DEBUG=0
SECRET_KEY=your-production-secret-key
ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com
CSRF_TRUSTED_ORIGINS=https://yourdomain.com,https://www.yourdomain.com
DJANGO_SETTINGS_MODULE=config.django.production

# Database
DATABASE_URL=postgis://user:password@host:port/database

# Redis
REDIS_URL=redis://host:port/0

# Email
EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend
EMAIL_HOST=smtp.yourmailprovider.com
EMAIL_PORT=587
EMAIL_USE_TLS=True
EMAIL_HOST_USER=your-email@yourdomain.com
EMAIL_HOST_PASSWORD=your-email-password

# Cloudflare Images
CLOUDFLARE_IMAGES_ACCOUNT_ID=your-account-id
CLOUDFLARE_IMAGES_API_TOKEN=your-api-token
CLOUDFLARE_IMAGES_ACCOUNT_HASH=your-account-hash

# Sentry (optional)
SENTRY_DSN=your-sentry-dsn
SENTRY_ENVIRONMENT=production

Performance Optimization

Database Optimization

# backend/config/django/production.py
DATABASES = {
    'default': {
        'ENGINE': 'django.contrib.gis.db.backends.postgis',
        'CONN_MAX_AGE': 60,  # Keep connections alive for 60 seconds
        'OPTIONS': {
            'connect_timeout': 10,
            'options': '-c statement_timeout=30000',  # 30 second query timeout
        }
    }
}

Redis Caching

# Caching configuration is in config/django/production.py
# Multiple cache backends for different purposes:
# - default: General caching
# - sessions: Session storage
# - api: API response caching

Static Files with WhiteNoise

# backend/config/django/production.py
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"

Monitoring and Logging

Health Check Endpoints

Endpoint Purpose Use Case
/api/v1/health/ Comprehensive health check Monitoring dashboards
/api/v1/health/simple/ Simple OK/ERROR Load balancer health checks
/api/v1/health/performance/ Performance metrics Debug mode only

Logging Configuration

Production logging uses JSON format for log aggregation:

# backend/config/django/production.py
LOGGING = {
    'handlers': {
        'console': {
            'class': 'logging.StreamHandler',
            'formatter': 'json',
        },
        'file': {
            'class': 'logging.handlers.RotatingFileHandler',
            'filename': 'logs/django.log',
            'maxBytes': 1024 * 1024 * 15,  # 15MB
            'backupCount': 10,
            'formatter': 'json',
        },
    },
}

Sentry Integration

# Sentry is configured in config/django/production.py
# Enable by setting SENTRY_DSN environment variable

Security Considerations

Production Security Checklist

  • DEBUG=False in production
  • SECRET_KEY is unique and secure
  • ALLOWED_HOSTS properly configured
  • HTTPS enforced with SSL certificates
  • Security headers configured (HSTS, CSP, etc.)
  • Database credentials secured
  • Redis password configured (if exposed)
  • CORS properly configured
  • Rate limiting enabled
  • File upload validation
  • SQL injection protection (Django ORM)
  • XSS protection enabled
  • CSRF protection active

Security Headers

# backend/config/django/production.py
SECURE_SSL_REDIRECT = True
SECURE_HSTS_SECONDS = 31536000  # 1 year
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
X_FRAME_OPTIONS = 'DENY'
SECURE_CONTENT_TYPE_NOSNIFF = True

Backup and Recovery

Database Backup Strategy

#!/bin/bash
# Automated backup script
pg_dump $DATABASE_URL | gzip > backup_$(date +%Y%m%d_%H%M%S).sql.gz
aws s3 cp backup_*.sql.gz s3://your-backup-bucket/database/

Media Files Backup

# Sync media files to S3
aws s3 sync ./shared/media/ s3://your-media-bucket/media/ --delete

Scaling Strategies

Horizontal Scaling

  • Use load balancer (nginx, AWS ALB, etc.)
  • Database read replicas for read-heavy workloads
  • CDN for static assets (Cloudflare, CloudFront)
  • Redis cluster for session/cache scaling
  • Multiple Gunicorn workers per container

Vertical Scaling

  • Database connection pooling (pgBouncer)
  • Query optimization with select_related/prefetch_related
  • Memory usage optimization
  • Background task offloading to Celery

Troubleshooting Guide

Common Issues

  1. Static files not loading

    • Run python manage.py collectstatic
    • Check nginx static file configuration
    • Verify WhiteNoise settings
  2. Database connection errors

    • Verify DATABASE_URL format
    • Check firewall rules
    • Verify PostGIS extension is installed
  3. CORS errors

    • Check CORS_ALLOWED_ORIGINS setting
    • Verify CSRF_TRUSTED_ORIGINS
  4. Memory issues

    • Monitor with docker stats
    • Optimize Gunicorn worker count
    • Check for query inefficiencies

Debug Commands

# Check Django configuration
cd backend
uv run manage.py check --deploy

# Database shell
uv run manage.py dbshell

# Django shell
uv run manage.py shell

# Validate settings
uv run manage.py validate_settings

This deployment guide provides a comprehensive approach to deploying the ThrillWiki Django + HTMX application while maintaining security, performance, and scalability.