# 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. ```mermaid 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 ```bash # Clone repository git clone 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 ### 1. Containerized Deployment (Recommended) #### Multi-stage Dockerfile ```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 ```yaml # 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 ```yaml # 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/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 ```yaml # .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 ```bash # 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 ```python # 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 ```python # 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 ```python # 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: ```python # 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 ```python # 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 ```python # 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 ```bash #!/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 ```bash # 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 ```bash # 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.