mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-24 07:51:08 -05:00
- 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.
630 lines
14 KiB
Markdown
630 lines
14 KiB
Markdown
# 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 <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
|
|
|
|
### 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.
|