build front back #397
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # ============================================================================ | |
| # Production CI/CD Pipeline - Traefik Zero-Downtime Deployment | |
| # ============================================================================ | |
| # | |
| # Architecture: | |
| # Traefik (SSL/LB) → Services (auto-discovered via Docker labels) | |
| # | |
| # Deployment Strategy: | |
| # Blue-Green rolling: New containers start → health check passes → old removed | |
| # Traefik automatically routes to healthy containers only | |
| # | |
| # ============================================================================ | |
| name: CI-Production | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| env: | |
| ENV_SOURCE: /opt/projects/prod.docs.plus/.env | |
| ENV_FILE: .env.production | |
| COMPOSE_FILE: docker-compose.prod.yml | |
| DEPLOY_TAG: ${{ github.sha }} | |
| jobs: | |
| deploy: | |
| name: 🚀 Deploy to Production | |
| runs-on: prod.docs.plus | |
| if: contains(github.event.head_commit.message, 'build') && (contains(github.event.head_commit.message, 'front') || contains(github.event.head_commit.message, 'back')) | |
| steps: | |
| - name: 📦 Checkout Code | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 1 | |
| - name: 🥟 Setup Bun | |
| uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: latest | |
| - name: 📥 Install Dependencies | |
| run: bun install --frozen-lockfile | |
| - name: 🔐 Prepare Environment | |
| run: | | |
| echo "📁 Copying environment file..." | |
| if [ ! -f "${{ env.ENV_SOURCE }}" ]; then | |
| echo "❌ .env file not found at ${{ env.ENV_SOURCE }}" | |
| exit 1 | |
| fi | |
| cp "${{ env.ENV_SOURCE }}" "${{ env.ENV_FILE }}" | |
| # Add deploy tag | |
| echo "DEPLOY_TAG=${{ env.DEPLOY_TAG }}" >> "${{ env.ENV_FILE }}" | |
| # Validate required vars | |
| for var in DATABASE_URL SUPABASE_URL SUPABASE_ANON_KEY; do | |
| if ! grep -q "^${var}=" "${{ env.ENV_FILE }}"; then | |
| echo "❌ ${var} not found in .env" | |
| exit 1 | |
| fi | |
| done | |
| echo "✅ Environment prepared" | |
| - name: 🏗️ Build Docker Images | |
| run: | | |
| echo "🔨 Building images with tag: ${{ env.DEPLOY_TAG }}" | |
| # Load env vars for build args | |
| set -a | |
| source ${{ env.ENV_FILE }} | |
| set +a | |
| docker compose -f ${{ env.COMPOSE_FILE }} \ | |
| --env-file ${{ env.ENV_FILE }} \ | |
| build --parallel | |
| echo "✅ Images built" | |
| - name: 🔧 Ensure Infrastructure (Traefik + Redis) | |
| run: | | |
| echo "🔧 Ensuring infrastructure is running..." | |
| # Create network if not exists | |
| docker network create docsplus-network 2>/dev/null || true | |
| # Start Traefik and Redis with --no-recreate (don't restart if running) | |
| # This prevents Traefik restart which causes downtime | |
| docker compose -f ${{ env.COMPOSE_FILE }} \ | |
| --env-file ${{ env.ENV_FILE }} \ | |
| up -d --no-recreate traefik redis | |
| # Only if Traefik is not running at all, start it | |
| if ! docker ps --filter "name=traefik" --filter "status=running" | grep -q traefik; then | |
| echo "⚠️ Traefik not running, starting..." | |
| docker compose -f ${{ env.COMPOSE_FILE }} \ | |
| --env-file ${{ env.ENV_FILE }} \ | |
| up -d traefik | |
| sleep 15 | |
| fi | |
| # Wait for Traefik to be healthy | |
| echo "⏳ Waiting for Traefik health..." | |
| for i in {1..30}; do | |
| if docker ps --filter "name=traefik" --filter "health=healthy" | grep -q traefik; then | |
| echo "✅ Traefik is healthy" | |
| break | |
| fi | |
| if [ $i -eq 30 ]; then | |
| echo "⚠️ Traefik health timeout, but continuing..." | |
| fi | |
| sleep 2 | |
| done | |
| - name: 🚀 Deploy Services (Zero-Downtime) | |
| run: | | |
| echo "🚀 Starting zero-downtime deployment..." | |
| # Function to deploy a service with zero-downtime | |
| # Strategy: Start new containers → Wait healthy → Remove old | |
| deploy_service() { | |
| local SERVICE=$1 | |
| local TARGET_REPLICAS=$2 | |
| echo "" | |
| echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" | |
| echo "📦 Deploying $SERVICE (target: $TARGET_REPLICAS replicas)" | |
| echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" | |
| # Get current container IDs (before deployment) | |
| OLD_CONTAINERS=$(docker ps -q --filter "label=com.docker.compose.service=${SERVICE}" 2>/dev/null || true) | |
| OLD_COUNT=$(echo "$OLD_CONTAINERS" | grep -c . 2>/dev/null || echo "0") | |
| echo "📊 Current containers: $OLD_COUNT" | |
| # Calculate total (old + new) - we want both running during transition | |
| TOTAL=$((OLD_COUNT + TARGET_REPLICAS)) | |
| if [ "$TOTAL" -lt "$TARGET_REPLICAS" ]; then | |
| TOTAL=$TARGET_REPLICAS | |
| fi | |
| # Scale UP to add new containers alongside old ones | |
| echo "⬆️ Scaling to $TOTAL containers (old + new)..." | |
| docker compose -f ${{ env.COMPOSE_FILE }} \ | |
| --env-file ${{ env.ENV_FILE }} \ | |
| up -d --no-deps --scale ${SERVICE}=${TOTAL} --no-recreate ${SERVICE} 2>/dev/null || \ | |
| docker compose -f ${{ env.COMPOSE_FILE }} \ | |
| --env-file ${{ env.ENV_FILE }} \ | |
| up -d --no-deps --scale ${SERVICE}=${TOTAL} ${SERVICE} | |
| # Wait for NEW containers to become healthy | |
| echo "⏳ Waiting for new containers to be healthy..." | |
| HEALTHY_NEEDED=$TARGET_REPLICAS | |
| for i in {1..90}; do | |
| # Count healthy containers | |
| HEALTHY=$(docker ps --filter "label=com.docker.compose.service=${SERVICE}" --filter "health=healthy" --format "{{.ID}}" 2>/dev/null | wc -l || echo "0") | |
| if [ "$HEALTHY" -ge "$HEALTHY_NEEDED" ]; then | |
| echo "✅ $SERVICE: $HEALTHY healthy containers (needed: $HEALTHY_NEEDED)" | |
| break | |
| fi | |
| if [ $i -eq 90 ]; then | |
| echo "⚠️ Health check timeout for $SERVICE (got $HEALTHY, needed $HEALTHY_NEEDED)" | |
| echo " Containers may still be starting..." | |
| fi | |
| # Show progress every 10 seconds | |
| if [ $((i % 5)) -eq 0 ]; then | |
| echo " ... waiting ($HEALTHY/$HEALTHY_NEEDED healthy, attempt $i/90)" | |
| fi | |
| sleep 2 | |
| done | |
| # Now we have healthy new containers, scale DOWN to remove old ones | |
| # Docker Compose will remove the oldest containers when scaling down | |
| echo "⬇️ Scaling down to $TARGET_REPLICAS containers..." | |
| docker compose -f ${{ env.COMPOSE_FILE }} \ | |
| --env-file ${{ env.ENV_FILE }} \ | |
| up -d --no-deps --scale ${SERVICE}=${TARGET_REPLICAS} ${SERVICE} | |
| # Verify final state | |
| sleep 3 | |
| FINAL_COUNT=$(docker ps --filter "label=com.docker.compose.service=${SERVICE}" --format "{{.ID}}" | wc -l) | |
| FINAL_HEALTHY=$(docker ps --filter "label=com.docker.compose.service=${SERVICE}" --filter "health=healthy" --format "{{.ID}}" | wc -l) | |
| echo "✅ $SERVICE deployed: $FINAL_COUNT running, $FINAL_HEALTHY healthy" | |
| } | |
| # Deploy services in order (backend first, then frontend) | |
| deploy_service "rest-api" 2 | |
| deploy_service "hocuspocus-server" 2 | |
| deploy_service "hocuspocus-worker" 1 | |
| deploy_service "webapp" 2 | |
| echo "" | |
| echo "✅ All services deployed" | |
| - name: 🩺 Verify Deployment | |
| run: | | |
| echo "🩺 Verifying deployment..." | |
| # Wait a bit for everything to stabilize | |
| sleep 10 | |
| # Check all core services | |
| echo "📊 Service Status:" | |
| for svc in traefik docsplus-redis; do | |
| if docker ps --filter "name=$svc" --filter "status=running" | grep -q "$svc"; then | |
| echo " ✅ $svc: running" | |
| else | |
| echo " ❌ $svc: NOT running" | |
| docker logs $svc --tail 30 2>/dev/null || true | |
| exit 1 | |
| fi | |
| done | |
| # Check scaled services | |
| for svc in webapp rest-api hocuspocus-server hocuspocus-worker; do | |
| RUNNING=$(docker ps --filter "label=com.docker.compose.service=${svc}" --filter "status=running" --format "{{.Names}}" | wc -l) | |
| HEALTHY=$(docker ps --filter "label=com.docker.compose.service=${svc}" --filter "health=healthy" --format "{{.Names}}" | wc -l) | |
| if [ "$RUNNING" -gt 0 ]; then | |
| echo " ✅ $svc: $RUNNING running, $HEALTHY healthy" | |
| else | |
| echo " ❌ $svc: NOT running" | |
| exit 1 | |
| fi | |
| done | |
| # Health check via Traefik endpoints | |
| echo "" | |
| echo "🔍 Testing endpoints..." | |
| # Test main site | |
| for i in {1..20}; do | |
| HTTP_CODE=$(curl -sf -o /dev/null -w "%{http_code}" https://docs.plus/ 2>/dev/null || echo "000") | |
| if [ "$HTTP_CODE" = "200" ]; then | |
| echo " ✅ https://docs.plus/ → $HTTP_CODE" | |
| break | |
| fi | |
| if [ $i -eq 20 ]; then | |
| echo " ⚠️ https://docs.plus/ → $HTTP_CODE (may still be provisioning)" | |
| fi | |
| sleep 3 | |
| done | |
| # Test API health | |
| HTTP_CODE=$(curl -sf -o /dev/null -w "%{http_code}" https://prodback.docs.plus/api/health 2>/dev/null || echo "000") | |
| if [ "$HTTP_CODE" = "200" ]; then | |
| echo " ✅ https://prodback.docs.plus/api/health → $HTTP_CODE" | |
| else | |
| echo " ⚠️ https://prodback.docs.plus/api/health → $HTTP_CODE" | |
| fi | |
| echo "" | |
| echo "✅ Deployment verified" | |
| - name: 🧹 Cleanup | |
| run: | | |
| # Remove dangling images | |
| docker image prune -f | |
| # Remove old images (older than 24h) | |
| docker image prune -f --filter "until=24h" 2>/dev/null || true | |
| echo "✅ Cleanup complete" | |
| - name: 📊 Summary | |
| run: | | |
| echo "======================================" | |
| echo "✅ DEPLOYMENT SUCCESSFUL" | |
| echo "======================================" | |
| echo "Tag: ${{ env.DEPLOY_TAG }}" | |
| echo "" | |
| echo "Services:" | |
| docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" | grep -E "(traefik|docsplus|webapp|rest-api|hocuspocus)" | head -15 | |
| echo "" | |
| echo "URLs:" | |
| echo " - https://docs.plus" | |
| echo " - https://prodback.docs.plus" | |
| echo "======================================" | |
| - name: 🚨 Rollback on Failure | |
| if: failure() | |
| run: | | |
| echo "⚠️ Deployment failed - attempting recovery..." | |
| # Don't do aggressive rollback - just ensure services are running | |
| # Traefik will route to whatever containers are healthy | |
| docker compose -f ${{ env.COMPOSE_FILE }} \ | |
| --env-file ${{ env.ENV_FILE }} \ | |
| up -d --no-recreate 2>/dev/null || true | |
| echo "📊 Current state:" | |
| docker ps --format "table {{.Names}}\t{{.Status}}" | head -15 | |
| # =========================================================================== | |
| # UPTIME KUMA (Monitoring) | |
| # =========================================================================== | |
| deploy-uptime-kuma: | |
| name: 🔔 Deploy Uptime Kuma | |
| runs-on: prod.docs.plus | |
| if: contains(github.event.head_commit.message, 'build') && contains(github.event.head_commit.message, 'uptime-kuma') | |
| steps: | |
| - name: 🚀 Deploy | |
| run: | | |
| docker network create docsplus-network 2>/dev/null || true | |
| docker stop uptime-kuma 2>/dev/null || true | |
| docker rm uptime-kuma 2>/dev/null || true | |
| docker run -d \ | |
| --name uptime-kuma \ | |
| --network docsplus-network \ | |
| --restart unless-stopped \ | |
| -v uptime-kuma-data:/app/data \ | |
| --label "traefik.enable=true" \ | |
| --label "traefik.http.routers.uptime.rule=Host(\`status.docs.plus\`)" \ | |
| --label "traefik.http.routers.uptime.entrypoints=websecure" \ | |
| --label "traefik.http.routers.uptime.tls.certresolver=letsencrypt" \ | |
| --label "traefik.http.services.uptime.loadbalancer.server.port=3001" \ | |
| louislam/uptime-kuma:latest | |
| sleep 15 | |
| echo "✅ Uptime Kuma deployed at https://status.docs.plus" |