# IGNY8 Phase 0: Staging Environment Setup (Doc 00D) **Document Status:** Build Specification **Date Created:** 2026-03-23 **Target Phase:** Phase 0 - Infrastructure & Deployment **Related Docs:** [00B Infrastructure Setup](00B-infrastructure-setup.md) | [00C Production Migration](00C-production-migration.md) | [00B Version Matrix](00B-infrastructure-setup.md#version-matrix) (SINGLE SOURCE OF TRUTH for all versions) **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 all component versions (PostgreSQL, Redis, Docker, etc.), refer to the **Version Matrix in 00B Infrastructure Setup** as the single source of truth. This document reflects those versions. All staging components use the latest versions matching production on the NEW VPS. ### 1.1 Infrastructure Baseline - **Host Server:** Single Linux VM running Docker on NEW VPS (from 00B Infrastructure Setup) - **Base OS:** Ubuntu 24.04 LTS - **Shared Resources:** - PostgreSQL 18 server (port 5432) - Redis 8 server (port 6379) - Docker network: `igny8_net` - Caddy 2.11 reverse proxy (port 80/443) - Cloudflare DNS management (may or may not be active - dependent on 00C flow stage) - Log directory: `/data/app/logs/` ### 1.2 Production Environment (Already Complete - Doc 00C) - **Database:** `igny8_db` (PostgreSQL) - **Cache:** Redis DB 0 - **Compose file:** `docker-compose.yml` - **Containers:** - `igny8_backend` (port 8010) - `igny8_frontend` (port 5173) - `igny8_marketing_dev` (port 5174) - `igny8_celery_worker` - `igny8_celery_beat` - **Env file:** `.env` (production settings) - **Domains:** igny8.com, api.igny8.com, marketing.igny8.com - **Logs:** `/data/app/logs/production/` ### 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) │ │ igny8_staging_frontend:8024 → :5173 (Vue) │ │ igny8_staging_marketing_dev:8026 → :5174 (Nuxt) │ │ 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 referenced below are from the **00B Version Matrix (source of truth for all versions)**. The staging environment uses identical versions to production on the NEW VPS: - PostgreSQL 18 (postgres:18-alpine) - Redis 8 (redis:8-alpine) - Caddy 2.11 (caddy:2-alpine) - Ubuntu 24.04 LTS (base OS) - Docker Engine 29.x - Python 3.14 (in backend container) - Node 24 LTS (in frontend and marketing containers) - Django 6.0 - Vite 8 - Gunicorn 25 - Celery 5.6 ### 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 igny8_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 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 version: '3.8' services: # Backend API Service igny8_staging_backend: image: igny8-backend:staging container_name: igny8_staging_backend environment: - DJANGO_ENV=staging - DEBUG=True - ALLOWED_HOSTS=staging.igny8.com,staging-api.igny8.com,localhost,127.0.0.1 - SECRET_KEY=${STAGING_SECRET_KEY} - DATABASE_URL=postgresql://igny8_user:${DB_PASSWORD}@igny8_postgres:5432/igny8_staging_db - REDIS_URL=redis://igny8_redis:6379/1 - CELERY_BROKER_URL=redis://igny8_redis:6379/1 - CELERY_RESULT_BACKEND=redis://igny8_redis:6379/1 - CACHE_URL=redis://igny8_redis:6379/1 - CORS_ALLOWED_ORIGINS=https://staging.igny8.com,https://staging-marketing.igny8.com - STRIPE_PUBLIC_KEY=${STAGING_STRIPE_PUBLIC_KEY} - STRIPE_SECRET_KEY=${STAGING_STRIPE_SECRET_KEY} - STRIPE_WEBHOOK_SECRET=${STAGING_STRIPE_WEBHOOK_SECRET} - API_BASE_URL=https://staging-api.igny8.com - FRONTEND_URL=https://staging.igny8.com - MARKETING_URL=https://staging-marketing.igny8.com - AWS_ACCESS_KEY_ID=${STAGING_AWS_ACCESS_KEY_ID} - AWS_SECRET_ACCESS_KEY=${STAGING_AWS_SECRET_ACCESS_KEY} - AWS_S3_BUCKET=${STAGING_AWS_S3_BUCKET} - AWS_REGION=${AWS_REGION} - SENTRY_DSN=${STAGING_SENTRY_DSN} - LOG_LEVEL=INFO ports: - "8012:8010" volumes: - ./backend:/app/backend - /data/app/logs/staging:/var/log/igny8 networks: - igny8_net depends_on: - igny8_postgres - igny8_redis restart: unless-stopped healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8010/health/"] interval: 30s timeout: 10s retries: 3 start_period: 40s labels: - "com.igny8.component=backend" - "com.igny8.environment=staging" # Frontend Service igny8_staging_frontend: image: igny8-frontend-dev:staging container_name: igny8_staging_frontend environment: - NODE_ENV=staging - VITE_API_URL=https://staging-api.igny8.com - VITE_ENVIRONMENT=staging ports: - "8024:5173" volumes: - ./frontend:/app - /app/node_modules networks: - igny8_net restart: unless-stopped labels: - "com.igny8.component=frontend" - "com.igny8.environment=staging" # Marketing Site Service igny8_staging_marketing_dev: image: igny8-marketing-dev:staging container_name: igny8_staging_marketing_dev environment: - NODE_ENV=staging - NUXT_PUBLIC_API_URL=https://staging-api.igny8.com - NUXT_PUBLIC_ENVIRONMENT=staging ports: - "8026:5174" volumes: - ./marketing:/app - /app/.nuxt - /app/node_modules networks: - igny8_net restart: unless-stopped labels: - "com.igny8.component=marketing" - "com.igny8.environment=staging" # Celery Worker igny8_staging_celery_worker: image: igny8-backend:staging container_name: igny8_staging_celery_worker command: celery -A backend.celery worker --loglevel=info --concurrency=2 environment: - DJANGO_ENV=staging - DEBUG=True - SECRET_KEY=${STAGING_SECRET_KEY} - DATABASE_URL=postgresql://igny8_user:${DB_PASSWORD}@igny8_postgres:5432/igny8_staging_db - REDIS_URL=redis://igny8_redis:6379/1 - CELERY_BROKER_URL=redis://igny8_redis:6379/1 - CELERY_RESULT_BACKEND=redis://igny8_redis:6379/1 - AWS_ACCESS_KEY_ID=${STAGING_AWS_ACCESS_KEY_ID} - AWS_SECRET_ACCESS_KEY=${STAGING_AWS_SECRET_ACCESS_KEY} - AWS_S3_BUCKET=${STAGING_AWS_S3_BUCKET} volumes: - ./backend:/app/backend - /data/app/logs/staging:/var/log/igny8 networks: - igny8_net depends_on: - igny8_postgres - igny8_redis restart: unless-stopped labels: - "com.igny8.component=celery-worker" - "com.igny8.environment=staging" # Celery Beat (Scheduler) igny8_staging_celery_beat: image: igny8-backend:staging container_name: igny8_staging_celery_beat command: celery -A backend.celery beat --loglevel=info --scheduler django_celery_beat.schedulers:DatabaseScheduler environment: - DJANGO_ENV=staging - DEBUG=True - SECRET_KEY=${STAGING_SECRET_KEY} - DATABASE_URL=postgresql://igny8_user:${DB_PASSWORD}@igny8_postgres:5432/igny8_staging_db - REDIS_URL=redis://igny8_redis:6379/1 - CELERY_BROKER_URL=redis://igny8_redis:6379/1 - CELERY_RESULT_BACKEND=redis://igny8_redis:6379/1 volumes: - ./backend:/app/backend - /data/app/logs/staging:/var/log/igny8 networks: - igny8_net depends_on: - igny8_postgres - igny8_redis restart: unless-stopped labels: - "com.igny8.component=celery-beat" - "com.igny8.environment=staging" networks: igny8_net: external: true volumes: # Data volumes referenced from external production infrastructure ``` --- ### 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=igny8_postgres DATABASE_PORT=5432 DATABASE_NAME=igny8_staging_db DATABASE_USER=igny8_user DATABASE_PASSWORD=${DB_PASSWORD} # Full URL for Django DATABASE_URL=postgresql://igny8_user:${DB_PASSWORD}@igny8_postgres:5432/igny8_staging_db # ============================================================================== # CACHE & QUEUE (REDIS DB 1 - Separate from Production) # ============================================================================== REDIS_HOST=igny8_redis REDIS_PORT=6379 REDIS_DB=1 REDIS_PASSWORD= REDIS_URL=redis://igny8_redis:6379/1 CACHE_URL=redis://igny8_redis:6379/1 # ============================================================================== # CELERY (Uses Redis DB 1) # ============================================================================== CELERY_BROKER_URL=redis://igny8_redis:6379/1 CELERY_RESULT_BACKEND=redis://igny8_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 MAILGUN_API_KEY=your-staging-mailgun-key MAILGUN_DOMAIN=staging-mail.igny8.com # Analytics MIXPANEL_TOKEN=your-staging-mixpanel-token # Error Tracking STAGING_SENTRY_DSN=https://your-staging-sentry-dsn # SMS TWILIO_ACCOUNT_SID=your-staging-twilio-sid TWILIO_AUTH_TOKEN=your-staging-twilio-token TWILIO_PHONE_NUMBER=+15551234567 # ============================================================================== # 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 / VUE # ============================================================================== VITE_API_URL=https://staging-api.igny8.com VITE_ENVIRONMENT=staging # ============================================================================== # MARKETING / NUXT # ============================================================================== NUXT_PUBLIC_API_URL=https://staging-api.igny8.com NUXT_PUBLIC_ENVIRONMENT=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 /path/to/backend docker build -f Dockerfile -t igny8-backend:staging . # Verify docker images | grep igny8-backend ``` **Frontend Image** ```bash cd /path/to/frontend docker build -f Dockerfile.dev -t igny8-frontend-dev:staging . # Verify docker images | grep igny8-frontend-dev ``` **Marketing Image** ```bash cd /path/to/marketing docker build -f Dockerfile.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.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 igny8_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 igny8_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 igny8_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 igny8_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 igny8_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 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.models import User 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.models import User 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/health/" &> /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="igny8_postgres" DB_USER="igny8_user" 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 if ! docker exec igny8_postgres pg_isready -U "$DB_USER" -h "$DB_HOST" &> /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 igny8_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 igny8_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 igny8_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 igny8_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 ighty8_postgres psql \ -U "$DB_USER" \ -d "$STAGING_DB" << 'EOF' -- Reset payment information UPDATE billing_paymentmethod SET token = NULL WHERE token IS NOT NULL; -- Reset API tokens UPDATE api_token SET token = 'staging-token-' || id WHERE 1=1; -- Reset user passwords (set to default) UPDATE auth_user SET password = 'pbkdf2_sha256$600000$abcdefg$hashed' WHERE is_staff = false; -- Reset email addresses for non-admin users (optional - for testing) -- UPDATE auth_user SET email = CONCAT(username, '@staging-test.local') WHERE is_staff = false; -- Clear sensitive logs DELETE FROM audit_log WHERE action_type IN ('payment', 'user_data_export'); -- Clear transient data DELETE FROM celery_taskmeta; DELETE FROM django_session; -- Reset any third-party API keys to staging versions UPDATE integration_apikey SET secret = 'sk_staging_' || id WHERE 1=1; EOF log_success "Sensitive data handled" } sync_redis_cache() { log_info "Clearing staging Redis cache (DB 1)..." docker exec igny8_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/health/ # 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 `/health/` ### 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 igny8_postgres pg_isready -U postgres # Verify PostgreSQL docker exec igny8_redis redis-cli ping # Verify Redis ``` **Step 2: Create Staging Database** ```bash docker exec -i igny8_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 igny8_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/health/ # 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 igny8_postgres psql -U igny8_user -d igny8_staging_db -c "SELECT 1" ``` **Redis connection failing:** ```bash docker exec igny8_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 igny8_caddy caddy list-config docker exec igny8_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 igny8_postgres dropdb -U igny8_user igny8_staging_db ./deploy-staging.sh # Recreates and migrations ``` --- ## 7. Related Documentation - **[00B Infrastructure Setup](00B-infrastructure-setup.md):** NEW VPS provisioning, Docker, PostgreSQL 18, Redis 8, Caddy 2.11 configuration - **Version Matrix (in 00B):** SINGLE SOURCE OF TRUTH for all component versions (PostgreSQL 18, Redis 8, Caddy 2.11, Python 3.14, Node 24 LTS, Django 6.0, Vite 8, Gunicorn 25, Celery 5.6, etc.) - Staging environment on NEW VPS uses identical versions - **[00C Production Migration](00C-production-migration.md):** 3-stage migration flow (DNS Preparation, DNS Flip, Cloudflare Onboarding), production database setup and initial deployment - **DNS Reference:** Staging setup coordinates with 00C Stage 1/2/3 to determine active DNS provider and domain naming (staging may use test variants during migration) - **Prerequisite:** 00B must be complete to provision the NEW VPS where staging runs. 00C determines which DNS provider is active for staging domain records. --- ## 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 ---