diff --git a/.env.staging.example b/.env.staging.example new file mode 100644 index 00000000..4a526389 --- /dev/null +++ b/.env.staging.example @@ -0,0 +1,83 @@ +# ============================================================================= +# IGNY8 STAGING ENVIRONMENT CONFIGURATION +# ============================================================================= +# Copy this file to .env.staging and configure values +# This environment runs alongside production with separate database/redis +# ============================================================================= + +# Environment Identifier +DJANGO_ENV=staging +DEBUG=False + +# Database (Uses separate staging database) +DB_HOST=postgres +DB_NAME=igny8_staging_db +DB_USER=igny8 +DB_PASSWORD=igny8pass +DB_PORT=5432 + +# Redis (Uses DB index 1 instead of 0) +REDIS_HOST=redis +REDIS_PORT=6379 +REDIS_DB=1 + +# Security (Generate unique key for staging) +SECRET_KEY=staging-secret-key-CHANGE-THIS-TO-UNIQUE-VALUE +ALLOWED_HOSTS=staging-api.igny8.com,staging.igny8.com,localhost,127.0.0.1 +CORS_ALLOWED_ORIGINS=https://staging.igny8.com,https://staging-api.igny8.com + +# ============================================================================= +# API Keys - USE TEST/SANDBOX KEYS FOR STAGING +# ============================================================================= + +# AI Services (can use same keys or separate test keys) +OPENAI_API_KEY=sk-your-openai-key +ANTHROPIC_API_KEY=sk-ant-your-anthropic-key + +# Image Generation +RUNWARE_API_KEY=your-runware-key +BRIA_API_KEY=your-bria-key + +# ============================================================================= +# Payment Gateways - MUST USE SANDBOX/TEST MODE +# ============================================================================= + +# Stripe (TEST MODE) +STRIPE_SECRET_KEY=sk_test_your_stripe_test_key +STRIPE_PUBLISHABLE_KEY=pk_test_your_stripe_test_key +STRIPE_WEBHOOK_SECRET=whsec_test_your_webhook_secret + +# PayPal (SANDBOX MODE) +PAYPAL_CLIENT_ID=sandbox_client_id +PAYPAL_CLIENT_SECRET=sandbox_client_secret +PAYPAL_MODE=sandbox + +# ============================================================================= +# Email - Use test email service or same service +# ============================================================================= + +RESEND_API_KEY=re_your_resend_key +BREVO_API_KEY=your_brevo_key +DEFAULT_FROM_EMAIL=staging@igny8.com + +# ============================================================================= +# URLs +# ============================================================================= + +FRONTEND_URL=https://staging.igny8.com +BACKEND_URL=https://staging-api.igny8.com +MARKETING_URL=https://staging-marketing.igny8.com + +# ============================================================================= +# Feature Flags (can enable/disable features for testing) +# ============================================================================= + +ENABLE_LINKER=True +ENABLE_OPTIMIZER=True +ENABLE_SOCIALIZER=False + +# ============================================================================= +# Logging +# ============================================================================= + +LOG_LEVEL=DEBUG diff --git a/GO-LIVE-CHECKLIST.md b/GO-LIVE-CHECKLIST.md new file mode 100644 index 00000000..01e39de0 --- /dev/null +++ b/GO-LIVE-CHECKLIST.md @@ -0,0 +1,145 @@ +# πŸš€ IGNY8 Go-Live Checklist + +**Date:** January 20, 2026 +**Purpose:** Quick reference for launching IGNY8 to production + +--- + +## βœ… Pre-Launch Checklist + +### Infrastructure Ready +- [ ] PostgreSQL running and accessible +- [ ] Redis running and accessible +- [ ] Caddy configured with SSL for all domains +- [ ] DNS records pointing to server +- [ ] Firewall configured (ports 80, 443 open) + +### Application Ready +- [ ] Production `.env` configured with real secrets +- [ ] All API keys set (OpenAI, Stripe, etc.) +- [ ] Django `SECRET_KEY` is unique and secure +- [ ] `DEBUG=False` in production +- [ ] CORS and ALLOWED_HOSTS configured + +### Operational Scripts Ready +- [x] `/data/app/igny8/scripts/ops/backup-db.sh` +- [x] `/data/app/igny8/scripts/ops/backup-full.sh` +- [x] `/data/app/igny8/scripts/ops/restore-db.sh` +- [x] `/data/app/igny8/scripts/ops/deploy-production.sh` +- [x] `/data/app/igny8/scripts/ops/deploy-staging.sh` +- [x] `/data/app/igny8/scripts/ops/rollback.sh` +- [x] `/data/app/igny8/scripts/ops/health-check.sh` +- [x] `/data/app/igny8/scripts/ops/sync-prod-to-staging.sh` +- [x] `/data/app/igny8/scripts/ops/log-rotate.sh` + +### Staging Environment (Optional but Recommended) +- [x] `docker-compose.staging.yml` created +- [x] `.env.staging.example` created +- [ ] Copy to `.env.staging` and configure +- [ ] Create staging database +- [ ] Configure staging DNS records + +--- + +## 🏁 Go-Live Steps + +### Step 1: Create Initial Backup +```bash +/data/app/igny8/scripts/ops/backup-db.sh pre-deploy +``` + +### Step 2: Verify Health +```bash +/data/app/igny8/scripts/ops/health-check.sh +``` + +### Step 3: Set Up Automated Backups +```bash +# Install cron job +sudo cp /data/app/igny8/scripts/ops/igny8-cron /etc/cron.d/igny8 +sudo chmod 644 /etc/cron.d/igny8 + +# Verify cron +sudo crontab -l -u root +``` + +### Step 4: Test Backup & Restore (Optional) +```bash +# Create test backup +/data/app/igny8/scripts/ops/backup-db.sh daily + +# Verify backup exists +ls -la /data/backups/daily/ +``` + +--- + +## πŸ“‹ Daily Operations + +### Check System Health +```bash +/data/app/igny8/scripts/ops/health-check.sh +``` + +### View Logs +```bash +# Backend logs +docker logs -f igny8_backend + +# All app logs +docker compose -f /data/app/igny8/docker-compose.app.yml -p igny8-app logs -f +``` + +### Container Status +```bash +docker compose -f /data/app/igny8/docker-compose.app.yml -p igny8-app ps +``` + +--- + +## 🚨 Emergency Procedures + +### Immediate Rollback +```bash +/data/app/igny8/scripts/ops/rollback.sh +``` + +### Restore Database +```bash +# List available backups +ls -la /data/backups/ + +# Restore from latest +/data/app/igny8/scripts/ops/restore-db.sh /data/backups/latest_db.sql.gz +``` + +### Restart All Services +```bash +docker compose -f /data/app/igny8/docker-compose.app.yml -p igny8-app restart +``` + +--- + +## πŸ“ Key File Locations + +| Item | Location | +|------|----------| +| Production Compose | `/data/app/igny8/docker-compose.app.yml` | +| Staging Compose | `/data/app/igny8/docker-compose.staging.yml` | +| Production Env | `/data/app/igny8/.env` | +| Staging Env | `/data/app/igny8/.env.staging` | +| Ops Scripts | `/data/app/igny8/scripts/ops/` | +| Backups | `/data/backups/` | +| Logs | `/data/logs/` | + +--- + +## πŸ“ž Support Contacts + +- **Documentation:** `/data/app/igny8/docs/` +- **Deployment Guide:** `/data/app/igny8/docs/50-DEPLOYMENT/` +- **Operations Guide:** `/data/app/igny8/docs/50-DEPLOYMENT/DEVOPS-OPERATIONS-GUIDE.md` + +--- + +**You're ready to go live! πŸŽ‰** diff --git a/docker-compose.staging.yml b/docker-compose.staging.yml new file mode 100644 index 00000000..206ac578 --- /dev/null +++ b/docker-compose.staging.yml @@ -0,0 +1,150 @@ +# ============================================================================= +# IGNY8 STAGING ENVIRONMENT COMPOSE FILE +# ============================================================================= +# Runs alongside production on the same server using different: +# - Ports, database, Redis DB, domains, and container names +# ============================================================================= +# +# Usage: +# docker compose -f docker-compose.staging.yml -p igny8-staging up -d +# docker compose -f docker-compose.staging.yml -p igny8-staging down +# docker compose -f docker-compose.staging.yml -p igny8-staging logs -f +# ============================================================================= + +name: igny8-staging + +services: + igny8_staging_backend: + image: igny8-backend:staging + container_name: igny8_staging_backend + restart: always + working_dir: /app + ports: + - "0.0.0.0:8012:8010" + environment: + DJANGO_ENV: staging + DB_HOST: postgres + DB_NAME: igny8_staging_db + DB_USER: igny8 + DB_PASSWORD: igny8pass + REDIS_HOST: redis + REDIS_PORT: "6379" + REDIS_DB: "1" + USE_SECURE_COOKIES: "True" + USE_SECURE_PROXY_HEADER: "True" + DEBUG: "False" + volumes: + - /data/app/igny8/backend:/app:rw + - /data/app/igny8:/data/app/igny8:rw + - /var/run/docker.sock:/var/run/docker.sock:ro + - /data/logs/staging:/app/logs:rw + env_file: + - .env.staging + healthcheck: + test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:8010/api/v1/system/status/').read()\" || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + command: ["gunicorn", "igny8_core.wsgi:application", "--bind", "0.0.0.0:8010", "--workers", "2", "--timeout", "120"] + networks: [igny8_net] + labels: + - "com.docker.compose.project=igny8-staging" + - "com.docker.compose.service=igny8_staging_backend" + + igny8_staging_frontend: + image: igny8-frontend-dev:staging + container_name: igny8_staging_frontend + restart: always + ports: + - "0.0.0.0:8024:5173" + environment: + VITE_BACKEND_URL: "https://staging-api.igny8.com/api" + VITE_ENV: "staging" + volumes: + - /data/app/igny8/frontend:/app:rw + depends_on: + igny8_staging_backend: + condition: service_healthy + networks: [igny8_net] + labels: + - "com.docker.compose.project=igny8-staging" + - "com.docker.compose.service=igny8_staging_frontend" + + igny8_staging_marketing_dev: + image: igny8-marketing-dev:staging + container_name: igny8_staging_marketing_dev + restart: always + ports: + - "0.0.0.0:8026:5174" + environment: + VITE_BACKEND_URL: "https://staging-api.igny8.com/api" + VITE_ENV: "staging" + volumes: + - /data/app/igny8/frontend:/app:rw + networks: [igny8_net] + labels: + - "com.docker.compose.project=igny8-staging" + - "com.docker.compose.service=igny8_staging_marketing_dev" + + igny8_staging_celery_worker: + image: igny8-backend:staging + container_name: igny8_staging_celery_worker + restart: always + working_dir: /app + environment: + DJANGO_ENV: staging + DB_HOST: postgres + DB_NAME: igny8_staging_db + DB_USER: igny8 + DB_PASSWORD: igny8pass + REDIS_HOST: redis + REDIS_PORT: "6379" + REDIS_DB: "1" + C_FORCE_ROOT: "true" + volumes: + - /data/app/igny8/backend:/app:rw + - /data/logs/staging:/app/logs:rw + env_file: + - .env.staging + command: ["celery", "-A", "igny8_core", "worker", "--loglevel=info", "--concurrency=2"] + depends_on: + igny8_staging_backend: + condition: service_healthy + networks: [igny8_net] + labels: + - "com.docker.compose.project=igny8-staging" + - "com.docker.compose.service=igny8_staging_celery_worker" + + igny8_staging_celery_beat: + image: igny8-backend:staging + container_name: igny8_staging_celery_beat + restart: always + working_dir: /app + environment: + DJANGO_ENV: staging + DB_HOST: postgres + DB_NAME: igny8_staging_db + DB_USER: igny8 + DB_PASSWORD: igny8pass + REDIS_HOST: redis + REDIS_PORT: "6379" + REDIS_DB: "1" + C_FORCE_ROOT: "true" + volumes: + - /data/app/igny8/backend:/app:rw + - /data/logs/staging:/app/logs:rw + env_file: + - .env.staging + command: ["celery", "-A", "igny8_core", "beat", "--loglevel=info", "--scheduler", "django_celery_beat.schedulers:DatabaseScheduler"] + depends_on: + igny8_staging_backend: + condition: service_healthy + networks: [igny8_net] + labels: + - "com.docker.compose.project=igny8-staging" + - "com.docker.compose.service=igny8_staging_celery_beat" + +networks: + igny8_net: + external: true diff --git a/docs/50-DEPLOYMENT/DEVOPS-OPERATIONS-GUIDE.md b/docs/50-DEPLOYMENT/DEVOPS-OPERATIONS-GUIDE.md new file mode 100644 index 00000000..84998716 --- /dev/null +++ b/docs/50-DEPLOYMENT/DEVOPS-OPERATIONS-GUIDE.md @@ -0,0 +1,303 @@ +# DevOps Operations Guide + +**Purpose:** Complete operational procedures for managing IGNY8 in production +**Version:** 1.0 +**Last Updated:** January 20, 2026 + +--- + +## πŸ“‹ Executive Summary + +This document provides a complete structure for: +1. **Automated Backups** - Regular database + config backups +2. **Environment Management** - Dev vs Staging vs Production +3. **Health Monitoring** - Automated health checks & alerts +4. **Disaster Recovery** - Quick recovery procedures +5. **Change Management** - Safe deployment workflow + +--- + +## πŸ—‚οΈ Directory Structure (To Be Implemented) + +``` +/data/ +β”œβ”€β”€ app/ +β”‚ └── igny8/ # Application code +β”‚ β”œβ”€β”€ docker-compose.app.yml # Production compose βœ… +β”‚ β”œβ”€β”€ docker-compose.staging.yml # Staging compose ⚠️ TO CREATE +β”‚ β”œβ”€β”€ .env # Production env +β”‚ β”œβ”€β”€ .env.staging # Staging env ⚠️ TO CREATE +β”‚ └── scripts/ +β”‚ └── ops/ # ⚠️ TO CREATE +β”‚ β”œβ”€β”€ backup-db.sh # Database backup +β”‚ β”œβ”€β”€ backup-full.sh # Full backup (db + code + config) +β”‚ β”œβ”€β”€ restore-db.sh # Database restore +β”‚ β”œβ”€β”€ deploy-staging.sh # Deploy to staging +β”‚ β”œβ”€β”€ deploy-production.sh# Deploy to production +β”‚ β”œβ”€β”€ rollback.sh # Rollback deployment +β”‚ β”œβ”€β”€ health-check.sh # System health check +β”‚ β”œβ”€β”€ sync-prod-to-staging.sh # Sync data +β”‚ └── log-rotate.sh # Log rotation +β”‚ +β”œβ”€β”€ backups/ # Backup storage +β”‚ β”œβ”€β”€ daily/ # Daily automated backups +β”‚ β”‚ └── YYYYMMDD/ +β”‚ β”‚ β”œβ”€β”€ db_igny8_YYYYMMDD_HHMMSS.sql.gz +β”‚ β”‚ └── config_YYYYMMDD.tar.gz +β”‚ β”œβ”€β”€ weekly/ # Weekly backups (kept 4 weeks) +β”‚ β”œβ”€β”€ monthly/ # Monthly backups (kept 12 months) +β”‚ └── pre-deploy/ # Pre-deployment snapshots +β”‚ └── YYYYMMDD_HHMMSS/ +β”‚ +β”œβ”€β”€ logs/ # Centralized logs +β”‚ β”œβ”€β”€ production/ +β”‚ β”‚ β”œβ”€β”€ backend.log +β”‚ β”‚ β”œβ”€β”€ celery-worker.log +β”‚ β”‚ β”œβ”€β”€ celery-beat.log +β”‚ β”‚ └── access.log +β”‚ β”œβ”€β”€ staging/ +β”‚ └── caddy/ +β”‚ +└── stack/ # Infrastructure stack + └── igny8-stack/ # (Future - not yet separated) +``` + +--- + +## πŸ”„ Automated Backup System + +### Backup Strategy + +| Type | Frequency | Retention | Content | +|------|-----------|-----------|---------| +| **Daily** | 1:00 AM | 7 days | Database + configs | +| **Weekly** | Sunday 2:00 AM | 4 weeks | Full backup | +| **Monthly** | 1st of month | 12 months | Full backup | +| **Pre-Deploy** | Before each deploy | 5 most recent | Database snapshot | + +### Cron Schedule + +```bash +# /etc/cron.d/igny8-backup + +# Daily database backup at 1:00 AM +0 1 * * * root /data/app/igny8/scripts/ops/backup-db.sh daily >> /data/logs/backup.log 2>&1 + +# Weekly full backup on Sunday at 2:00 AM +0 2 * * 0 root /data/app/igny8/scripts/ops/backup-full.sh weekly >> /data/logs/backup.log 2>&1 + +# Monthly full backup on 1st at 3:00 AM +0 3 1 * * root /data/app/igny8/scripts/ops/backup-full.sh monthly >> /data/logs/backup.log 2>&1 + +# Health check every 5 minutes +*/5 * * * * root /data/app/igny8/scripts/ops/health-check.sh >> /data/logs/health.log 2>&1 + +# Log rotation daily at midnight +0 0 * * * root /data/app/igny8/scripts/ops/log-rotate.sh >> /data/logs/maintenance.log 2>&1 +``` + +--- + +## 🌍 Environment Management + +### Environment Comparison + +| Aspect | Development | Staging | Production | +|--------|-------------|---------|------------| +| **Domain** | localhost:5173 | staging.igny8.com | app.igny8.com | +| **API** | localhost:8010 | staging-api.igny8.com | api.igny8.com | +| **Database** | igny8_dev_db | igny8_staging_db | igny8_db | +| **Redis DB** | 2 | 1 | 0 | +| **Debug** | True | False | False | +| **AI Keys** | Test/Limited | Test/Limited | Production | +| **Payments** | Sandbox | Sandbox | Live | +| **Compose File** | docker-compose.dev.yml | docker-compose.staging.yml | docker-compose.app.yml | +| **Project Name** | igny8-dev | igny8-staging | igny8-app | + +### Port Allocation + +| Service | Dev | Staging | Production | +|---------|-----|---------|------------| +| Backend | 8010 | 8012 | 8011 | +| Frontend | 5173 | 8024 | 8021 | +| Marketing | 5174 | 8026 | 8023 | +| Flower | - | 5556 | 5555 | + +--- + +## πŸš€ Deployment Workflow + +### Safe Deployment Checklist + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ DEPLOYMENT CHECKLIST β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ PRE-DEPLOYMENT β”‚ +β”‚ β–‘ All tests passing on staging? β”‚ +β”‚ β–‘ Database migrations reviewed? β”‚ +β”‚ β–‘ Backup created? β”‚ +β”‚ β–‘ Rollback plan ready? β”‚ +β”‚ β–‘ Team notified? β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ DEPLOYMENT β”‚ +β”‚ β–‘ Create pre-deploy backup β”‚ +β”‚ β–‘ Tag current images for rollback β”‚ +β”‚ β–‘ Pull latest code β”‚ +β”‚ β–‘ Build new images β”‚ +β”‚ β–‘ Apply migrations β”‚ +β”‚ β–‘ Restart containers β”‚ +β”‚ β–‘ Verify health check β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ POST-DEPLOYMENT β”‚ +β”‚ β–‘ Monitor logs for 10 minutes β”‚ +β”‚ β–‘ Test critical paths (login, API, AI functions) β”‚ +β”‚ β–‘ Check error rates β”‚ +β”‚ β–‘ If issues β†’ ROLLBACK β”‚ +β”‚ β–‘ Update changelog β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +### Git Branch Strategy + +``` + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ main β”‚ ← Production deployments + β””β”€β”€β”€β”€β–²β”€β”€β”€β”€β”€β”˜ + β”‚ merge (after staging approval) + β”Œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β” + β”‚ staging β”‚ ← Staging deployments + β””β”€β”€β”€β”€β–²β”€β”€β”€β”€β”€β”˜ + β”‚ merge + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ β”‚ β”‚ +β”Œβ”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β” +β”‚feature/xyz β”‚ β”‚feature/abc β”‚ β”‚hotfix/urgent β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +--- + +## πŸ₯ Health Monitoring + +### Health Check Endpoints + +| Endpoint | Purpose | Expected Response | +|----------|---------|-------------------| +| `/api/v1/system/status/` | Overall system status | `{"status": "healthy"}` | +| `/api/v1/system/health/` | Detailed component health | JSON with all components | + +### Monitoring Targets + +1. **Backend API** - Response time < 500ms +2. **Database** - Connection pool healthy +3. **Redis** - Connection alive +4. **Celery Workers** - Queue length < 100 +5. **Celery Beat** - Scheduler running +6. **Disk Space** - > 20% free +7. **Memory** - < 80% used + +### Alert Thresholds + +| Metric | Warning | Critical | +|--------|---------|----------| +| API Response Time | > 1s | > 5s | +| Error Rate | > 1% | > 5% | +| CPU Usage | > 70% | > 90% | +| Memory Usage | > 70% | > 90% | +| Disk Usage | > 70% | > 90% | +| Celery Queue | > 50 | > 200 | + +--- + +## πŸ”§ Common Operations + +### Daily Operations + +```bash +# Check system health +/data/app/igny8/scripts/ops/health-check.sh + +# View logs +tail -f /data/logs/production/backend.log + +# Check container status +docker compose -f /data/app/igny8/docker-compose.app.yml -p igny8-app ps +``` + +### Weekly Operations + +```bash +# Review backup status +ls -la /data/backups/daily/ +du -sh /data/backups/* + +# Check disk space +df -h + +# Review error logs +grep -i error /data/logs/production/backend.log | tail -50 +``` + +### Emergency Procedures + +```bash +# Immediate rollback +/data/app/igny8/scripts/ops/rollback.sh + +# Emergency restart +docker compose -f /data/app/igny8/docker-compose.app.yml -p igny8-app restart + +# Emergency database restore +/data/app/igny8/scripts/ops/restore-db.sh /data/backups/latest.sql.gz +``` + +--- + +## πŸ“Š What's Missing (Action Items) + +### Priority 1 - Critical (Before Go-Live) + +| Item | Status | Action | +|------|--------|--------| +| `docker-compose.staging.yml` | ❌ Missing | Create from documentation | +| `.env.staging` | ❌ Missing | Create from example | +| Deployment scripts | ❌ Missing | Create all ops scripts | +| Automated backup cron | ❌ Missing | Set up cron jobs | +| Pre-deploy backup | ❌ Missing | Add to deploy script | + +### Priority 2 - Important (First Week) + +| Item | Status | Action | +|------|--------|--------| +| Health check automation | ❌ Missing | Create monitoring | +| Log rotation | ❌ Missing | Set up logrotate | +| Staging DNS | ❌ Unknown | Configure if needed | +| Caddyfile staging routes | ❌ Unknown | Add staging domains | + +### Priority 3 - Nice to Have (First Month) + +| Item | Status | Action | +|------|--------|--------| +| CI/CD pipeline | ❌ Not set | Optional automation | +| External monitoring | ❌ Not set | UptimeRobot/Datadog | +| Alerting system | ❌ Not set | Email/Slack alerts | + +--- + +## Next Steps + +1. **Create ops scripts directory**: `/data/app/igny8/scripts/ops/` +2. **Create all deployment scripts** (see STAGING-SETUP-GUIDE.md) +3. **Create staging compose file** (copy from documentation) +4. **Set up automated backups** +5. **Test complete deployment cycle** on staging +6. **Go live with confidence** + +--- + +## Related Documentation + +- [STAGING-SETUP-GUIDE.md](final-clean-best-deployment-plan/STAGING-SETUP-GUIDE.md) - Detailed staging setup +- [TWO-REPO-ARCHITECTURE.md](final-clean-best-deployment-plan/TWO-REPO-ARCHITECTURE.md) - Architecture overview +- [INFRASTRUCTURE-STACK.md](final-clean-best-deployment-plan/INFRASTRUCTURE-STACK.md) - Stack details diff --git a/scripts/ops/backup-db.sh b/scripts/ops/backup-db.sh new file mode 100644 index 00000000..573adbd2 --- /dev/null +++ b/scripts/ops/backup-db.sh @@ -0,0 +1,193 @@ +#!/bin/bash +# ============================================================================= +# IGNY8 Database Backup Script +# ============================================================================= +# Usage: ./backup-db.sh [daily|weekly|monthly|pre-deploy] +# Creates compressed database backup with automatic retention +# ============================================================================= + +set -e + +# Configuration +BACKUP_TYPE="${1:-daily}" +TIMESTAMP=$(date +%Y%m%d_%H%M%S) +DATE_FOLDER=$(date +%Y%m%d) + +# Directories +BACKUP_ROOT="/data/backups" +DAILY_DIR="${BACKUP_ROOT}/daily" +WEEKLY_DIR="${BACKUP_ROOT}/weekly" +MONTHLY_DIR="${BACKUP_ROOT}/monthly" +PREDEPLOY_DIR="${BACKUP_ROOT}/pre-deploy" + +# Database settings (from docker environment) +DB_CONTAINER="postgres" +DB_NAME="igny8_db" +DB_USER="igny8" + +# Retention settings +DAILY_RETENTION_DAYS=7 +WEEKLY_RETENTION_WEEKS=4 +MONTHLY_RETENTION_MONTHS=12 +PREDEPLOY_RETENTION_COUNT=5 + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +log() { + echo -e "[$(date '+%Y-%m-%d %H:%M:%S')] $1" +} + +log_success() { + log "${GREEN}βœ… $1${NC}" +} + +log_warn() { + log "${YELLOW}⚠️ $1${NC}" +} + +log_error() { + log "${RED}❌ $1${NC}" +} + +# Create directories if they don't exist +create_dirs() { + mkdir -p "${DAILY_DIR}" + mkdir -p "${WEEKLY_DIR}" + mkdir -p "${MONTHLY_DIR}" + mkdir -p "${PREDEPLOY_DIR}" +} + +# Determine backup destination based on type +get_backup_path() { + case $BACKUP_TYPE in + daily) + echo "${DAILY_DIR}/${DATE_FOLDER}" + ;; + weekly) + echo "${WEEKLY_DIR}/week_$(date +%Y_%W)" + ;; + monthly) + echo "${MONTHLY_DIR}/$(date +%Y_%m)" + ;; + pre-deploy) + echo "${PREDEPLOY_DIR}/${TIMESTAMP}" + ;; + *) + log_error "Invalid backup type: $BACKUP_TYPE" + exit 1 + ;; + esac +} + +# Perform database backup +backup_database() { + local backup_dir="$1" + local backup_file="${backup_dir}/db_${DB_NAME}_${TIMESTAMP}.sql.gz" + + mkdir -p "${backup_dir}" + + log "Starting database backup: ${DB_NAME}" + log "Destination: ${backup_file}" + + # Check if postgres container is running + if ! docker ps --format '{{.Names}}' | grep -q "^${DB_CONTAINER}$"; then + log_error "PostgreSQL container '${DB_CONTAINER}' is not running" + exit 1 + fi + + # Perform backup with compression + if docker exec "${DB_CONTAINER}" pg_dump -U "${DB_USER}" "${DB_NAME}" | gzip > "${backup_file}"; then + local size=$(du -h "${backup_file}" | cut -f1) + log_success "Database backup complete: ${backup_file} (${size})" + + # Create latest symlink + ln -sf "${backup_file}" "${BACKUP_ROOT}/latest_db.sql.gz" + + return 0 + else + log_error "Database backup failed" + rm -f "${backup_file}" + return 1 + fi +} + +# Clean up old backups based on retention policy +cleanup_old_backups() { + log "Cleaning up old backups..." + + # Daily backups - keep last N days + if [[ -d "${DAILY_DIR}" ]]; then + find "${DAILY_DIR}" -type d -mtime +${DAILY_RETENTION_DAYS} -exec rm -rf {} \; 2>/dev/null || true + local daily_count=$(find "${DAILY_DIR}" -type f -name "*.sql.gz" | wc -l) + log "Daily backups: ${daily_count} files" + fi + + # Weekly backups - keep last N weeks + if [[ -d "${WEEKLY_DIR}" ]]; then + find "${WEEKLY_DIR}" -type d -mtime +$((WEEKLY_RETENTION_WEEKS * 7)) -exec rm -rf {} \; 2>/dev/null || true + local weekly_count=$(find "${WEEKLY_DIR}" -type f -name "*.sql.gz" | wc -l) + log "Weekly backups: ${weekly_count} files" + fi + + # Monthly backups - keep last N months + if [[ -d "${MONTHLY_DIR}" ]]; then + find "${MONTHLY_DIR}" -type d -mtime +$((MONTHLY_RETENTION_MONTHS * 30)) -exec rm -rf {} \; 2>/dev/null || true + local monthly_count=$(find "${MONTHLY_DIR}" -type f -name "*.sql.gz" | wc -l) + log "Monthly backups: ${monthly_count} files" + fi + + # Pre-deploy backups - keep last N + if [[ -d "${PREDEPLOY_DIR}" ]]; then + local predeploy_dirs=$(ls -dt "${PREDEPLOY_DIR}"/*/ 2>/dev/null | tail -n +$((PREDEPLOY_RETENTION_COUNT + 1))) + if [[ -n "$predeploy_dirs" ]]; then + echo "$predeploy_dirs" | xargs rm -rf 2>/dev/null || true + fi + local predeploy_count=$(find "${PREDEPLOY_DIR}" -maxdepth 1 -type d | wc -l) + log "Pre-deploy backups: $((predeploy_count - 1)) snapshots" + fi + + log_success "Cleanup complete" +} + +# Display backup summary +show_summary() { + echo "" + echo "==========================================" + echo "BACKUP SUMMARY" + echo "==========================================" + echo "Type: ${BACKUP_TYPE}" + echo "Database: ${DB_NAME}" + echo "Timestamp: ${TIMESTAMP}" + echo "" + echo "Disk Usage:" + du -sh "${BACKUP_ROOT}"/* 2>/dev/null || echo "No backups yet" + echo "" + echo "Latest backup: $(readlink -f ${BACKUP_ROOT}/latest_db.sql.gz 2>/dev/null || echo 'None')" + echo "==========================================" +} + +# Main execution +main() { + echo "==========================================" + echo "IGNY8 Database Backup" + echo "==========================================" + echo "" + + create_dirs + + local backup_path=$(get_backup_path) + + if backup_database "${backup_path}"; then + cleanup_old_backups + show_summary + exit 0 + else + exit 1 + fi +} + +main "$@" diff --git a/scripts/ops/backup-full.sh b/scripts/ops/backup-full.sh new file mode 100644 index 00000000..5883a611 --- /dev/null +++ b/scripts/ops/backup-full.sh @@ -0,0 +1,150 @@ +#!/bin/bash +# ============================================================================= +# IGNY8 Full Backup Script +# ============================================================================= +# Creates complete backup: database + configuration + code snapshot +# Usage: ./backup-full.sh [weekly|monthly] +# ============================================================================= + +set -e + +# Configuration +BACKUP_TYPE="${1:-weekly}" +TIMESTAMP=$(date +%Y%m%d_%H%M%S) +APP_DIR="/data/app/igny8" +BACKUP_ROOT="/data/backups" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +log() { + echo -e "[$(date '+%Y-%m-%d %H:%M:%S')] $1" +} + +log_success() { + log "${GREEN}βœ… $1${NC}" +} + +log_error() { + log "${RED}❌ $1${NC}" +} + +# Determine backup directory +case $BACKUP_TYPE in + weekly) + BACKUP_DIR="${BACKUP_ROOT}/weekly/week_$(date +%Y_%W)" + ;; + monthly) + BACKUP_DIR="${BACKUP_ROOT}/monthly/$(date +%Y_%m)" + ;; + *) + log_error "Invalid backup type. Use: weekly or monthly" + exit 1 + ;; +esac + +mkdir -p "${BACKUP_DIR}" + +echo "==========================================" +echo "IGNY8 Full Backup" +echo "==========================================" +echo "Type: ${BACKUP_TYPE}" +echo "Destination: ${BACKUP_DIR}" +echo "" + +# Step 1: Database backup +log "Step 1/4: Backing up database..." +/data/app/igny8/scripts/ops/backup-db.sh "${BACKUP_TYPE}" || { + log_error "Database backup failed" + exit 1 +} + +# Step 2: Configuration backup +log "Step 2/4: Backing up configuration files..." +CONFIG_BACKUP="${BACKUP_DIR}/config_${TIMESTAMP}.tar.gz" +tar -czf "${CONFIG_BACKUP}" \ + -C "${APP_DIR}" \ + .env \ + .env.staging 2>/dev/null \ + docker-compose.app.yml \ + docker-compose.staging.yml 2>/dev/null \ + --ignore-failed-read || true + +if [[ -f "${CONFIG_BACKUP}" ]]; then + log_success "Config backup: ${CONFIG_BACKUP}" +else + log_error "Config backup failed" +fi + +# Step 3: Code snapshot (without node_modules, __pycache__, etc.) +log "Step 3/4: Creating code snapshot..." +CODE_BACKUP="${BACKUP_DIR}/code_${TIMESTAMP}.tar.gz" +tar -czf "${CODE_BACKUP}" \ + -C "/data/app" \ + --exclude='igny8/node_modules' \ + --exclude='igny8/frontend/node_modules' \ + --exclude='igny8/__pycache__' \ + --exclude='igny8/**/__pycache__' \ + --exclude='igny8/*.pyc' \ + --exclude='igny8/**/*.pyc' \ + --exclude='igny8/.git' \ + --exclude='igny8/backend/staticfiles' \ + --exclude='igny8/frontend/dist' \ + --exclude='igny8/backend/media' \ + --exclude='igny8/logs' \ + igny8/ 2>/dev/null || true + +if [[ -f "${CODE_BACKUP}" ]]; then + local size=$(du -h "${CODE_BACKUP}" | cut -f1) + log_success "Code backup: ${CODE_BACKUP} (${size})" +else + log_error "Code backup failed" +fi + +# Step 4: Media files (if they exist) +log "Step 4/4: Backing up media files..." +MEDIA_DIR="${APP_DIR}/backend/media" +if [[ -d "${MEDIA_DIR}" ]] && [[ "$(ls -A ${MEDIA_DIR})" ]]; then + MEDIA_BACKUP="${BACKUP_DIR}/media_${TIMESTAMP}.tar.gz" + tar -czf "${MEDIA_BACKUP}" -C "${APP_DIR}/backend" media/ 2>/dev/null || true + if [[ -f "${MEDIA_BACKUP}" ]]; then + local media_size=$(du -h "${MEDIA_BACKUP}" | cut -f1) + log_success "Media backup: ${MEDIA_BACKUP} (${media_size})" + fi +else + log "No media files to backup" +fi + +# Create manifest +MANIFEST="${BACKUP_DIR}/manifest.txt" +cat > "${MANIFEST}" << EOF +IGNY8 Full Backup Manifest +=========================== +Timestamp: ${TIMESTAMP} +Type: ${BACKUP_TYPE} +Server: $(hostname) + +Files: +$(ls -la "${BACKUP_DIR}") + +Sizes: +$(du -sh "${BACKUP_DIR}"/*) + +Git Info: +$(cd "${APP_DIR}" && git log -1 --format='Commit: %H%nDate: %ci%nMessage: %s' 2>/dev/null || echo 'Not a git repo') +EOF + +echo "" +echo "==========================================" +echo "BACKUP COMPLETE" +echo "==========================================" +echo "Location: ${BACKUP_DIR}" +echo "" +echo "Contents:" +ls -lh "${BACKUP_DIR}" +echo "" +echo "Total size: $(du -sh ${BACKUP_DIR} | cut -f1)" +echo "==========================================" diff --git a/scripts/ops/deploy-production.sh b/scripts/ops/deploy-production.sh new file mode 100644 index 00000000..4647e752 --- /dev/null +++ b/scripts/ops/deploy-production.sh @@ -0,0 +1,145 @@ +#!/bin/bash +# ============================================================================= +# IGNY8 Deploy to Production +# ============================================================================= +# Safely deploys code to production with backup and rollback capability +# Usage: ./deploy-production.sh +# ============================================================================= + +set -e + +APP_DIR="/data/app/igny8" +COMPOSE_FILE="docker-compose.app.yml" +PROJECT_NAME="igny8-app" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +log() { + echo -e "${BLUE}[$(date '+%H:%M:%S')]${NC} $1" +} + +log_success() { + echo -e "${GREEN}βœ… $1${NC}" +} + +log_warn() { + echo -e "${YELLOW}⚠️ $1${NC}" +} + +log_error() { + echo -e "${RED}❌ $1${NC}" +} + +echo "==========================================" +echo "IGNY8 PRODUCTION DEPLOYMENT" +echo "==========================================" +echo "" + +# Safety confirmation +log_warn "This will deploy to PRODUCTION!" +log_warn "Make sure you have tested on staging first." +echo "" +read -p "Type 'deploy' to continue: " confirm +if [[ "${confirm}" != "deploy" ]]; then + log "Deployment cancelled" + exit 0 +fi + +cd "${APP_DIR}" + +# Verify on main branch +CURRENT_BRANCH=$(git branch --show-current) +if [[ "${CURRENT_BRANCH}" != "main" ]]; then + log_error "Not on main branch (currently on: ${CURRENT_BRANCH})" + log "Switch to main: git checkout main && git pull" + exit 1 +fi + +# Step 1: Create pre-deploy backup +log "Step 1/8: Creating pre-deploy backup..." +/data/app/igny8/scripts/ops/backup-db.sh pre-deploy +log_success "Backup created" + +# Step 2: Tag current images for rollback +log "Step 2/8: Tagging current images for rollback..." +docker tag igny8-backend:latest igny8-backend:rollback 2>/dev/null || true +docker tag igny8-frontend-dev:latest igny8-frontend-dev:rollback 2>/dev/null || true +docker tag igny8-marketing-dev:latest igny8-marketing-dev:rollback 2>/dev/null || true +log_success "Rollback images tagged" + +# Step 3: Pull latest code +log "Step 3/8: Pulling latest code..." +git pull origin main +log_success "Code updated" + +# Step 4: Build new images +log "Step 4/8: Building new images..." +docker build -t igny8-backend:latest -f backend/Dockerfile backend/ +docker build -t igny8-frontend-dev:latest -f frontend/Dockerfile.dev frontend/ +docker build -t igny8-marketing-dev:latest -f frontend/Dockerfile.marketing.dev frontend/ +log_success "Images built" + +# Step 5: Check for pending migrations +log "Step 5/8: Checking for pending migrations..." +PENDING=$(docker exec igny8_backend python manage.py showmigrations --plan 2>/dev/null | grep "\[ \]" || true) +if [[ -n "${PENDING}" ]]; then + log_warn "Pending migrations found:" + echo "${PENDING}" + read -p "Apply migrations? (yes/no): " apply_migrations + if [[ "${apply_migrations}" != "yes" ]]; then + log "Skipping migrations" + fi +fi + +# Step 6: Restart containers +log "Step 6/8: Restarting containers..." +docker compose -f "${COMPOSE_FILE}" -p "${PROJECT_NAME}" down +docker compose -f "${COMPOSE_FILE}" -p "${PROJECT_NAME}" up -d + +# Step 7: Wait for backend to be healthy +log "Step 7/8: Waiting for backend to be healthy..." +for i in {1..30}; do + if docker exec igny8_backend python -c "import urllib.request; urllib.request.urlopen('http://localhost:8010/api/v1/system/status/').read()" 2>/dev/null; then + log_success "Backend is healthy" + break + fi + if [[ $i -eq 30 ]]; then + log_error "Backend failed to become healthy" + log_error "ROLLING BACK..." + /data/app/igny8/scripts/ops/rollback.sh --auto + exit 1 + fi + echo -n "." + sleep 2 +done + +# Step 8: Apply migrations and collect static +log "Step 8/8: Applying migrations and collecting static..." +if [[ "${apply_migrations}" == "yes" ]] || [[ -z "${PENDING}" ]]; then + docker exec igny8_backend python manage.py migrate --noinput +fi +docker exec igny8_backend python manage.py collectstatic --noinput + +echo "" +echo "==========================================" +log_success "PRODUCTION DEPLOYMENT COMPLETE" +echo "==========================================" +echo "" +echo "Production is live!" +echo " App: https://app.igny8.com" +echo " API: https://api.igny8.com" +echo "" +echo "Monitor logs for 10 minutes:" +echo " docker compose -f ${COMPOSE_FILE} -p ${PROJECT_NAME} logs -f" +echo "" +echo "If issues occur, rollback:" +echo " /data/app/igny8/scripts/ops/rollback.sh" +echo "" +echo "Container status:" +docker compose -f "${COMPOSE_FILE}" -p "${PROJECT_NAME}" ps +echo "==========================================" diff --git a/scripts/ops/deploy-staging.sh b/scripts/ops/deploy-staging.sh new file mode 100644 index 00000000..7edaaaca --- /dev/null +++ b/scripts/ops/deploy-staging.sh @@ -0,0 +1,125 @@ +#!/bin/bash +# ============================================================================= +# IGNY8 Deploy to Staging +# ============================================================================= +# Safely deploys code to staging environment +# Usage: ./deploy-staging.sh +# ============================================================================= + +set -e + +APP_DIR="/data/app/igny8" +COMPOSE_FILE="docker-compose.staging.yml" +PROJECT_NAME="igny8-staging" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +log() { + echo -e "${BLUE}[$(date '+%H:%M:%S')]${NC} $1" +} + +log_success() { + echo -e "${GREEN}βœ… $1${NC}" +} + +log_warn() { + echo -e "${YELLOW}⚠️ $1${NC}" +} + +log_error() { + echo -e "${RED}❌ $1${NC}" +} + +echo "==========================================" +echo "IGNY8 STAGING DEPLOYMENT" +echo "==========================================" +echo "" + +cd "${APP_DIR}" + +# Check if staging compose file exists +if [[ ! -f "${COMPOSE_FILE}" ]]; then + log_error "Staging compose file not found: ${COMPOSE_FILE}" + log "Please create it from the documentation template" + exit 1 +fi + +# Check if .env.staging exists +if [[ ! -f ".env.staging" ]]; then + log_error "Staging environment file not found: .env.staging" + log "Please create it from .env.example" + exit 1 +fi + +# Step 1: Pull latest code +log "Step 1/6: Pulling latest code..." +git fetch origin +CURRENT_BRANCH=$(git branch --show-current) +log "Current branch: ${CURRENT_BRANCH}" + +if [[ "${CURRENT_BRANCH}" == "staging" ]]; then + git pull origin staging +elif [[ "${CURRENT_BRANCH}" == "main" ]]; then + log_warn "On main branch, switching to staging..." + git checkout staging + git pull origin staging +else + log_warn "On feature branch '${CURRENT_BRANCH}', not pulling..." +fi + +# Step 2: Build staging images +log "Step 2/6: Building staging images..." +docker build -t igny8-backend:staging -f backend/Dockerfile backend/ +docker build -t igny8-frontend-dev:staging -f frontend/Dockerfile.dev frontend/ +docker build -t igny8-marketing-dev:staging -f frontend/Dockerfile.marketing.dev frontend/ +log_success "Images built" + +# Step 3: Stop existing staging containers +log "Step 3/6: Stopping existing staging containers..." +docker compose -f "${COMPOSE_FILE}" -p "${PROJECT_NAME}" down 2>/dev/null || true + +# Step 4: Start staging containers +log "Step 4/6: Starting staging containers..." +docker compose -f "${COMPOSE_FILE}" -p "${PROJECT_NAME}" up -d + +# Step 5: Wait for backend to be healthy +log "Step 5/6: Waiting for backend to be healthy..." +for i in {1..30}; do + if docker exec igny8_staging_backend python -c "import urllib.request; urllib.request.urlopen('http://localhost:8010/api/v1/system/status/').read()" 2>/dev/null; then + log_success "Backend is healthy" + break + fi + if [[ $i -eq 30 ]]; then + log_error "Backend failed to become healthy" + docker logs igny8_staging_backend --tail 50 + exit 1 + fi + echo -n "." + sleep 2 +done + +# Step 6: Run migrations +log "Step 6/6: Running database migrations..." +docker exec igny8_staging_backend python manage.py migrate --noinput +docker exec igny8_staging_backend python manage.py collectstatic --noinput + +echo "" +echo "==========================================" +log_success "STAGING DEPLOYMENT COMPLETE" +echo "==========================================" +echo "" +echo "Staging environment is ready:" +echo " App: https://staging.igny8.com" +echo " API: https://staging-api.igny8.com" +echo "" +echo "View logs:" +echo " docker compose -f ${COMPOSE_FILE} -p ${PROJECT_NAME} logs -f" +echo "" +echo "Container status:" +docker compose -f "${COMPOSE_FILE}" -p "${PROJECT_NAME}" ps +echo "==========================================" diff --git a/scripts/ops/health-check.sh b/scripts/ops/health-check.sh new file mode 100644 index 00000000..49e32ce7 --- /dev/null +++ b/scripts/ops/health-check.sh @@ -0,0 +1,199 @@ +#!/bin/bash +# ============================================================================= +# IGNY8 Health Check Script +# ============================================================================= +# Checks health of all services and reports status +# Usage: ./health-check.sh [--quiet] +# ============================================================================= + +set -e + +QUIET="${1}" +EXIT_CODE=0 + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +# Output functions +output() { + if [[ "${QUIET}" != "--quiet" ]]; then + echo -e "$1" + fi +} + +check_passed() { + output "${GREEN}βœ… $1${NC}" +} + +check_failed() { + output "${RED}❌ $1${NC}" + EXIT_CODE=1 +} + +check_warn() { + output "${YELLOW}⚠️ $1${NC}" +} + +# Check HTTP endpoint +check_endpoint() { + local name="$1" + local url="$2" + local timeout="${3:-10}" + + local response + local http_code + local time_total + + response=$(curl -s -o /dev/null -w "%{http_code}|%{time_total}" --max-time "${timeout}" "${url}" 2>/dev/null || echo "000|0") + http_code=$(echo "${response}" | cut -d'|' -f1) + time_total=$(echo "${response}" | cut -d'|' -f2) + + if [[ "${http_code}" == "200" ]]; then + check_passed "${name}: OK (${time_total}s)" + return 0 + else + check_failed "${name}: FAILED (HTTP ${http_code})" + return 1 + fi +} + +# Check container status +check_container() { + local name="$1" + + if docker ps --format '{{.Names}}' | grep -q "^${name}$"; then + local status=$(docker inspect --format='{{.State.Health.Status}}' "${name}" 2>/dev/null || echo "none") + if [[ "${status}" == "healthy" ]]; then + check_passed "Container ${name}: Running (healthy)" + elif [[ "${status}" == "none" ]]; then + check_passed "Container ${name}: Running (no healthcheck)" + else + check_warn "Container ${name}: Running (${status})" + fi + return 0 + else + check_failed "Container ${name}: NOT RUNNING" + return 1 + fi +} + +# Check disk space +check_disk() { + local path="$1" + local threshold="${2:-80}" + + local usage=$(df "${path}" | tail -1 | awk '{print $5}' | sed 's/%//') + + if [[ ${usage} -lt ${threshold} ]]; then + check_passed "Disk ${path}: ${usage}% used" + elif [[ ${usage} -lt 90 ]]; then + check_warn "Disk ${path}: ${usage}% used (warning)" + else + check_failed "Disk ${path}: ${usage}% used (critical)" + fi +} + +# Check Redis connection +check_redis() { + if docker exec redis redis-cli ping 2>/dev/null | grep -q "PONG"; then + check_passed "Redis: Connected" + return 0 + else + check_failed "Redis: NOT RESPONDING" + return 1 + fi +} + +# Check PostgreSQL connection +check_postgres() { + if docker exec postgres pg_isready -U igny8 2>/dev/null | grep -q "accepting"; then + check_passed "PostgreSQL: Accepting connections" + return 0 + else + check_failed "PostgreSQL: NOT READY" + return 1 + fi +} + +# Check Celery queue +check_celery_queue() { + local queue_length=$(docker exec redis redis-cli llen celery 2>/dev/null || echo "error") + + if [[ "${queue_length}" == "error" ]]; then + check_warn "Celery queue: Cannot check" + elif [[ ${queue_length} -lt 50 ]]; then + check_passed "Celery queue: ${queue_length} tasks" + elif [[ ${queue_length} -lt 200 ]]; then + check_warn "Celery queue: ${queue_length} tasks (high)" + else + check_failed "Celery queue: ${queue_length} tasks (critical)" + fi +} + +# Main health check +main() { + if [[ "${QUIET}" != "--quiet" ]]; then + echo "==========================================" + echo "IGNY8 Health Check" + echo "$(date '+%Y-%m-%d %H:%M:%S')" + echo "==========================================" + echo "" + fi + + # Infrastructure + output "INFRASTRUCTURE:" + check_postgres + check_redis + check_disk "/data" + echo "" + + # Production containers + output "PRODUCTION CONTAINERS:" + check_container "igny8_backend" + check_container "igny8_frontend" + check_container "igny8_celery_worker" + check_container "igny8_celery_beat" + echo "" + + # Production endpoints + output "PRODUCTION ENDPOINTS:" + check_endpoint "API Status" "http://localhost:8011/api/v1/system/status/" 5 + check_endpoint "Frontend" "http://localhost:8021" 5 + echo "" + + # Celery + output "BACKGROUND TASKS:" + check_celery_queue + echo "" + + # Staging (if running) + if docker ps --format '{{.Names}}' | grep -q "igny8_staging_backend"; then + output "STAGING CONTAINERS:" + check_container "igny8_staging_backend" + check_container "igny8_staging_frontend" + echo "" + + output "STAGING ENDPOINTS:" + check_endpoint "Staging API" "http://localhost:8012/api/v1/system/status/" 5 + check_endpoint "Staging Frontend" "http://localhost:8024" 5 + echo "" + fi + + # Summary + if [[ "${QUIET}" != "--quiet" ]]; then + echo "==========================================" + if [[ ${EXIT_CODE} -eq 0 ]]; then + echo -e "${GREEN}All checks passed${NC}" + else + echo -e "${RED}Some checks failed${NC}" + fi + echo "==========================================" + fi + + exit ${EXIT_CODE} +} + +main "$@" diff --git a/scripts/ops/igny8-cron b/scripts/ops/igny8-cron new file mode 100644 index 00000000..054f17f1 --- /dev/null +++ b/scripts/ops/igny8-cron @@ -0,0 +1,49 @@ +# ============================================================================= +# IGNY8 Automated Tasks (Cron Configuration) +# ============================================================================= +# Install: sudo cp igny8-cron /etc/cron.d/igny8 +# Verify: sudo crontab -l -u root +# ============================================================================= + +# Environment +SHELL=/bin/bash +PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin + +# ============================================================================= +# BACKUP JOBS +# ============================================================================= + +# Daily database backup at 1:00 AM +0 1 * * * root /data/app/igny8/scripts/ops/backup-db.sh daily >> /data/logs/backup.log 2>&1 + +# Weekly full backup on Sunday at 2:00 AM +0 2 * * 0 root /data/app/igny8/scripts/ops/backup-full.sh weekly >> /data/logs/backup.log 2>&1 + +# Monthly full backup on 1st of month at 3:00 AM +0 3 1 * * root /data/app/igny8/scripts/ops/backup-full.sh monthly >> /data/logs/backup.log 2>&1 + +# ============================================================================= +# HEALTH & MONITORING +# ============================================================================= + +# Health check every 5 minutes (logs only failures) +*/5 * * * * root /data/app/igny8/scripts/ops/health-check.sh --quiet || echo "[$(date)] Health check failed" >> /data/logs/health.log 2>&1 + +# ============================================================================= +# MAINTENANCE +# ============================================================================= + +# Log rotation daily at midnight +0 0 * * * root /data/app/igny8/scripts/ops/log-rotate.sh >> /data/logs/maintenance.log 2>&1 + +# Docker cleanup weekly on Saturday at 4:00 AM +0 4 * * 6 root docker system prune -f >> /data/logs/maintenance.log 2>&1 + +# ============================================================================= +# OPTIONAL: External backup sync (uncomment if using remote backup) +# ============================================================================= + +# Sync backups to remote storage daily at 5:00 AM +# 0 5 * * * root rsync -avz /data/backups/ user@backup-server:/backups/igny8/ >> /data/logs/backup.log 2>&1 + +# ============================================================================= diff --git a/scripts/ops/log-rotate.sh b/scripts/ops/log-rotate.sh new file mode 100644 index 00000000..fd67ba98 --- /dev/null +++ b/scripts/ops/log-rotate.sh @@ -0,0 +1,108 @@ +#!/bin/bash +# ============================================================================= +# IGNY8 Log Rotation Script +# ============================================================================= +# Rotates and compresses old log files +# Usage: ./log-rotate.sh +# ============================================================================= + +set -e + +LOG_DIR="/data/logs" +ARCHIVE_DIR="${LOG_DIR}/archive" +RETENTION_DAYS=30 + +# Colors +GREEN='\033[0;32m' +BLUE='\033[0;34m' +NC='\033[0m' + +log() { + echo -e "${BLUE}[$(date '+%H:%M:%S')]${NC} $1" +} + +log_success() { + echo -e "${GREEN}βœ… $1${NC}" +} + +echo "==========================================" +echo "IGNY8 Log Rotation" +echo "$(date '+%Y-%m-%d %H:%M:%S')" +echo "==========================================" +echo "" + +# Create archive directory +mkdir -p "${ARCHIVE_DIR}" + +# Rotate function +rotate_log() { + local log_file="$1" + local max_size="${2:-50M}" + + if [[ ! -f "${log_file}" ]]; then + return + fi + + local size=$(stat -f%z "${log_file}" 2>/dev/null || stat -c%s "${log_file}" 2>/dev/null || echo 0) + local max_bytes=$((50 * 1024 * 1024)) # 50MB + + if [[ ${size} -gt ${max_bytes} ]]; then + local basename=$(basename "${log_file}") + local timestamp=$(date +%Y%m%d_%H%M%S) + local archive_file="${ARCHIVE_DIR}/${basename}.${timestamp}.gz" + + log "Rotating ${log_file} ($(numfmt --to=iec ${size}))" + gzip -c "${log_file}" > "${archive_file}" + > "${log_file}" # Truncate original + log_success "Archived to ${archive_file}" + fi +} + +# Rotate production logs +log "Checking production logs..." +for log_file in "${LOG_DIR}/production"/*.log; do + [[ -f "${log_file}" ]] && rotate_log "${log_file}" +done + +# Rotate staging logs +log "Checking staging logs..." +for log_file in "${LOG_DIR}/staging"/*.log; do + [[ -f "${log_file}" ]] && rotate_log "${log_file}" +done + +# Rotate Caddy logs +log "Checking Caddy logs..." +for log_file in "${LOG_DIR}/caddy"/*.log; do + [[ -f "${log_file}" ]] && rotate_log "${log_file}" +done + +# Clean up old archives +log "Cleaning old archives (> ${RETENTION_DAYS} days)..." +DELETED=$(find "${ARCHIVE_DIR}" -name "*.gz" -mtime +${RETENTION_DAYS} -delete -print | wc -l) +if [[ ${DELETED} -gt 0 ]]; then + log "Deleted ${DELETED} old archive files" +fi + +# Docker container logs +log "Pruning Docker container logs..." +for container in igny8_backend igny8_frontend igny8_celery_worker igny8_celery_beat; do + if docker ps -a --format '{{.Names}}' | grep -q "^${container}$"; then + LOG_FILE=$(docker inspect --format='{{.LogPath}}' "${container}" 2>/dev/null || true) + if [[ -n "${LOG_FILE}" ]] && [[ -f "${LOG_FILE}" ]]; then + local size=$(stat -c%s "${LOG_FILE}" 2>/dev/null || echo 0) + if [[ ${size} -gt $((100 * 1024 * 1024)) ]]; then # 100MB + log "Truncating Docker log for ${container}" + truncate -s 0 "${LOG_FILE}" + fi + fi + fi +done + +# Report disk usage +echo "" +echo "Log disk usage:" +du -sh "${LOG_DIR}"/* 2>/dev/null || echo "No logs found" + +echo "" +log_success "Log rotation complete" +echo "==========================================" diff --git a/scripts/ops/restore-db.sh b/scripts/ops/restore-db.sh new file mode 100644 index 00000000..ea2e15ec --- /dev/null +++ b/scripts/ops/restore-db.sh @@ -0,0 +1,142 @@ +#!/bin/bash +# ============================================================================= +# IGNY8 Database Restore Script +# ============================================================================= +# Usage: ./restore-db.sh [target_db] +# Restores database from a backup file (.sql or .sql.gz) +# ============================================================================= + +set -e + +# Configuration +BACKUP_FILE="${1}" +TARGET_DB="${2:-igny8_db}" +DB_CONTAINER="postgres" +DB_USER="igny8" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +log() { + echo -e "[$(date '+%Y-%m-%d %H:%M:%S')] $1" +} + +log_success() { + log "${GREEN}βœ… $1${NC}" +} + +log_warn() { + log "${YELLOW}⚠️ $1${NC}" +} + +log_error() { + log "${RED}❌ $1${NC}" +} + +# Validate input +if [[ -z "${BACKUP_FILE}" ]]; then + echo "Usage: ./restore-db.sh [target_db]" + echo "" + echo "Arguments:" + echo " backup_file Path to backup file (.sql or .sql.gz)" + echo " target_db Target database (default: igny8_db)" + echo "" + echo "Available backups:" + ls -la /data/backups/latest_db.sql.gz 2>/dev/null || echo " No latest backup found" + echo "" + find /data/backups -name "*.sql.gz" -type f 2>/dev/null | head -10 + exit 1 +fi + +if [[ ! -f "${BACKUP_FILE}" ]]; then + log_error "Backup file not found: ${BACKUP_FILE}" + exit 1 +fi + +echo "==========================================" +echo "IGNY8 Database Restore" +echo "==========================================" +echo "" +echo "Backup file: ${BACKUP_FILE}" +echo "Target DB: ${TARGET_DB}" +echo "" + +# Safety confirmation +log_warn "This will REPLACE the database '${TARGET_DB}' with backup data!" +read -p "Are you sure? Type 'yes' to continue: " confirm +if [[ "${confirm}" != "yes" ]]; then + log "Restore cancelled" + exit 0 +fi + +# Check if postgres container is running +if ! docker ps --format '{{.Names}}' | grep -q "^${DB_CONTAINER}$"; then + log_error "PostgreSQL container '${DB_CONTAINER}' is not running" + exit 1 +fi + +# Create pre-restore backup +log "Creating pre-restore backup of current database..." +PRE_RESTORE_BACKUP="/data/backups/pre-restore_${TARGET_DB}_$(date +%Y%m%d_%H%M%S).sql.gz" +if docker exec "${DB_CONTAINER}" pg_dump -U "${DB_USER}" "${TARGET_DB}" 2>/dev/null | gzip > "${PRE_RESTORE_BACKUP}"; then + log_success "Pre-restore backup: ${PRE_RESTORE_BACKUP}" +else + log_warn "Could not create pre-restore backup (database might not exist)" +fi + +# Stop application containers to prevent connections +log "Stopping application containers..." +docker compose -f /data/app/igny8/docker-compose.app.yml -p igny8-app stop igny8_backend igny8_celery_worker igny8_celery_beat 2>/dev/null || true + +# Drop and recreate database +log "Dropping existing database..." +docker exec "${DB_CONTAINER}" psql -U postgres -c "DROP DATABASE IF EXISTS ${TARGET_DB};" || true + +log "Creating fresh database..." +docker exec "${DB_CONTAINER}" psql -U postgres -c "CREATE DATABASE ${TARGET_DB} OWNER ${DB_USER};" + +# Restore from backup +log "Restoring database from backup..." +if [[ "${BACKUP_FILE}" == *.gz ]]; then + # Compressed backup + gunzip -c "${BACKUP_FILE}" | docker exec -i "${DB_CONTAINER}" psql -U "${DB_USER}" -d "${TARGET_DB}" +else + # Uncompressed backup + docker exec -i "${DB_CONTAINER}" psql -U "${DB_USER}" -d "${TARGET_DB}" < "${BACKUP_FILE}" +fi + +if [[ $? -eq 0 ]]; then + log_success "Database restore complete" +else + log_error "Database restore failed" + exit 1 +fi + +# Restart application containers +log "Restarting application containers..." +docker compose -f /data/app/igny8/docker-compose.app.yml -p igny8-app start igny8_backend igny8_celery_worker igny8_celery_beat 2>/dev/null || true + +# Wait for backend to be healthy +log "Waiting for backend to be healthy..." +sleep 10 + +# Verify restoration +log "Verifying database..." +TABLE_COUNT=$(docker exec "${DB_CONTAINER}" psql -U "${DB_USER}" -d "${TARGET_DB}" -t -c "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public';") +log_success "Database restored with ${TABLE_COUNT} tables" + +echo "" +echo "==========================================" +echo "RESTORE COMPLETE" +echo "==========================================" +echo "Database '${TARGET_DB}' has been restored" +echo "" +echo "Pre-restore backup saved to:" +echo " ${PRE_RESTORE_BACKUP}" +echo "" +echo "To rollback this restore, run:" +echo " ./restore-db.sh ${PRE_RESTORE_BACKUP} ${TARGET_DB}" +echo "==========================================" diff --git a/scripts/ops/rollback.sh b/scripts/ops/rollback.sh new file mode 100644 index 00000000..3016eb7e --- /dev/null +++ b/scripts/ops/rollback.sh @@ -0,0 +1,108 @@ +#!/bin/bash +# ============================================================================= +# IGNY8 Rollback Script +# ============================================================================= +# Rolls back production to previous version using tagged images +# Usage: ./rollback.sh [--auto] +# ============================================================================= + +set -e + +APP_DIR="/data/app/igny8" +COMPOSE_FILE="docker-compose.app.yml" +PROJECT_NAME="igny8-app" +AUTO_MODE="${1}" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +log() { + echo -e "${BLUE}[$(date '+%H:%M:%S')]${NC} $1" +} + +log_success() { + echo -e "${GREEN}βœ… $1${NC}" +} + +log_warn() { + echo -e "${YELLOW}⚠️ $1${NC}" +} + +log_error() { + echo -e "${RED}❌ $1${NC}" +} + +echo "==========================================" +echo "IGNY8 PRODUCTION ROLLBACK" +echo "==========================================" +echo "" + +# Check for rollback images +if ! docker image inspect igny8-backend:rollback &>/dev/null; then + log_error "No rollback image found!" + log "Rollback images are created during deployment." + log "To restore from backup, use:" + log " /data/app/igny8/scripts/ops/restore-db.sh /data/backups/latest_db.sql.gz" + exit 1 +fi + +# Confirmation (unless auto mode) +if [[ "${AUTO_MODE}" != "--auto" ]]; then + log_warn "This will rollback production to the previous version!" + read -p "Type 'rollback' to continue: " confirm + if [[ "${confirm}" != "rollback" ]]; then + log "Rollback cancelled" + exit 0 + fi +fi + +cd "${APP_DIR}" + +# Step 1: Stop current containers +log "Step 1/4: Stopping current containers..." +docker compose -f "${COMPOSE_FILE}" -p "${PROJECT_NAME}" down + +# Step 2: Restore rollback images +log "Step 2/4: Restoring previous images..." +docker tag igny8-backend:rollback igny8-backend:latest +docker tag igny8-frontend-dev:rollback igny8-frontend-dev:latest +docker tag igny8-marketing-dev:rollback igny8-marketing-dev:latest 2>/dev/null || true +log_success "Images restored" + +# Step 3: Start containers +log "Step 3/4: Starting containers with previous version..." +docker compose -f "${COMPOSE_FILE}" -p "${PROJECT_NAME}" up -d + +# Step 4: Wait for backend +log "Step 4/4: Waiting for backend to be healthy..." +for i in {1..30}; do + if docker exec igny8_backend python -c "import urllib.request; urllib.request.urlopen('http://localhost:8010/api/v1/system/status/').read()" 2>/dev/null; then + log_success "Backend is healthy" + break + fi + if [[ $i -eq 30 ]]; then + log_error "Backend failed to start after rollback" + log "Manual intervention required!" + exit 1 + fi + echo -n "." + sleep 2 +done + +echo "" +echo "==========================================" +log_success "ROLLBACK COMPLETE" +echo "==========================================" +echo "" +echo "Production is running on previous version." +echo "" +echo "If database rollback is also needed:" +echo " /data/app/igny8/scripts/ops/restore-db.sh /data/backups/pre-deploy/LATEST/db_igny8_*.sql.gz" +echo "" +echo "Container status:" +docker compose -f "${COMPOSE_FILE}" -p "${PROJECT_NAME}" ps +echo "==========================================" diff --git a/scripts/ops/sync-prod-to-staging.sh b/scripts/ops/sync-prod-to-staging.sh new file mode 100644 index 00000000..8e5ec0a9 --- /dev/null +++ b/scripts/ops/sync-prod-to-staging.sh @@ -0,0 +1,127 @@ +#!/bin/bash +# ============================================================================= +# IGNY8 Sync Production Data to Staging +# ============================================================================= +# Syncs production database to staging with data sanitization +# Usage: ./sync-prod-to-staging.sh +# ============================================================================= + +set -e + +DB_CONTAINER="postgres" +PROD_DB="igny8_db" +STAGING_DB="igny8_staging_db" +DB_USER="igny8" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +log() { + echo -e "${BLUE}[$(date '+%H:%M:%S')]${NC} $1" +} + +log_success() { + echo -e "${GREEN}βœ… $1${NC}" +} + +log_warn() { + echo -e "${YELLOW}⚠️ $1${NC}" +} + +log_error() { + echo -e "${RED}❌ $1${NC}" +} + +echo "==========================================" +echo "SYNC PRODUCTION β†’ STAGING" +echo "==========================================" +echo "" + +log_warn "This will REPLACE staging database with production data!" +log_warn "Data will be sanitized (emails, passwords anonymized)" +echo "" +read -p "Type 'sync' to continue: " confirm +if [[ "${confirm}" != "sync" ]]; then + log "Sync cancelled" + exit 0 +fi + +# Check if staging containers are running +STAGING_RUNNING=$(docker ps --format '{{.Names}}' | grep -c "igny8_staging" || true) +if [[ ${STAGING_RUNNING} -gt 0 ]]; then + log "Stopping staging containers..." + docker compose -f /data/app/igny8/docker-compose.staging.yml -p igny8-staging stop igny8_staging_backend igny8_staging_celery_worker igny8_staging_celery_beat 2>/dev/null || true +fi + +# Step 1: Dump production +log "Step 1/5: Dumping production database..." +TEMP_DUMP="/tmp/prod_sync_$(date +%Y%m%d_%H%M%S).sql" +docker exec "${DB_CONTAINER}" pg_dump -U "${DB_USER}" "${PROD_DB}" > "${TEMP_DUMP}" +log_success "Production dump created: ${TEMP_DUMP}" + +# Step 2: Drop staging database +log "Step 2/5: Dropping staging database..." +docker exec "${DB_CONTAINER}" psql -U postgres -c "DROP DATABASE IF EXISTS ${STAGING_DB};" +docker exec "${DB_CONTAINER}" psql -U postgres -c "CREATE DATABASE ${STAGING_DB} OWNER ${DB_USER};" +log_success "Staging database recreated" + +# Step 3: Restore to staging +log "Step 3/5: Restoring to staging database..." +docker exec -i "${DB_CONTAINER}" psql -U "${DB_USER}" -d "${STAGING_DB}" < "${TEMP_DUMP}" +log_success "Data restored to staging" + +# Step 4: Sanitize data +log "Step 4/5: Sanitizing staging data..." +docker exec "${DB_CONTAINER}" psql -U "${DB_USER}" -d "${STAGING_DB}" << 'EOF' +-- Sanitize user emails (keep superusers intact) +UPDATE auth_user +SET email = CONCAT('staging_', id, '@igny8.test') +WHERE is_superuser = FALSE; + +-- Invalidate all sessions +DELETE FROM django_session; + +-- Update notification endpoints to staging +UPDATE notification_settings +SET webhook_url = REPLACE(webhook_url, 'app.igny8.com', 'staging.igny8.com') +WHERE webhook_url IS NOT NULL; + +-- Log sanitization +SELECT 'Sanitized ' || COUNT(*) || ' user emails' FROM auth_user WHERE email LIKE '%@igny8.test'; +EOF +log_success "Data sanitized" + +# Step 5: Run migrations (in case staging has newer schema) +log "Step 5/5: Checking migrations..." +if docker ps --format '{{.Names}}' | grep -q "igny8_staging_backend"; then + docker exec igny8_staging_backend python manage.py migrate --noinput 2>/dev/null || true +fi + +# Cleanup +rm -f "${TEMP_DUMP}" + +# Restart staging +if [[ ${STAGING_RUNNING} -gt 0 ]]; then + log "Restarting staging containers..." + docker compose -f /data/app/igny8/docker-compose.staging.yml -p igny8-staging start igny8_staging_backend igny8_staging_celery_worker igny8_staging_celery_beat 2>/dev/null || true +fi + +echo "" +echo "==========================================" +log_success "SYNC COMPLETE" +echo "==========================================" +echo "" +echo "Staging database now contains sanitized production data." +echo "" +echo "Changes made:" +echo " - User emails changed to staging_*@igny8.test" +echo " - All sessions invalidated" +echo " - Webhook URLs updated to staging domain" +echo "" +echo "To test with a specific user:" +echo " docker exec -it igny8_staging_backend python manage.py changepassword " +echo "=========================================="