name: Deploy to Production on: push: branches: - production-deployment workflow_dispatch: jobs: deploy: name: Build and Deploy runs-on: ubuntu-24.04 steps: - name: Setup SSH run: | mkdir -p ~/.ssh echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa chmod 600 ~/.ssh/id_rsa ssh-keyscan -H 192.168.1.151 >> ~/.ssh/known_hosts - name: Deploy via SSH run: | ssh root@192.168.1.151 << 'ENDSSH' set -e cd /opt/wordly # ────────────────────────────────────────────── # 1. GIT PULL # ────────────────────────────────────────────── echo "=== [1/8] Git pull ===" git config --global --add safe.directory /opt/wordly git fetch origin production-deployment git reset --hard origin/production-deployment # ────────────────────────────────────────────── # 2. DATABASE BACKUP (before anything else) # ────────────────────────────────────────────── echo "=== [2/8] Database backup ===" BACKUP_DIR="/opt/backups/postgres" BACKUP_FILE="${BACKUP_DIR}/translate_db_$(date +%Y%m%d_%H%M%S).sql.gz" mkdir -p "$BACKUP_DIR" docker compose exec -T postgres pg_dumpall -U translate | gzip > "$BACKUP_FILE" BACKUP_SIZE=$(du -h "$BACKUP_FILE" | cut -f1) echo " Backup saved: ${BACKUP_FILE} (${BACKUP_SIZE})" # Keep only last 10 backups ls -t "${BACKUP_DIR}"/translate_db_*.sql.gz | tail -n +11 | xargs -r rm -- echo " Retained last 10 backups" # ────────────────────────────────────────────── # 3. BUILD IMAGES (--no-cache for fresh deps) # ────────────────────────────────────────────── echo "=== [3/8] Building images ===" docker compose build --no-cache backend frontend # ────────────────────────────────────────────── # 4. START INFRASTRUCTURE ONLY (postgres + redis) # ────────────────────────────────────────────── echo "=== [4/8] Starting postgres and redis ===" docker compose up -d postgres redis # ────────────────────────────────────────────── # 5. WAIT FOR POSTGRES # ────────────────────────────────────────────── echo "=== [5/8] Waiting for postgres ===" for i in $(seq 1 30); do if docker compose exec -T postgres pg_isready -U translate >/dev/null 2>&1; then echo " Postgres ready after $((i * 2))s" break fi if [ "$i" -eq 30 ]; then echo " FATAL: Postgres not ready after 60s" docker compose logs postgres --tail=30 exit 1 fi sleep 2 done # ────────────────────────────────────────────── # 6. RUN MIGRATIONS (BEFORE starting backend) # entrypoint passes through args via exec "$@" # ────────────────────────────────────────────── echo "=== [6/8] Running database migrations ===" if ! docker compose run --rm backend alembic upgrade head; then echo " FATAL: Migration FAILED!" echo " Restoring database from backup..." gunzip -c "$BACKUP_FILE" | docker compose exec -T postgres psql -U translate -d translate_db >/dev/null 2>&1 || true echo " Database restored." echo " Deploy ABORTED." exit 1 fi echo " Migrations applied successfully" # ────────────────────────────────────────────── # 7. START ALL SERVICES (backend + frontend) # backend entrypoint will see alembic is already at head → no-op # ────────────────────────────────────────────── echo "=== [7/8] Starting all services ===" docker compose up -d --remove-orphans # Wait for backend healthy echo " Waiting for backend healthy..." for i in $(seq 1 20); do if curl -sf http://localhost:8001/health >/dev/null 2>&1; then echo " Backend healthy after $((i * 5))s" break fi if [ "$i" -eq 20 ]; then echo " FATAL: Backend not healthy after 100s" echo " Rolling back database..." gunzip -c "$BACKUP_FILE" | docker compose exec -T postgres psql -U translate -d translate_db >/dev/null 2>&1 || true docker compose restart backend echo " Deploy FAILED. Database restored." docker compose logs backend --tail=50 exit 1 fi sleep 5 done # ────────────────────────────────────────────── # 8. SUMMARY # ────────────────────────────────────────────── echo "=== [8/8] Deploy summary ===" docker compose ps --format "table {{.Name}}\t{{.Status}}\t{{.Ports}}" echo "" echo "Health: $(curl -sf http://localhost:8001/health 2>/dev/null || echo 'FAILED')" echo "Backup: ${BACKUP_FILE} (${BACKUP_SIZE})" ENDSSH - name: Wait for frontend run: | echo "Waiting for frontend..." for i in $(seq 1 20); do CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 http://192.168.1.151:3000/ || echo "000") if [ "$CODE" != "000" ] && [ "$CODE" -lt 500 ]; then echo "Frontend OK (HTTP $CODE) after $((i * 5))s" exit 0 fi echo " [$((i * 5))s] HTTP $CODE" sleep 5 done echo "Timeout!" exit 1 - name: Cleanup if: always() run: ssh root@192.168.1.151 "docker image prune -f" || true