# IGNY8 Phase 0: Staging Environment Setup (Doc 00D) **Document Status:** Build Specification **Version:** 2.1 **Date Created:** 2026-03-23 **Target Phase:** Phase 0 - Infrastructure & Deployment **Source of Truth:** Codebase at `/data/app/igny8/` **Related Docs:** [00B Infrastructure Setup](00B-infrastructure-setup.md) | [00C Production Migration](00C-production-migration.md) **Key Details:** - Staging runs on the NEW VPS (from 00B Infrastructure Setup) - Separate database: `igny8_staging_db` on PostgreSQL 18 instance - Redis DB 1 (production uses DB 0) - Separate ports: Backend 8012, Frontend 8024, Marketing 8026 - Separate compose file: `docker-compose.staging.yml` with project name `igny8-staging` - DNS coordination with 00C 3-stage migration flow --- ## 1. Current State **Staging Environment Location:** On the NEW VPS, as provisioned in 00B Infrastructure Setup. **Note on Versions:** For component versions, the codebase (requirements.txt, package.json, Dockerfiles) is the source of truth. Aspirational upgrade targets are in 00B but current verified versions are: Python 3.11, Django >=5.2.7, Node 18, React ^19, Vite ^6.1.0, Celery >=5.3.0. ### 1.1 Infrastructure Baseline - **Host Server:** Single Linux VM running Docker on NEW VPS (from 00B Infrastructure Setup) - **Base OS:** Ubuntu (version per 00B) - **Shared Resources:** - PostgreSQL server (port 5432) — version set by infra stack - Redis server (port 6379) — version set by infra stack - Docker network: `igny8_net` - Caddy reverse proxy (port 80/443) - Cloudflare DNS management (may or may not be active — dependent on 00C flow stage) - Log directory: `/data/app/logs/` (production), `/data/logs/staging/` (staging) ### 1.2 Production Environment (Already Complete - Doc 00C) - **Database:** `igny8_db` (PostgreSQL) - **Cache:** Redis DB 0 - **Compose file:** `docker-compose.app.yml` (project name: `igny8-app`) - **Containers (7):** - `igny8_backend` (host port 8011, container port 8010) - `igny8_frontend` (host port 8021, container port 5173) - `igny8_marketing_dev` (host port 8023, container port 5174) - `igny8_celery_worker` - `igny8_celery_beat` - `igny8_flower` (port 5555) - **Env file:** `.env` (production settings) — NOT used inline; env vars set in compose - **Domains:** igny8.com, api.igny8.com, marketing.igny8.com - **Logs:** `/data/app/logs/` ### 1.3 Staging Environment (To Be Built) **Does not yet exist.** This document defines the complete staging setup. --- ## 2. What to Build ### 2.1 Staging Environment Architecture A complete parallel environment sharing infrastructure with production: ``` ┌─────────────────────────────────────────────────────┐ │ Docker Containers (Staging) │ ├─────────────────────────────────────────────────────┤ │ │ │ igny8_staging_backend:8012 → :8010 (Django/Gunicorn) │ │ igny8_staging_frontend:8024 → :5173 (React/Vite) │ │ igny8_staging_marketing_dev:8026 → :5174 (Vite) │ │ igny8_staging_celery_worker │ │ igny8_staging_celery_beat │ │ │ └─────────────────────────────────────────────────────┘ ↓ igny8_net (Docker network) ↓ ┌─────────────────────────────────────────────────────┐ │ Shared Infrastructure Services │ ├─────────────────────────────────────────────────────┤ │ │ │ PostgreSQL 18 (port 5432) │ │ ├── igny8_db (production) │ │ └── igny8_staging_db (staging) │ │ │ │ Redis 8 (port 6379) │ │ ├── DB 0 (production) │ │ └── DB 1 (staging) │ │ │ │ Caddy 2.11 (port 80/443) → Routes staging domains│ │ │ └─────────────────────────────────────────────────────┘ ↓ Cloudflare DNS → staging.igny8.com api.staging.igny8.com marketing.staging.igny8.com ``` ### 2.2 Deliverables 1. Staging PostgreSQL database (`igny8_staging_db`) 2. Docker Compose file: `docker-compose.staging.yml` 3. Environment file: `.env.staging` 4. Caddyfile additions for staging route configuration 5. Cloudflare DNS records for staging subdomains 6. Staging Docker images (tagged `:staging`) 7. Deployment script: `deploy-staging.sh` 8. Sync script: `sync-prod-to-staging.sh` 9. Logs directory: `/data/app/logs/staging/` ### 2.3 Key Characteristics - **Independent Databases:** Separate PostgreSQL DB and Redis DB - **Shared Infrastructure:** Same PostgreSQL server, Redis server, Docker network, Caddy - **Isolated Configuration:** Separate env file with test credentials, sandbox API keys - **Feature Branch Testing:** `staging` branch receives feature PRs before merging to `main` - **Production Data Sync:** Ability to copy production data to staging for realistic testing - **Domain Isolation:** Staging domains clearly identified (staging subdomain) --- ## 3. Data Models / APIs ### 3.1 PostgreSQL Database Setup **PostgreSQL Version:** PostgreSQL 18 (postgres:18-alpine from 00B Version Matrix) **Database Name:** `igny8_staging_db` **Owner:** `igny8_user` (same as production) **Encoding:** UTF8 **Locale:** Same as production **Extensions:** Same as production (uuid-ossp, etc.) **Schema:** Identical structure to production (migrations auto-generate on first startup) ### 3.2 Redis Setup **Redis Version:** Redis 8 (redis:8-alpine from 00B Version Matrix) **Instance:** Same Redis server (port 6379) **Database:** DB 1 (production uses DB 0) **Prefix:** `staging:` (for key namespacing) **Persistence:** Same RDB/AOF configuration as production ### 3.3 Environment Variables **DJANGO_ENV:** `staging` **DEBUG:** `True` (staging allows debug mode for testing) **ALLOWED_HOSTS:** `staging.igny8.com,staging-api.igny8.com,localhost,127.0.0.1` **API_KEYS:** Test sandbox keys (different from production) **PAYMENT_GATEWAY:** Sandbox credentials (Stripe test keys, etc.) **CELERY:** Routes to staging Redis DB 1 **CACHE:** Routes to staging Redis DB 1 **CORS:** Allows staging frontend origins ### 3.4 API Endpoints | Component | Production | Staging | |-----------|-----------|---------| | Frontend | `igny8.com` | `staging.igny8.com` | | Backend API | `api.igny8.com` | `staging-api.igny8.com` | | Marketing | `marketing.igny8.com` | `staging-marketing.igny8.com` | --- ## 4. Implementation Steps **Version Requirements:** All versions below are verified from the codebase. The staging environment uses identical versions to production: - PostgreSQL (version set by infra stack) - Redis (version set by infra stack) - Caddy 2 (version set by infra stack) - Ubuntu base OS (set by infra stack) - Docker Engine (installed on VPS) - Python 3.11-slim (in backend Dockerfile) - Node 18-alpine (in frontend Dockerfile) - Django >=5.2.7 (requirements.txt) - Vite ^6.1.0 (package.json) - Gunicorn (requirements.txt) - Celery >=5.3.0 (requirements.txt) **Note:** The actual `docker-compose.staging.yml` already exists in the repo and is the source of truth. The compose excerpt below is for reference only — always use the actual file. ### Step 1: Create Staging PostgreSQL Database **File:** SQL commands to run on the host PostgreSQL server ```sql -- Connect as superuser psql -U postgres -h localhost -- Create staging database CREATE DATABASE igny8_staging_db WITH OWNER igny8_user ENCODING 'UTF8' LOCALE 'en_US.UTF-8' TEMPLATE template0; -- Verify creation \l | grep igny8 -- Grant privileges GRANT ALL PRIVILEGES ON DATABASE igny8_staging_db TO igny8_user; GRANT ALL PRIVILEGES ON SCHEMA public TO igny8_user; ``` **Execution:** ```bash # On host server docker exec -i postgres psql -U postgres -d postgres << 'EOF' CREATE DATABASE igny8_staging_db WITH OWNER igny8 ENCODING 'UTF8' LOCALE 'en_US.UTF-8' TEMPLATE template0; GRANT ALL PRIVILEGES ON DATABASE igny8_staging_db TO igny8; GRANT ALL PRIVILEGES ON SCHEMA public TO igny8; EOF echo "Staging database created" ``` --- ### Step 2: Create `docker-compose.staging.yml` **Location:** Root of project directory (alongside `docker-compose.yml`) **Project Name:** `igny8-staging` ```yaml # Actual file: docker-compose.staging.yml (already exists in repo) # Key differences from this reference: the actual file uses env_file: .env.staging # and individual env vars (DB_HOST, DB_NAME) rather than DATABASE_URL format. name: igny8-staging services: # Backend API Service 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 # External infra container name (NOT igny8_postgres) DB_NAME: igny8_staging_db DB_USER: igny8 DB_PASSWORD: igny8pass REDIS_HOST: redis # External infra container name (NOT igny8_redis) REDIS_PORT: "6379" REDIS_DB: "1" # DB 1 for staging (production uses DB 0) 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] # Frontend Service (React + Vite) 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] # Marketing Site Service (Vite, NOT Nuxt — built from frontend/Dockerfile.marketing.dev) 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 # Same frontend dir — marketing is a Vite build mode networks: [igny8_net] # Celery Worker 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] # Celery Beat (Scheduler) 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] networks: igny8_net: external: true ``` > **Note:** The actual `docker-compose.staging.yml` in the repo is the definitive version. The above is aligned with it as of this writing. --- ### Step 3: Create `.env.staging` **Location:** Root of project directory **Purpose:** Staging-specific environment variables ```bash # ============================================================================== # IGNY8 STAGING ENVIRONMENT CONFIGURATION # ============================================================================== # ============================================================================== # GENERAL # ============================================================================== ENVIRONMENT=staging DJANGO_ENV=staging DEBUG=True LOG_LEVEL=INFO # ============================================================================== # SECRETS (STAGING VERSIONS) # ============================================================================== # Generate new secrets for staging (do NOT copy from production) STAGING_SECRET_KEY=your-generated-staging-secret-key-here-minimum-50-chars-random DB_PASSWORD=your-staging-db-password # ============================================================================== # DATABASE # ============================================================================== DATABASE_ENGINE=postgresql DATABASE_HOST=postgres DATABASE_PORT=5432 DATABASE_NAME=igny8_staging_db DATABASE_USER=igny8 DATABASE_PASSWORD=${DB_PASSWORD} # ============================================================================== # CACHE & QUEUE (REDIS DB 1 - Separate from Production) # ============================================================================== REDIS_HOST=redis REDIS_PORT=6379 REDIS_DB=1 REDIS_PASSWORD= REDIS_URL=redis://redis:6379/1 CACHE_URL=redis://redis:6379/1 # ============================================================================== # CELERY (Uses Redis DB 1) # ============================================================================== CELERY_BROKER_URL=redis://redis:6379/1 CELERY_RESULT_BACKEND=redis://redis:6379/1 CELERY_ACCEPT_CONTENT=json CELERY_TASK_SERIALIZER=json # ============================================================================== # HOSTS & DOMAINS (STAGING) # ============================================================================== ALLOWED_HOSTS=staging.igny8.com,staging-api.igny8.com,localhost,127.0.0.1 FRONTEND_URL=https://staging.igny8.com API_BASE_URL=https://staging-api.igny8.com MARKETING_URL=https://staging-marketing.igny8.com CORS_ALLOWED_ORIGINS=https://staging.igny8.com,https://staging-marketing.igny8.com,http://localhost:8024,http://localhost:8026 # ============================================================================== # PAYMENT GATEWAY (STRIPE - SANDBOX) # ============================================================================== STAGING_STRIPE_PUBLIC_KEY=pk_test_your_staging_stripe_public_key STAGING_STRIPE_SECRET_KEY=sk_test_your_staging_stripe_secret_key STAGING_STRIPE_WEBHOOK_SECRET=whsec_your_staging_webhook_secret # ============================================================================== # AWS / S3 (STAGING BUCKET) # ============================================================================== STAGING_AWS_ACCESS_KEY_ID=your-staging-aws-access-key STAGING_AWS_SECRET_ACCESS_KEY=your-staging-aws-secret-key STAGING_AWS_S3_BUCKET=igny8-staging-assets AWS_REGION=us-east-1 AWS_S3_CUSTOM_DOMAIN=staging-assets.igny8.com # ============================================================================== # EXTERNAL SERVICES (STAGING / SANDBOX CREDENTIALS) # ============================================================================== # Email (app uses Resend, not Mailgun) # RESEND_API_KEY=your-staging-resend-key # Error Tracking (optional) # SENTRY_DSN=https://your-staging-sentry-dsn # ============================================================================== # SECURITY (STAGING) # ============================================================================== SECURE_SSL_REDIRECT=True SESSION_COOKIE_SECURE=True CSRF_COOKIE_SECURE=True SECURE_HSTS_SECONDS=31536000 SECURE_HSTS_INCLUDE_SUBDOMAINS=True # ============================================================================== # DJANGO SETTINGS # ============================================================================== DJANGO_SUPERUSER_USERNAME=admin DJANGO_SUPERUSER_EMAIL=admin@staging.igny8.com DJANGO_SUPERUSER_PASSWORD=your-staging-admin-password # ============================================================================== # FRONTEND (React + Vite) # ============================================================================== VITE_BACKEND_URL=https://staging-api.igny8.com/api VITE_ENV=staging ``` --- ### Step 4: Update Caddyfile for Staging Routes **Location:** `/data/caddy/Caddyfile` (append to existing production configuration) ```caddy # ============================================================================== # STAGING ENVIRONMENT ROUTES # ============================================================================== # Staging API Backend staging-api.igny8.com { reverse_proxy igny8_staging_backend:8010 { header_uri X-Forwarded-Proto https header_uri X-Forwarded-Host {host} } log { output file /data/app/logs/staging/staging-api-access.log { roll_size 100mb roll_keep 10 } } encode gzip } # Staging Frontend staging.igny8.com { reverse_proxy igny8_staging_frontend:5173 { header_uri X-Forwarded-Proto https header_uri X-Forwarded-Host {host} } log { output file /data/app/logs/staging/staging-frontend-access.log { roll_size 100mb roll_keep 10 } } encode gzip } # Staging Marketing Site staging-marketing.igny8.com { reverse_proxy igny8_staging_marketing_dev:5174 { header_uri X-Forwarded-Proto https header_uri X-Forwarded-Host {host} } log { output file /data/app/logs/staging/staging-marketing-access.log { roll_size 100mb roll_keep 10 } } encode gzip } ``` **Reload Caddy:** ```bash # On host docker exec igny8_caddy caddy reload --config /etc/caddy/Caddyfile ``` --- ### Step 5: Create DNS Records for Staging Subdomains **DNS Provider Timing:** Staging setup is on the NEW VPS (from 00B). This assumes DNS infrastructure is at one of the stages in the 00C 3-stage migration flow: - **00C Stage 1 (DNS Preparation):** Old provider still active - use old provider for staging DNS - **00C Stage 2 (DNS Flip):** Migrating to new provider - add staging records to new provider - **00C Stage 3 (Cloudflare Onboarding):** Cloudflare now active - add staging records to Cloudflare **Current DNS Provider:** Use whichever DNS provider is currently managing igny8.com per the 00C flow: - If 00C Stage 3 complete (Cloudflare active): Use Cloudflare - If 00C Stage 2 in progress or complete: Use the new DNS provider - Otherwise: Use the current active DNS provider **Note on Staging Domains:** Staging may use test variants (e.g., `test-staging.igny8.com`) during the migration period if production domains are already in use. Coordinate with 00C DNS flow to avoid conflicts. **Add the following DNS records to your active provider:** | Type | Name | Content | TTL | Proxy | |------|------|---------|-----|-------| | CNAME | staging | igny8.com | 3600 | Proxied (if Cloudflare) | | CNAME | staging-api | igny8.com | 3600 | Proxied (if Cloudflare) | | CNAME | staging-marketing | igny8.com | 3600 | Proxied (if Cloudflare) | **SSL/TLS Setup:** - If using Cloudflare: Ensure staging subdomains are included in SSL certificate (auto-included if using wildcard or zone certificate) - If using another DNS provider: Ensure Caddy can obtain certificates for staging subdomains (will auto-issue via ACME) **Wait for propagation:** DNS changes typically propagate within 5-10 minutes. --- ### Step 6: Build Staging Docker Images **Backend Image** ```bash cd /data/app/igny8/backend docker build -f Dockerfile -t igny8-backend:staging . # Verify docker images | grep igny8-backend ``` **Frontend Image (React/Vite)** ```bash cd /data/app/igny8/frontend docker build -f Dockerfile.dev -t igny8-frontend-dev:staging . # Verify docker images | grep igny8-frontend-dev ``` **Marketing Image (built from frontend dir using Dockerfile.marketing.dev)** ```bash cd /data/app/igny8/frontend docker build -f Dockerfile.marketing.dev -t igny8-marketing-dev:staging . # Verify docker images | grep igny8-marketing-dev ``` **Tag images for registry (optional, if using Docker Hub/ECR):** ```bash # Example: tag for Docker Hub docker tag igny8-backend:staging yourregistry/igny8-backend:staging docker tag igny8-frontend-dev:staging yourregistry/igny8-frontend-dev:staging docker tag igny8-marketing-dev:staging yourregistry/igny8-marketing-dev:staging # Push to registry docker push yourregistry/igny8-backend:staging docker push yourregistry/igny8-frontend-dev:staging docker push yourregistry/igny8-marketing-dev:staging ``` --- ### Step 7: Create Logs Directory ```bash # On host server sudo mkdir -p /data/app/logs/staging sudo chown -R 1000:1000 /data/app/logs/staging sudo chmod 755 /data/app/logs/staging ``` --- ### Step 8: Create `deploy-staging.sh` Script **Location:** Root project directory or `scripts/deploy-staging.sh` ```bash #!/bin/bash # ============================================================================== # IGNY8 Staging Environment Deployment Script # ============================================================================== # Usage: ./deploy-staging.sh [pull-images] # ============================================================================== set -e # Exit on error # Configuration PROJECT_NAME="igny8-staging" COMPOSE_FILE="docker-compose.staging.yml" ENV_FILE=".env.staging" LOG_DIR="/data/app/logs/staging" PROD_COMPOSE_FILE="docker-compose.app.yml" PROD_ENV_FILE=".env" # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # ============================================================================== # FUNCTIONS # ============================================================================== log_info() { echo -e "${BLUE}[INFO]${NC} $1" } log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1" } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1" } log_error() { echo -e "${RED}[ERROR]${NC} $1" } check_prerequisites() { log_info "Checking prerequisites..." if ! command -v docker &> /dev/null; then log_error "Docker not installed" exit 1 fi if ! command -v docker-compose &> /dev/null; then log_error "docker-compose not installed" exit 1 fi if [ ! -f "$COMPOSE_FILE" ]; then log_error "File not found: $COMPOSE_FILE" exit 1 fi if [ ! -f "$ENV_FILE" ]; then log_warn "File not found: $ENV_FILE" log_info "Creating $ENV_FILE from template..." # You could create a template here exit 1 fi log_success "Prerequisites check passed" } verify_network() { log_info "Verifying Docker network..." if ! docker network inspect igny8_net &> /dev/null; then log_error "Docker network 'igny8_net' not found" log_info "Expected to be created by production setup (doc 00B)" exit 1 fi log_success "Docker network verified" } verify_shared_services() { log_info "Verifying shared infrastructure services..." # Check PostgreSQL if ! docker exec postgres pg_isready -U postgres &> /dev/null; then log_error "PostgreSQL service not running" exit 1 fi log_success "PostgreSQL verified" # Check Redis if ! docker exec redis redis-cli ping &> /dev/null; then log_error "Redis service not running" exit 1 fi log_success "Redis verified" # Check Caddy if ! docker ps | grep -q caddy; then log_error "Caddy service not running" exit 1 fi log_success "Caddy verified" } create_staging_database() { log_info "Creating staging database..." # Check if database already exists if docker exec postgres psql -U postgres -lqt | cut -d \| -f 1 | grep -qw igny8_staging_db; then log_warn "Database 'igny8_staging_db' already exists, skipping creation" return 0 fi docker exec -i postgres psql -U postgres -d postgres << 'EOF' CREATE DATABASE igny8_staging_db WITH OWNER igny8 ENCODING 'UTF8' LOCALE 'en_US.UTF-8' TEMPLATE template0; GRANT ALL PRIVILEGES ON DATABASE igny8_staging_db TO igny8; GRANT ALL PRIVILEGES ON SCHEMA public TO igny8; EOF log_success "Staging database created" } pull_images() { log_info "Pulling latest staging images..." docker pull igny8-backend:staging 2>/dev/null || log_warn "Could not pull igny8-backend:staging (may not exist in registry yet)" docker pull igny8-frontend-dev:staging 2>/dev/null || log_warn "Could not pull igny8-frontend-dev:staging (may not exist in registry yet)" docker pull igny8-marketing-dev:staging 2>/dev/null || log_warn "Could not pull igny8-marketing-dev:staging (may not exist in registry yet)" log_info "Image pull complete" } start_staging_containers() { log_info "Starting staging containers..." docker-compose \ --project-name "$PROJECT_NAME" \ --file "$COMPOSE_FILE" \ --env-file "$ENV_FILE" \ up -d log_success "Staging containers started" } run_migrations() { log_info "Running database migrations..." docker-compose \ --project-name "$PROJECT_NAME" \ --file "$COMPOSE_FILE" \ --env-file "$ENV_FILE" \ exec -T igny8_staging_backend python manage.py migrate log_success "Database migrations completed" } create_superuser() { log_info "Checking for superuser..." SUPERUSER_EXISTS=$(docker-compose \ --project-name "$PROJECT_NAME" \ --file "$COMPOSE_FILE" \ --env-file "$ENV_FILE" \ exec -T igny8_staging_backend python manage.py shell << 'EOF' from django.contrib.auth import get_user_model User = get_user_model() print(User.objects.filter(is_superuser=True).exists()) EOF ) if [ "$SUPERUSER_EXISTS" = "True" ]; then log_warn "Superuser already exists, skipping creation" return 0 fi log_info "Creating superuser..." docker-compose \ --project-name "$PROJECT_NAME" \ --file "$COMPOSE_FILE" \ --env-file "$ENV_FILE" \ exec -T igny8_staging_backend python manage.py createsuperuser \ --username admin \ --email admin@staging.igny8.com \ --noinput # Set password from env or prompt docker-compose \ --project-name "$PROJECT_NAME" \ --file "$COMPOSE_FILE" \ --env-file "$ENV_FILE" \ exec -T igny8_staging_backend python manage.py shell << 'EOF' from django.contrib.auth import get_user_model User = get_user_model() user = User.objects.get(username='admin') user.set_password('$DJANGO_SUPERUSER_PASSWORD') user.save() EOF log_success "Superuser created" } health_check() { log_info "Running health checks..." RETRIES=30 RETRY_COUNT=0 while [ $RETRY_COUNT -lt $RETRIES ]; do if curl -s -f "http://localhost:8012/api/v1/system/status/" &> /dev/null; then log_success "Backend health check passed" return 0 fi RETRY_COUNT=$((RETRY_COUNT + 1)) if [ $RETRY_COUNT -lt $RETRIES ]; then sleep 2 fi done log_warn "Backend health check failed after $RETRIES retries" return 1 } print_summary() { log_success "Staging environment deployment completed!" echo "" echo -e "${BLUE}========================================${NC}" echo -e "${BLUE}STAGING ENVIRONMENT READY${NC}" echo -e "${BLUE}========================================${NC}" echo "" echo "Frontend: https://staging.igny8.com" echo "API: https://staging-api.igny8.com" echo "Marketing: https://staging-marketing.igny8.com" echo "Admin: https://staging-api.igny8.com/admin/" echo "" echo "Container Status:" docker-compose \ --project-name "$PROJECT_NAME" \ --file "$COMPOSE_FILE" \ ps echo "" } # ============================================================================== # MAIN # ============================================================================== main() { log_info "Starting staging environment deployment..." echo "" check_prerequisites verify_network verify_shared_services create_staging_database if [ "$1" = "pull-images" ]; then pull_images fi start_staging_containers run_migrations create_superuser health_check print_summary } main "$@" ``` **Make script executable:** ```bash chmod +x deploy-staging.sh ``` --- ### Step 9: Create `sync-prod-to-staging.sh` Script **Location:** Root project directory or `scripts/sync-prod-to-staging.sh` ```bash #!/bin/bash # ============================================================================== # IGNY8 Production to Staging Data Sync Script # ============================================================================== # Usage: ./sync-prod-to-staging.sh [--full] [--backup] # --full: Sync entire database (slower, more comprehensive) # --backup: Create backup of staging DB before sync # ============================================================================== set -e # Exit on error # Configuration PROD_DB="igny8_db" STAGING_DB="igny8_staging_db" DB_HOST="postgres" DB_USER="igny8" BACKUP_DIR="/data/backups/staging" TIMESTAMP=$(date +%Y%m%d_%H%M%S) # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Flags FULL_SYNC=false CREATE_BACKUP=false # ============================================================================== # FUNCTIONS # ============================================================================== log_info() { echo -e "${BLUE}[INFO]${NC} $1" } log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1" } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1" } log_error() { echo -e "${RED}[ERROR]${NC} $1" } parse_args() { while [[ $# -gt 0 ]]; do case $1 in --full) FULL_SYNC=true shift ;; --backup) CREATE_BACKUP=true shift ;; *) log_error "Unknown option: $1" exit 1 ;; esac done } check_prerequisites() { log_info "Checking prerequisites..." if ! command -v docker &> /dev/null; then log_error "Docker not installed" exit 1 fi # Check PostgreSQL accessibility (container name is 'postgres', not 'igny8_postgres') if ! docker exec postgres pg_isready -U "$DB_USER" &> /dev/null; then log_error "Cannot connect to PostgreSQL" exit 1 fi log_success "Prerequisites check passed" } backup_staging_db() { if [ "$CREATE_BACKUP" = false ]; then return 0 fi log_info "Creating backup of staging database..." mkdir -p "$BACKUP_DIR" BACKUP_FILE="$BACKUP_DIR/igny8_staging_db_${TIMESTAMP}.sql.gz" docker exec postgres pg_dump \ -U "$DB_USER" \ --format=plain \ "$STAGING_DB" | gzip > "$BACKUP_FILE" log_success "Backup created: $BACKUP_FILE" } truncate_staging_tables() { log_info "Truncating staging database tables..." docker exec -i postgres psql \ -U "$DB_USER" \ -d "$STAGING_DB" << 'EOF' -- Get list of all tables DO $$ DECLARE r RECORD; BEGIN FOR r IN ( SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type = 'BASE TABLE' ) LOOP EXECUTE 'TRUNCATE TABLE "' || r.table_name || '" CASCADE'; END LOOP; END $$; EOF log_success "Staging database truncated" } dump_production_data() { log_info "Dumping production database..." DUMP_FILE="/tmp/igny8_prod_dump_${TIMESTAMP}.sql" docker exec postgres pg_dump \ -U "$DB_USER" \ --format=plain \ "$PROD_DB" > "$DUMP_FILE" log_success "Production database dumped: $DUMP_FILE" echo "$DUMP_FILE" } restore_to_staging() { local DUMP_FILE=$1 log_info "Restoring data to staging database..." cat "$DUMP_FILE" | docker exec -i postgres psql \ -U "$DB_USER" \ -d "$STAGING_DB" \ --quiet log_success "Data restored to staging database" } handle_sensitive_data() { log_info "Anonymizing/resetting sensitive data in staging..." docker exec -i postgres psql \ -U "$DB_USER" \ -d "$STAGING_DB" << 'EOF' -- Reset/anonymize sensitive data in staging using ACTUAL table names -- (All tables prefixed with igny8_ — see 00C for full table list) -- Reset user passwords for non-staff users (set to a known staging password hash) UPDATE igny8_users SET password = 'pbkdf2_sha256$600000$stagingsalt$hashedvalue' WHERE is_staff = false; -- Clear payment tokens (integration keys are in DB, not env vars) -- Integration settings are in igny8_integration_settings and igny8_integration_providers -- Do NOT delete these — just note they need to be updated to sandbox keys post-sync -- Clear webhook event records (contain real payment data) DELETE FROM igny8_webhook_events; -- Clear transient data DELETE FROM django_session; -- Clear AI task logs (optional — may contain API call details) -- DELETE FROM igny8_ai_task_logs; -- NOTE: After sync, manually update igny8_integration_settings to use sandbox API keys -- for openai, stripe, paypal, runware, resend providers EOF log_success "Sensitive data handled" } sync_redis_cache() { log_info "Clearing staging Redis cache (DB 1)..." docker exec redis redis-cli -n 1 FLUSHDB log_success "Staging Redis cache cleared" } cleanup() { log_info "Cleaning up temporary files..." # Clean up old dump files (keep last 5) find /tmp -name "igny8_prod_dump_*.sql" -mtime +1 -delete log_success "Cleanup completed" } print_summary() { log_success "Data sync completed successfully!" echo "" echo -e "${BLUE}========================================${NC}" echo -e "${BLUE}SYNC SUMMARY${NC}" echo -e "${BLUE}========================================${NC}" echo "" echo "Source Database: $PROD_DB" echo "Target Database: $STAGING_DB" echo "Sync Type: $([ "$FULL_SYNC" = true ] && echo 'Full' || echo 'Standard')" echo "Backup Created: $([ "$CREATE_BACKUP" = true ] && echo 'Yes' || echo 'No')" echo "Timestamp: $TIMESTAMP" echo "" echo "Next steps:" echo "1. Verify staging data is correct" echo "2. Test data-dependent features" echo "3. Run staging tests: docker-compose -f docker-compose.staging.yml exec igny8_staging_backend pytest" echo "" } # ============================================================================== # MAIN # ============================================================================== main() { parse_args "$@" log_info "Starting production to staging data sync..." echo "" check_prerequisites backup_staging_db truncate_staging_tables DUMP_FILE=$(dump_production_data) restore_to_staging "$DUMP_FILE" handle_sensitive_data sync_redis_cache cleanup print_summary # Cleanup dump file rm -f "$DUMP_FILE" } main "$@" ``` **Make script executable:** ```bash chmod +x sync-prod-to-staging.sh ``` --- ### Step 10: Deploy Staging Environment **Execute deployment:** ```bash # From project root ./deploy-staging.sh # With image pull (if using registry) ./deploy-staging.sh pull-images ``` **Verify deployment:** ```bash # Check container status docker-compose -f docker-compose.staging.yml -p igny8-staging ps # Check logs docker-compose -f docker-compose.staging.yml -p igny8-staging logs -f # Test API endpoint curl -v https://staging-api.igny8.com/api/v1/system/status/ # Test frontend curl -v https://staging.igny8.com ``` --- ## 5. Acceptance Criteria ### 5.1 Infrastructure Acceptance - [ ] Staging PostgreSQL database `igny8_staging_db` created and accessible - [ ] Redis DB 1 operational and isolated from production (DB 0) - [ ] Docker network `igny8_net` shared between prod and staging containers - [ ] `/data/app/logs/staging/` directory created with proper permissions - [ ] All five staging containers running without errors: - `igny8_staging_backend` - `igny8_staging_frontend` - `igny8_staging_marketing_dev` - `igny8_staging_celery_worker` - `igny8_staging_celery_beat` ### 5.2 Configuration Acceptance - [ ] `docker-compose.staging.yml` properly configured with all services - [ ] `.env.staging` populated with test credentials and sandbox keys - [ ] Caddyfile updated with three staging domain routes - [ ] Caddy successfully reloaded without errors - [ ] Cloudflare DNS records created for: - `staging.igny8.com` - `staging-api.igny8.com` - `staging-marketing.igny8.com` ### 5.3 Application Acceptance - [ ] Database migrations run successfully on `igny8_staging_db` - [ ] Staging superuser created and accessible via admin interface - [ ] Frontend loads at `https://staging.igny8.com` without SSL errors - [ ] API accessible at `https://staging-api.igny8.com` with proper CORS headers - [ ] Marketing site loads at `https://staging-marketing.igny8.com` - [ ] Health check endpoint returns 200 at `/api/v1/system/status/` ### 5.4 Data Synchronization Acceptance - [ ] `sync-prod-to-staging.sh` script successfully copies production data - [ ] Sensitive data (passwords, API keys, payment tokens) properly anonymized - [ ] Redis cache (DB 1) cleared after sync - [ ] Staging database contains realistic test data from production - [ ] Database backup created and stored in `/data/backups/staging/` ### 5.5 Monitoring & Logging Acceptance - [ ] All container logs written to `/data/app/logs/staging/` - [ ] Caddy access logs written to `/data/app/logs/staging/staging-*.log` - [ ] No errors in container startup sequences - [ ] Health checks passing for backend container - [ ] Application errors properly captured in logs ### 5.6 Deployment Automation Acceptance - [ ] `deploy-staging.sh` script runs without manual intervention - [ ] Prerequisites validation prevents incomplete deployments - [ ] Database creation idempotent (safe to run multiple times) - [ ] Container restart behavior correct (unless-stopped policy) - [ ] Script provides clear success/failure feedback ### 5.7 Git Workflow Acceptance - [ ] Staging branch (`staging`) created in Git - [ ] Feature branches merge to `staging` for testing - [ ] `staging` branch merges to `main` for production deployment - [ ] Branch protection rules configured (code review requirements) - [ ] CI/CD pipeline builds staging images on commits to `staging` branch --- ## 6. Claude Code Instructions ### 6.1 Pre-Deployment Tasks Execute these tasks before running deployment scripts: ``` Tasks: 1. Verify all files exist: - docker-compose.staging.yml - .env.staging - deploy-staging.sh - sync-prod-to-staging.sh - Scripts must be executable (chmod +x) 2. Verify NEW VPS and production environment (00B must be complete): - NEW VPS provisioned with Ubuntu 24.04 LTS, Docker, PostgreSQL 18, Redis 8, Caddy 2.11 - PostgreSQL running with igny8_db on PostgreSQL 18 - Redis running with DB 0 active - Caddy running - docker-compose.yml (production) up and stable 3. Create logs directory: - sudo mkdir -p /data/app/logs/staging - sudo chmod 755 /data/app/logs/staging 4. Verify DNS configuration (coordinate with 00C 3-stage migration flow): - Determine current 00C stage (1: old provider, 2: new provider, 3: Cloudflare) - CNAME records created for staging subdomains on the appropriate active DNS provider - Wait for DNS propagation (5-10 minutes if newly created) - If DNS not yet ready, coordinate with 00C flow to complete Stage 2 (DNS flip) or Stage 3 (Cloudflare onboarding) - May use test variants (e.g., `test-staging.igny8.com`) if standard staging domains conflict during migration 5. Update .env.staging with actual values: - STAGING_SECRET_KEY (generate new, min 50 chars) - DB_PASSWORD (must match DATABASE_PASSWORD) - STAGING_STRIPE_PUBLIC_KEY (test key from Stripe) - STAGING_STRIPE_SECRET_KEY (test key from Stripe) - AWS credentials (staging bucket) - All other external service credentials ``` ### 6.2 Deployment Workflow **Step 1: Validate Prerequisites** ```bash # In project root docker-compose ps # Verify production running docker network inspect igny8_net # Verify network docker exec postgres pg_isready -U postgres # Verify PostgreSQL docker exec redis redis-cli ping # Verify Redis ``` **Step 2: Create Staging Database** ```bash docker exec -i postgres psql -U postgres -d postgres << 'EOF' CREATE DATABASE igny8_staging_db WITH OWNER igny8_user ENCODING 'UTF8' LOCALE 'en_US.UTF-8' TEMPLATE template0; GRANT ALL PRIVILEGES ON DATABASE igny8_staging_db TO igny8_user; GRANT ALL PRIVILEGES ON SCHEMA public TO igny8_user; EOF ``` **Step 3: Update Caddyfile** ```bash # Edit /data/caddy/Caddyfile and append staging routes # Reload: docker exec caddy caddy reload --config /etc/caddy/Caddyfile ``` **Step 4: Build Images** ```bash cd /path/to/backend && docker build -t igny8-backend:staging . cd /path/to/frontend && docker build -t igny8-frontend-dev:staging . cd /path/to/marketing && docker build -t igny8-marketing-dev:staging . ``` **Step 5: Deploy Staging** ```bash ./deploy-staging.sh # or with image pulling: ./deploy-staging.sh pull-images ``` **Step 6: Verify Deployment** ```bash # Check containers docker-compose -f docker-compose.staging.yml -p igny8-staging ps # Check health curl https://staging-api.igny8.com/api/v1/system/status/ # Check logs docker-compose -f docker-compose.staging.yml -p igny8-staging logs -f igny8_staging_backend ``` ### 6.3 Post-Deployment Tasks ``` Tasks: 1. Sync production data (optional, for testing with real data): ./sync-prod-to-staging.sh --backup (Creates backup before overwriting staging data) 2. Access staging environment: - Frontend: https://staging.igny8.com - API: https://staging-api.igny8.com - Admin: https://staging-api.igny8.com/admin/ - Username: admin - Password: (from .env.staging DJANGO_SUPERUSER_PASSWORD) 3. Run tests: docker-compose -f docker-compose.staging.yml -p igny8-staging exec igny8_staging_backend pytest 4. Monitor logs: tail -f /data/app/logs/staging/*.log 5. Document any issues found and file tickets against staging branch ``` ### 6.4 Troubleshooting Commands **Container won't start:** ```bash docker-compose -f docker-compose.staging.yml -p igny8-staging logs igny8_staging_backend ``` **Database connection failing:** ```bash docker exec postgres psql -U igny8 -d igny8_staging_db -c "SELECT 1" ``` **Redis connection failing:** ```bash docker exec redis redis-cli -n 1 ping ``` **DNS not resolving:** ```bash nslookup staging.igny8.com dig +short staging.igny8.com ``` **Caddy route not working:** ```bash docker exec caddy caddy list-config docker exec caddy caddy reload --config /etc/caddy/Caddyfile -v ``` **Restart entire staging environment:** ```bash docker-compose -f docker-compose.staging.yml -p igny8-staging down ./deploy-staging.sh ``` **Reset staging database:** ```bash docker exec postgres dropdb -U igny8 igny8_staging_db ./deploy-staging.sh # Recreates and migrations ``` --- ## 7. Related Documentation - **[00B Infrastructure Setup](00B-infrastructure-setup.md):** NEW VPS provisioning, Docker, PostgreSQL, Redis, Caddy configuration. Contains aspirational version targets; current versions verified from codebase (Python 3.11, Django >=5.2.7, Node 18, etc.) - **[00C Production Migration](00C-production-migration.md):** 3-stage migration flow (Deploy & Test, DNS Flip, Cloudflare Onboarding). DNS Reference: Staging setup coordinates with 00C stages to determine active DNS provider. - **Codebase files:** `docker-compose.staging.yml` (actual staging compose), `docker-compose.app.yml` (production compose), `backend/requirements.txt` (Python deps), `frontend/package.json` (JS deps). --- ## 8. Sign-Off This document is complete and ready for implementation. **Document Version:** 1.0 **Last Updated:** 2026-03-23 **Status:** Ready for Deployment **Author:** IGNY8 Phase 0 Planning ---