diff --git a/config/haproxy/haproxy-blue.cfg b/config/haproxy/haproxy-blue.cfg new file mode 100644 index 000000000..3c070a6e5 --- /dev/null +++ b/config/haproxy/haproxy-blue.cfg @@ -0,0 +1,77 @@ +# Blue stack active - copy to haproxy.cfg for blue deployment +# Generated from haproxy.cfg - blue servers main, green backup +global + log stdout format raw local0 + maxconn 4096 + daemon + stats socket /var/run/haproxy.sock level admin + +defaults + log global + mode http + option httplog + option dontlognull + option forwardfor + option http-server-close + timeout connect 5000ms + timeout client 50000ms + timeout server 50000ms + timeout http-request 10000ms + +frontend stats + bind *:8404 + stats enable + stats uri /stats + stats refresh 30s + acl from_internal src 127.0.0.1 172.20.0.0/16 + stats admin if from_internal + +frontend http_frontend + bind *:80 + mode http + redirect scheme https code 301 if !{ ssl_fc } + acl is_api path_beg /api/v1 + acl is_stream path_beg /stream + acl is_hls path_beg /hls + acl is_web path_beg / + use_backend backend_api if is_api + use_backend stream_ws if is_stream + use_backend stream_ws if is_hls + use_backend web_frontend if is_web + +frontend https_frontend + bind *:443 ssl crt /etc/ssl/veza/veza.pem + mode http + acl is_api path_beg /api/v1 + acl is_stream path_beg /stream + acl is_hls path_beg /hls + acl is_web path_beg / + use_backend backend_api if is_api + use_backend stream_ws if is_stream + use_backend stream_ws if is_hls + use_backend web_frontend if is_web + +backend backend_api + mode http + balance roundrobin + option httpchk GET /api/v1/health + http-check expect status 200 + server api_blue backend-api-blue:8080 check inter 5s fall 3 rise 2 + server api_green backend-api-green:8080 check inter 5s fall 3 rise 2 backup + +backend stream_ws + mode http + balance roundrobin + option httpchk GET /health + http-check expect status 200 + timeout tunnel 3600s + server stream_blue stream-server-blue:3001 check inter 5s fall 3 rise 2 + server stream_green stream-server-green:3001 check inter 5s fall 3 rise 2 backup + +backend web_frontend + mode http + balance roundrobin + option httpchk GET / + http-check expect status 200 + server web_blue web-blue:5173 check inter 5s fall 3 rise 2 + server web_green web-green:5173 check inter 5s fall 3 rise 2 backup diff --git a/config/haproxy/haproxy-green.cfg b/config/haproxy/haproxy-green.cfg new file mode 100644 index 000000000..6b4df8d28 --- /dev/null +++ b/config/haproxy/haproxy-green.cfg @@ -0,0 +1,77 @@ +# Green stack active - copy to haproxy.cfg for green deployment +# Generated from haproxy.cfg - green servers main, blue backup +global + log stdout format raw local0 + maxconn 4096 + daemon + stats socket /var/run/haproxy.sock level admin + +defaults + log global + mode http + option httplog + option dontlognull + option forwardfor + option http-server-close + timeout connect 5000ms + timeout client 50000ms + timeout server 50000ms + timeout http-request 10000ms + +frontend stats + bind *:8404 + stats enable + stats uri /stats + stats refresh 30s + acl from_internal src 127.0.0.1 172.20.0.0/16 + stats admin if from_internal + +frontend http_frontend + bind *:80 + mode http + redirect scheme https code 301 if !{ ssl_fc } + acl is_api path_beg /api/v1 + acl is_stream path_beg /stream + acl is_hls path_beg /hls + acl is_web path_beg / + use_backend backend_api if is_api + use_backend stream_ws if is_stream + use_backend stream_ws if is_hls + use_backend web_frontend if is_web + +frontend https_frontend + bind *:443 ssl crt /etc/ssl/veza/veza.pem + mode http + acl is_api path_beg /api/v1 + acl is_stream path_beg /stream + acl is_hls path_beg /hls + acl is_web path_beg / + use_backend backend_api if is_api + use_backend stream_ws if is_stream + use_backend stream_ws if is_hls + use_backend web_frontend if is_web + +backend backend_api + mode http + balance roundrobin + option httpchk GET /api/v1/health + http-check expect status 200 + server api_blue backend-api-blue:8080 check inter 5s fall 3 rise 2 backup + server api_green backend-api-green:8080 check inter 5s fall 3 rise 2 + +backend stream_ws + mode http + balance roundrobin + option httpchk GET /health + http-check expect status 200 + timeout tunnel 3600s + server stream_blue stream-server-blue:3001 check inter 5s fall 3 rise 2 backup + server stream_green stream-server-green:3001 check inter 5s fall 3 rise 2 + +backend web_frontend + mode http + balance roundrobin + option httpchk GET / + http-check expect status 200 + server web_blue web-blue:5173 check inter 5s fall 3 rise 2 backup + server web_green web-green:5173 check inter 5s fall 3 rise 2 diff --git a/config/haproxy/haproxy.cfg b/config/haproxy/haproxy.cfg index 6dd2cb039..d6015007b 100644 --- a/config/haproxy/haproxy.cfg +++ b/config/haproxy/haproxy.cfg @@ -2,6 +2,8 @@ global log stdout format raw local0 maxconn 4096 daemon + # Blue-green: runtime API for server enable/disable + stats socket /var/run/haproxy.sock level admin defaults log global @@ -67,35 +69,34 @@ frontend https_frontend use_backend web_frontend if is_web # ============================================================================ -# BACKENDS +# BACKENDS - Blue-Green Deployment +# Use scripts/deploy-blue-green.sh to switch active stack # ============================================================================ -# Backend API (Go) +# Backend API (Go) - blue/green backend backend_api mode http balance roundrobin option httpchk GET /api/v1/health http-check expect status 200 - server backend1 backend-api:8080 check inter 5s fall 3 rise 2 - # Add more servers for load balancing: - # server backend2 backend-api-2:8080 check inter 5s fall 3 rise 2 + server api_blue backend-api-blue:8080 check inter 5s fall 3 rise 2 + server api_green backend-api-green:8080 check inter 5s fall 3 rise 2 backup -# Stream WebSocket (Rust) +# Stream WebSocket (Rust) - blue/green backend stream_ws mode http balance roundrobin option httpchk GET /health http-check expect status 200 - server stream1 stream-server:3001 check inter 5s fall 3 rise 2 - # WebSocket specific options timeout tunnel 3600s + server stream_blue stream-server-blue:3001 check inter 5s fall 3 rise 2 + server stream_green stream-server-green:3001 check inter 5s fall 3 rise 2 backup -# Web Frontend (React/Vite) +# Web Frontend (React/Vite) - blue/green backend web_frontend mode http balance roundrobin option httpchk GET / http-check expect status 200 - server web1 web:5173 check inter 5s fall 3 rise 2 - # Add more servers for load balancing: - # server web2 web-2:5173 check inter 5s fall 3 rise 2 + server web_blue web-blue:5173 check inter 5s fall 3 rise 2 + server web_green web-green:5173 check inter 5s fall 3 rise 2 backup diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index bffc6cf8d..7cc8c27bb 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -139,17 +139,19 @@ services: memory: 256M # ============================================================================ - # APPLICATION SERVICES + # APPLICATION SERVICES - Blue-Green Deployment + # STACK_COLOR=blue|green. Use scripts/deploy-blue-green.sh to switch. # ============================================================================ - backend-api: + backend-api-blue: build: context: ./veza-backend-api dockerfile: Dockerfile.production image: veza-backend-api:latest - container_name: veza_backend_api + container_name: veza_backend_api_blue restart: unless-stopped environment: - APP_ENV=production + - STACK_COLOR=blue - DATABASE_URL=postgres://${DB_USER:-veza}:${DB_PASS:?DB_PASS must be set}@postgres:5432/${DB_NAME:-veza}?sslmode=require - REDIS_URL=redis://:${REDIS_PASSWORD:?REDIS_PASSWORD must be set}@redis:6379 - AMQP_URL=amqp://${DB_USER:-veza}:${RABBITMQ_PASS:?RABBITMQ_PASS must be set}@rabbitmq:5672 @@ -192,14 +194,92 @@ services: timeout: 5s retries: 3 - # Chat Server removed in v0.502 -- chat is now handled by backend-api WebSocket at /api/v1/ws + backend-api-green: + build: + context: ./veza-backend-api + dockerfile: Dockerfile.production + image: veza-backend-api:latest + container_name: veza_backend_api_green + restart: unless-stopped + environment: + - APP_ENV=production + - STACK_COLOR=green + - DATABASE_URL=postgres://${DB_USER:-veza}:${DB_PASS:?DB_PASS must be set}@postgres:5432/${DB_NAME:-veza}?sslmode=require + - REDIS_URL=redis://:${REDIS_PASSWORD:?REDIS_PASSWORD must be set}@redis:6379 + - AMQP_URL=amqp://${DB_USER:-veza}:${RABBITMQ_PASS:?RABBITMQ_PASS must be set}@rabbitmq:5672 + - JWT_SECRET=${JWT_SECRET:?JWT_SECRET must be set for production} + - COOKIE_SECURE=true + - COOKIE_SAME_SITE=strict + - COOKIE_HTTP_ONLY=true + - CORS_ALLOWED_ORIGINS=${CORS_ORIGINS:-http://veza.fr} + - HYPERSWITCH_URL=http://hyperswitch:8080 + - HYPERSWITCH_API_KEY=${HYPERSWITCH_API_KEY:-} + - HYPERSWITCH_WEBHOOK_SECRET=${HYPERSWITCH_WEBHOOK_SECRET:-} + - HYPERSWITCH_ENABLED=${HYPERSWITCH_ENABLED:-false} + - CHECKOUT_SUCCESS_URL=${CHECKOUT_SUCCESS_URL:-https://veza.fr/purchases} + - ENABLE_CLAMAV=true + - CLAMAV_REQUIRED=true + - CLAMAV_ADDRESS=clamav:3310 + - AWS_S3_ENDPOINT=http://minio:9000 + - AWS_S3_BUCKET=veza-files + - AWS_ACCESS_KEY_ID=${S3_ACCESS_KEY:?S3_ACCESS_KEY must be set} + - AWS_SECRET_ACCESS_KEY=${S3_SECRET_KEY:?S3_SECRET_KEY must be set} + - AWS_REGION=${AWS_REGION:-us-east-1} + - HLS_STREAMING=true + - HLS_STORAGE_DIR=/data/hls + volumes: + - hls_prod_data:/data/hls + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + rabbitmq: + condition: service_healthy + clamav: + condition: service_started + networks: + - veza-network + healthcheck: + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:8080/api/v1/health"] + interval: 10s + timeout: 5s + retries: 3 - stream-server: + stream-server-blue: build: context: ./veza-stream-server dockerfile: Dockerfile.production image: veza-stream-server:latest - container_name: veza_stream_server + container_name: veza_stream_server_blue + restart: unless-stopped + environment: + - DATABASE_URL=postgres://${DB_USER:-veza}:${DB_PASS:?DB_PASS must be set}@postgres:5432/${DB_NAME:-veza}?sslmode=require + - REDIS_URL=redis://:${REDIS_PASSWORD:?REDIS_PASSWORD must be set}@redis:6379 + - JWT_SECRET=${JWT_SECRET:?JWT_SECRET must be set} + - PORT=3001 + - HLS_OUTPUT_DIR=/data/hls + volumes: + - hls_prod_data:/data/hls + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + networks: + - veza-network + healthcheck: + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3001/health"] + interval: 10s + timeout: 5s + retries: 3 + + stream-server-green: + build: + context: ./veza-stream-server + dockerfile: Dockerfile.production + image: veza-stream-server:latest + container_name: veza_stream_server_green restart: unless-stopped environment: - DATABASE_URL=postgres://${DB_USER:-veza}:${DB_PASS:?DB_PASS must be set}@postgres:5432/${DB_NAME:-veza}?sslmode=require @@ -257,20 +337,42 @@ services: networks: - veza-network - web: + web-blue: build: context: ./apps/web dockerfile: Dockerfile.production image: veza-web:latest - container_name: veza_web + container_name: veza_web_blue restart: unless-stopped environment: - VITE_API_URL=http://haproxy/api/v1 - VITE_STREAM_URL=ws://haproxy/stream - VITE_UPLOAD_URL=http://haproxy/api/v1/uploads depends_on: - - backend-api - - stream-server + - backend-api-blue + - stream-server-blue + networks: + - veza-network + healthcheck: + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:5173"] + interval: 10s + timeout: 5s + retries: 3 + + web-green: + build: + context: ./apps/web + dockerfile: Dockerfile.production + image: veza-web:latest + container_name: veza_web_green + restart: unless-stopped + environment: + - VITE_API_URL=http://haproxy/api/v1 + - VITE_STREAM_URL=ws://haproxy/stream + - VITE_UPLOAD_URL=http://haproxy/api/v1/uploads + depends_on: + - backend-api-green + - stream-server-green networks: - veza-network healthcheck: @@ -280,7 +382,7 @@ services: retries: 3 # ============================================================================ - # REVERSE PROXY - HAProxy + # REVERSE PROXY - HAProxy (Blue-Green) # ============================================================================ haproxy: image: haproxy:2.8-alpine @@ -298,9 +400,12 @@ services: - ./config/haproxy/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro - ./config/ssl:/etc/ssl/veza:ro depends_on: - - backend-api - - stream-server - - web + - backend-api-blue + - backend-api-green + - stream-server-blue + - stream-server-green + - web-blue + - web-green networks: - veza-network healthcheck: diff --git a/scripts/deploy-blue-green.sh b/scripts/deploy-blue-green.sh new file mode 100755 index 000000000..63ae04598 --- /dev/null +++ b/scripts/deploy-blue-green.sh @@ -0,0 +1,38 @@ +#!/bin/bash +# Blue-Green deployment switch for Veza production +# Usage: ./scripts/deploy-blue-green.sh [blue|green] +# Requires: docker, config/haproxy/ directory, veza_haproxy container running +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +HAPROXY_DIR="$REPO_ROOT/config/haproxy" +TARGET="${1:-}" + +if [[ "$TARGET" != "blue" && "$TARGET" != "green" ]]; then + echo "Usage: $0 [blue|green]" + echo " blue - Route traffic to blue stack (default)" + echo " green - Route traffic to green stack" + exit 1 +fi + +CONFIG_SOURCE="$HAPROXY_DIR/haproxy-$TARGET.cfg" +CONFIG_DEST="$HAPROXY_DIR/haproxy.cfg" + +if [[ ! -f "$CONFIG_SOURCE" ]]; then + echo "Error: Config file not found: $CONFIG_SOURCE" + exit 1 +fi + +echo "Switching to $TARGET stack..." +cp "$CONFIG_SOURCE" "$CONFIG_DEST" + +# Reload HAProxy (graceful, no connection drop) +if docker ps --format '{{.Names}}' | grep -q '^veza_haproxy$'; then + docker kill -s HUP veza_haproxy 2>/dev/null || { + echo "Warning: Could not send HUP to HAProxy. Try: docker restart veza_haproxy" + } + echo "HAProxy reloaded. Active stack: $TARGET" +else + echo "Warning: veza_haproxy container not running. Config updated; start stack to apply." +fi