diff --git a/DEPLOYMENT_GUIDE.md b/DEPLOYMENT_GUIDE.md new file mode 100644 index 0000000..8ae9500 --- /dev/null +++ b/DEPLOYMENT_GUIDE.md @@ -0,0 +1,584 @@ +# MoreThanADiagnosis - Production Deployment Guide + +**Domain**: mtd.runfoo.run +**Server**: nexus-vector (100.95.3.92) +**Job ID**: MTAD-IMPL-2025-11-18-CL +**Date**: 2025-11-18 + +--- + +## 🚀 Pre-Deployment Checklist + +- [ ] SSH access to nexus-vector configured +- [ ] Domain mtd.runfoo.run points to nexus-vector (100.95.3.92) +- [ ] All secrets prepared in `.env` file +- [ ] Docker and Docker Compose installed on nexus-vector +- [ ] At least 20GB free disk space +- [ ] Ports 80, 443, 8000, 8080 available +- [ ] Database backups tested and automated +- [ ] Monitoring and alerting configured + +--- + +## 🔐 Secrets & Environment Variables + +Create `.env` file in `/srv/containers/mtad-api/`: + +```bash +# Database +DB_USER=admin +DB_PASSWORD= + +# Redis +REDIS_PASSWORD= + +# Security +SECRET_KEY= +ALGORITHM=HS256 + +# CORS +CORS_ORIGINS=["https://mtd.runfoo.run"] + +# Email (optional for future) +SMTP_PASSWORD= + +# AWS S3 (optional for file uploads) +S3_ACCESS_KEY= +S3_SECRET_KEY= +``` + +**Generate secure passwords**: +```bash +# Password (32 chars) +openssl rand -base64 24 + +# Secret key (64 hex chars) +openssl rand -hex 32 +``` + +--- + +## 📋 Step-by-Step Deployment + +### 1. Prepare Server + +```bash +# SSH to nexus-vector +ssh admin@nexus-vector + +# Create deployment directory +sudo mkdir -p /srv/containers/mtad-api +cd /srv/containers/mtad-api + +# Clone repository +git clone https://github.com/fullsizemalt/morethanadiagnosis-hub.git . +cd backend + +# Copy production docker-compose +cp docker-compose.prod.yml docker-compose.yml +``` + +### 2. Create SSL Certificates (Let's Encrypt) + +```bash +# Install certbot +sudo apt-get update +sudo apt-get install -y certbot python3-certbot-nginx + +# Create certificate (interactive) +sudo certbot certonly --standalone \ + -d mtd.runfoo.run \ + -d www.mtd.runfoo.run \ + --agree-tos \ + --email admin@morethanadiagnosis.com + +# Copy to nginx directory +sudo mkdir -p /srv/containers/mtad-api/certbot/conf +sudo cp -r /etc/letsencrypt /srv/containers/mtad-api/certbot/conf/ + +# Fix permissions +sudo chown -R admin:admin /srv/containers/mtad-api/certbot/ +``` + +### 3. Configure Environment + +```bash +# Copy .env template +cp backend/.env.example backend/.env + +# Edit with production values +nano backend/.env + +# Required values: +# DB_PASSWORD=... +# REDIS_PASSWORD=... +# SECRET_KEY=... +``` + +### 4. Build and Start Services + +```bash +# Build Docker image +docker-compose build + +# Start all services +docker-compose up -d + +# Wait for startup +sleep 10 + +# Check status +docker-compose ps + +# Expected output: +# NAME STATUS +# mtad-postgres Up (healthy) +# mtad-redis Up (healthy) +# mtad-api Up (healthy) +# mtad-nginx Up +``` + +### 5. Run Database Migrations + +```bash +# Enter API container +docker-compose exec api bash + +# Run migrations +cd /app +alembic upgrade head + +# Exit container +exit +``` + +### 6. Verify Deployment + +```bash +# Health check +curl https://mtd.runfoo.run/health + +# Expected response: +# {"status":"healthy","version":"v1","env":"production"} + +# API check +curl https://mtd.runfoo.run/api/v1/health + +# Expected response: +# {"status":"healthy","service":"MoreThanADiagnosis API"} + +# API docs +curl https://mtd.runfoo.run/docs +``` + +### 7. Set Up Automatic Backups + +```bash +# Create backup script +cat > /srv/containers/mtad-api/backup.sh << 'EOF' +#!/bin/bash +BACKUP_DIR="/srv/containers/mtad-api/backups" +TIMESTAMP=$(date +%Y%m%d_%H%M%S) + +mkdir -p $BACKUP_DIR + +# Backup database +docker-compose exec -T postgres pg_dump -U admin morethanadiagnosis \ + | gzip > $BACKUP_DIR/db_$TIMESTAMP.sql.gz + +# Keep only last 30 days +find $BACKUP_DIR -name "db_*.sql.gz" -mtime +30 -delete + +echo "Backup completed: $BACKUP_DIR/db_$TIMESTAMP.sql.gz" +EOF + +chmod +x /srv/containers/mtad-api/backup.sh + +# Schedule daily backups (crontab) +# Edit: crontab -e +# Add: 0 2 * * * /srv/containers/mtad-api/backup.sh >> /var/log/mtad-backup.log 2>&1 +``` + +--- + +## 🔍 Monitoring & Maintenance + +### Health Checks + +```bash +# System health +curl https://mtd.runfoo.run/health + +# API health +curl https://mtd.runfoo.run/api/v1/health + +# Readiness check +curl https://mtd.runfoo.run/api/v1/ready +``` + +### Logs + +```bash +# Real-time logs +docker-compose logs -f + +# Specific service +docker-compose logs -f api +docker-compose logs -f postgres +docker-compose logs -f redis + +# Last 100 lines +docker-compose logs --tail=100 +``` + +### Resource Usage + +```bash +# CPU and memory +docker stats + +# Disk usage +df -h /srv/containers/mtad-api + +# Database size +docker-compose exec postgres psql -U admin -d morethanadiagnosis -c \ + "SELECT pg_size_pretty(pg_database_size('morethanadiagnosis'));" +``` + +### Restart Services + +```bash +# Restart single service +docker-compose restart api + +# Restart all +docker-compose restart + +# Stop services +docker-compose stop + +# Start services +docker-compose start + +# Full restart +docker-compose down && docker-compose up -d +``` + +--- + +## 🔄 SSL Certificate Renewal + +Let's Encrypt certificates expire every 90 days. Set up automatic renewal: + +```bash +# Test renewal +sudo certbot renew --dry-run + +# Set up automatic renewal (cron) +# Edit: sudo crontab -e +# Add: 0 3 * * * certbot renew --quiet && docker-compose reload -s nginx +``` + +--- + +## 📊 Performance Tuning + +### Database Optimization + +```bash +# Connect to database +docker-compose exec postgres psql -U admin -d morethanadiagnosis + +# Check index usage +SELECT * FROM pg_stat_user_indexes; + +# Check table sizes +SELECT schemaname, tablename, pg_size_pretty(pg_total_relation_size(tablename)) +FROM pg_tables +ORDER BY pg_total_relation_size(tablename) DESC; + +# Vacuum and analyze +VACUUM ANALYZE; +``` + +### Redis Cache + +```bash +# Check memory usage +docker-compose exec redis redis-cli INFO memory + +# Clear cache (if needed) +docker-compose exec redis redis-cli FLUSHALL +``` + +### Nginx Performance + +```bash +# Check worker processes +ps aux | grep nginx + +# Monitor connections +netstat -an | grep ESTABLISHED | wc -l +``` + +--- + +## 🆘 Troubleshooting + +### API Not Responding + +```bash +# Check if containers are running +docker-compose ps + +# Check logs +docker-compose logs api + +# Restart API +docker-compose restart api + +# Check connectivity +curl -v https://mtd.runfoo.run/health +``` + +### Database Connection Errors + +```bash +# Check postgres status +docker-compose logs postgres + +# Test database connection +docker-compose exec postgres psql -U admin -d morethanadiagnosis -c "SELECT 1" + +# Check disk space +df -h + +# Restart postgres (WARNING: will interrupt connections) +docker-compose restart postgres +``` + +### High Memory Usage + +```bash +# Check which service +docker stats + +# Redis memory +docker-compose exec redis redis-cli INFO memory + +# Clear Redis cache (temporary) +docker-compose exec redis redis-cli FLUSHALL + +# Permanent: update docker-compose.yml maxmemory-policy +``` + +### Certificate Issues + +```bash +# Check certificate +sudo openssl x509 -in /etc/letsencrypt/live/mtd.runfoo.run/fullchain.pem -text + +# Renew certificate +sudo certbot renew --force-renewal + +# Restart nginx +docker-compose restart nginx +``` + +--- + +## 🔒 Security Best Practices + +### Firewall + +```bash +# Allow only necessary ports +sudo ufw allow 22/tcp # SSH +sudo ufw allow 80/tcp # HTTP +sudo ufw allow 443/tcp # HTTPS +sudo ufw allow 8080/tcp # Health check +sudo ufw enable +``` + +### Regular Updates + +```bash +# Update base images monthly +docker pull postgres:15-alpine +docker pull redis:7-alpine +docker pull nginx:alpine + +# Rebuild and restart +docker-compose build +docker-compose up -d +``` + +### Access Control + +```bash +# Restrict file permissions +chmod 600 /srv/containers/mtad-api/.env +chmod 600 /srv/containers/mtad-api/certbot/conf/letsencrypt/*/privkey.pem + +# Restrict directory +chmod 750 /srv/containers/mtad-api +``` + +--- + +## 📈 Scaling & Load Balancing + +### Horizontal Scaling + +For multiple API instances: + +```yaml +# In docker-compose.yml +services: + api: + deploy: + replicas: 3 + + # Nginx will load balance automatically +``` + +### Database Replication + +For high availability: +- Set up PostgreSQL primary-replica replication +- Use read replicas for analytics +- Configure automatic failover + +--- + +## 🔄 CI/CD Integration + +### GitHub Actions for Auto-Deploy + +```yaml +name: Deploy to Production + +on: + push: + branches: [main] + paths: + - 'backend/**' + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Deploy to nexus-vector + run: | + ssh admin@nexus-vector "cd /srv/containers/mtad-api && \ + git pull origin main && \ + docker-compose build && \ + docker-compose up -d" +``` + +--- + +## 📊 Monitoring Setup (Optional) + +### With Prometheus & Grafana + +```bash +# Add to docker-compose.yml for monitoring +prometheus: + image: prom/prometheus + volumes: + - ./prometheus.yml:/etc/prometheus/prometheus.yml + ports: + - "9090:9090" + +grafana: + image: grafana/grafana + ports: + - "3000:3000" +``` + +### Sentry for Error Tracking + +Update backend config to send errors to Sentry: + +```python +import sentry_sdk +sentry_sdk.init(dsn="https://YOUR_KEY@sentry.io/PROJECT_ID") +``` + +--- + +## 📚 Documentation + +- **API Docs**: https://mtd.runfoo.run/docs +- **ReDoc**: https://mtd.runfoo.run/redoc +- **OpenAPI Schema**: https://mtd.runfoo.run/openapi.json + +--- + +## 🎯 Deployment Status + +- **Server**: nexus-vector (100.95.3.92) +- **Domain**: mtd.runfoo.run +- **Service**: MoreThanADiagnosis API +- **Ports**: 80, 443, 8000, 8080 +- **Database**: PostgreSQL 15 (in Docker) +- **Cache**: Redis 7 (in Docker) +- **Reverse Proxy**: Nginx (Alpine) +- **SSL**: Let's Encrypt (auto-renewal) + +--- + +## 📞 Support + +For issues: +1. Check logs: `docker-compose logs` +2. Verify health: `curl https://mtd.runfoo.run/health` +3. Check backups: `ls -lh /srv/containers/mtad-api/backups/` +4. Review documentation: `https://mtd.runfoo.run/docs` + +--- + +**Job ID**: MTAD-IMPL-2025-11-18-CL +**Last Updated**: 2025-11-18 +**Status**: Ready for deployment +**Maintainer**: Claude (Implementation Agent) + +--- + +## Quick Reference Commands + +```bash +# Common operations +docker-compose up -d # Start all +docker-compose down # Stop all +docker-compose logs -f api # Follow API logs +docker-compose exec api bash # Shell into API +docker-compose restart api # Restart API +docker-compose build --no-cache # Rebuild images +docker-compose ps # Check status + +# Database +docker-compose exec postgres psql -U admin -d morethanadiagnosis # Connect to DB +alembic upgrade head # Run migrations +alembic downgrade -1 # Rollback last migration + +# Monitoring +curl https://mtd.runfoo.run/health # Health check +curl https://mtd.runfoo.run/api/v1/health # API health +docker stats # Resource usage +df -h # Disk usage + +# Backup +/srv/containers/mtad-api/backup.sh # Manual backup +ls -lh /srv/containers/mtad-api/backups/ # List backups + +# SSL +sudo certbot renew --dry-run # Test renewal +sudo certbot certificates # List certificates +``` + +--- + +**Ready to deploy! 🚀** diff --git a/backend/docker-compose.prod.yml b/backend/docker-compose.prod.yml new file mode 100644 index 0000000..6d2e6ad --- /dev/null +++ b/backend/docker-compose.prod.yml @@ -0,0 +1,152 @@ +version: '3.8' + +# Production deployment configuration for nexus-vector +# Job ID: MTAD-IMPL-2025-11-18-CL +# Domain: mtd.runfoo.run +# Deployment: nexus-vector (100.95.3.92) + +services: + postgres: + image: postgres:15-alpine + container_name: mtad-postgres + restart: always + environment: + POSTGRES_USER: ${DB_USER:-admin} + POSTGRES_PASSWORD: ${DB_PASSWORD} + POSTGRES_DB: morethanadiagnosis + POSTGRES_INITDB_ARGS: > + -c shared_preload_libraries=pg_stat_statements + -c max_connections=100 + -c shared_buffers=256MB + volumes: + - postgres_data:/var/lib/postgresql/data + - ./backups:/backups + networks: + - mtad-network + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-admin} -d morethanadiagnosis"] + interval: 10s + timeout: 5s + retries: 5 + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + + redis: + image: redis:7-alpine + container_name: mtad-redis + restart: always + ports: + - "6379:6379" + volumes: + - redis_data:/data + networks: + - mtad-network + command: > + redis-server + --appendonly yes + --appendfsync everysec + --requirepass ${REDIS_PASSWORD} + --maxmemory 512mb + --maxmemory-policy allkeys-lru + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + + api: + build: + context: . + dockerfile: Dockerfile + container_name: mtad-api + restart: always + environment: + ENV: production + DEBUG: "false" + DATABASE_URL: postgresql://${DB_USER:-admin}:${DB_PASSWORD}@postgres:5432/morethanadiagnosis + REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379/0 + SECRET_KEY: ${SECRET_KEY} + ALGORITHM: HS256 + ACCESS_TOKEN_EXPIRE_MINUTES: 15 + REFRESH_TOKEN_EXPIRE_DAYS: 30 + PASSWORD_HASHING_ALGORITHM: argon2 + CORS_ORIGINS: '["https://mtd.runfoo.run", "http://localhost:3000"]' + CORS_CREDENTIALS: "true" + CORS_METHODS: '["*"]' + CORS_HEADERS: '["*"]' + API_VERSION: v1 + API_TITLE: MoreThanADiagnosis API + API_DESCRIPTION: Community Hub for Chronically/Terminally Ill Individuals + LOG_LEVEL: INFO + LOG_FORMAT: json + JAEGER_ENABLED: "false" + RATE_LIMIT_ENABLED: "true" + RATE_LIMIT_REQUESTS: 100 + RATE_LIMIT_PERIOD: 60 + ports: + - "8000:8000" + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + networks: + - mtad-network + volumes: + - ./app:/app/app:ro + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "10" + + nginx: + image: nginx:alpine + container_name: mtad-nginx + restart: always + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf:ro + - ./ssl:/etc/nginx/ssl:ro + - ./certbot/conf:/etc/letsencrypt:ro + - ./certbot/www:/var/www/certbot:ro + networks: + - mtad-network + depends_on: + - api + healthcheck: + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/health"] + interval: 30s + timeout: 10s + retries: 3 + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + +networks: + mtad-network: + driver: bridge + +volumes: + postgres_data: + driver: local + redis_data: + driver: local diff --git a/backend/nginx.conf b/backend/nginx.conf new file mode 100644 index 0000000..66ae2b9 --- /dev/null +++ b/backend/nginx.conf @@ -0,0 +1,180 @@ +user nginx; +worker_processes auto; +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; + use epoll; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + # Performance + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + client_max_body_size 100M; + + # Gzip compression + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_types text/plain text/css text/xml text/javascript + application/json application/javascript application/xml+rss + application/rss+xml font/truetype font/opentype + application/vnd.ms-fontobject image/svg+xml; + + # Rate limiting + limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s; + limit_req_zone $binary_remote_addr zone=auth_limit:10m rate=5r/s; + + # Upstream API + upstream api { + least_conn; + server api:8000 max_fails=3 fail_timeout=30s; + keepalive 32; + } + + # HTTP redirect to HTTPS + server { + listen 80; + server_name mtd.runfoo.run www.mtd.runfoo.run; + + # Let's Encrypt challenge + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + # Redirect all other traffic to HTTPS + location / { + return 301 https://$server_name$request_uri; + } + } + + # HTTPS API server + server { + listen 443 ssl http2; + server_name mtd.runfoo.run; + + # SSL certificates (Let's Encrypt) + ssl_certificate /etc/letsencrypt/live/mtd.runfoo.run/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/mtd.runfoo.run/privkey.pem; + + # SSL configuration + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + ssl_prefer_server_ciphers on; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + + # Security headers + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header X-Frame-Options "SAMEORIGIN" 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; + add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always; + + # Health check endpoint + location /health { + access_log off; + proxy_pass http://api; + proxy_set_header Host $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; + } + + # API endpoints with rate limiting + location /api/v1/auth/ { + limit_req zone=auth_limit burst=10 nodelay; + + proxy_pass http://api; + proxy_http_version 1.1; + proxy_set_header Host $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; + proxy_set_header Connection ""; + proxy_redirect off; + } + + location /api/v1/ { + limit_req zone=api_limit burst=20 nodelay; + + proxy_pass http://api; + proxy_http_version 1.1; + proxy_set_header Host $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; + proxy_set_header Connection ""; + proxy_redirect off; + + # Timeouts + proxy_connect_timeout 10s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + + # API documentation + location /docs { + proxy_pass http://api; + proxy_set_header Host $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; + } + + location /redoc { + proxy_pass http://api; + proxy_set_header Host $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; + } + + location /openapi.json { + proxy_pass http://api; + proxy_set_header Host $host; + } + + # Default response + location / { + return 404; + } + + # Deny access to sensitive files + location ~ /\. { + deny all; + } + } + + # HTTP health check (for load balancers) + server { + listen 8080; + server_name _; + + location /health { + access_log off; + return 200 "healthy\n"; + add_header Content-Type text/plain; + } + + location / { + return 404; + } + } +}