#!/usr/bin/env bash # ============================================================================== # GoodGo Platform — Manual Production Deploy Script # Backup for CI/CD pipeline. Use when GitHub Actions is unavailable. # # Usage (from the server): # cd ~/goodgo # ./deploy-production.sh [image-tag] # # Usage (from local machine): # ssh ubuntu@185.225.232.65 'cd ~/goodgo && ./deploy-production.sh abc1234' # ============================================================================== set -euo pipefail # ── Configuration ───────────────────────────────────────────────────────────── COMPOSE_FILE="docker-compose.prod.yml" IMAGE_TAG="${1:-latest}" HEALTH_URL="http://127.0.0.1:3001/health" HEALTH_RETRIES=15 HEALTH_INTERVAL=5 ROLLBACK_ON_FAIL=true # ── Colors ──────────────────────────────────────────────────────────────────── RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' NC='\033[0m' log() { echo -e "${GREEN}[DEPLOY]${NC} $(date +%H:%M:%S) $*"; } warn() { echo -e "${YELLOW}[WARN]${NC} $(date +%H:%M:%S) $*"; } err() { echo -e "${RED}[ERROR]${NC} $(date +%H:%M:%S) $*" >&2; } info() { echo -e "${CYAN}[INFO]${NC} $(date +%H:%M:%S) $*"; } # ── Pre-flight Checks ──────────────────────────────────────────────────────── if [ ! -f "$COMPOSE_FILE" ]; then err "Compose file not found: $COMPOSE_FILE" err "Are you in the ~/goodgo directory?" exit 1 fi if [ ! -f ".env" ]; then err ".env file not found. Copy from infra/env.production.example" exit 1 fi log "==========================================" log " GoodGo Platform — Production Deploy" log " Image tag: ${IMAGE_TAG}" log " Compose: ${COMPOSE_FILE}" log "==========================================" echo "" # ── Step 1: Record Current State & Tag for Rollback ────────────────────────── log "Step 1/7: Recording current state and tagging rollback images..." PREV_API=$(docker inspect --format='{{.Config.Image}}' goodgo-api 2>/dev/null || echo "none") PREV_WEB=$(docker inspect --format='{{.Config.Image}}' goodgo-web 2>/dev/null || echo "none") PREV_AI=$(docker inspect --format='{{.Config.Image}}' goodgo-ai-services 2>/dev/null || echo "none") info "Previous API: ${PREV_API}" info "Previous Web: ${PREV_WEB}" info "Previous AI: ${PREV_AI}" # Tag current images as :rollback so they survive docker image prune if [ "$PREV_API" != "none" ]; then docker tag "$PREV_API" goodgo-api:rollback 2>/dev/null || warn "Could not tag API rollback image" info "Tagged API rollback: goodgo-api:rollback" fi if [ "$PREV_WEB" != "none" ]; then docker tag "$PREV_WEB" goodgo-web:rollback 2>/dev/null || warn "Could not tag Web rollback image" info "Tagged Web rollback: goodgo-web:rollback" fi if [ "$PREV_AI" != "none" ]; then docker tag "$PREV_AI" goodgo-ai-services:rollback 2>/dev/null || warn "Could not tag AI rollback image" info "Tagged AI rollback: goodgo-ai-services:rollback" fi # ── Step 2: Pull New Images ────────────────────────────────────────────────── log "Step 2/7: Pulling new images (tag: ${IMAGE_TAG})..." export IMAGE_TAG docker compose -f "$COMPOSE_FILE" pull api web ai-services log "Images pulled successfully." # ── Step 3: Rolling Update ─────────────────────────────────────────────────── log "Step 3/7: Rolling update (zero-downtime)..." info "Updating API..." docker compose -f "$COMPOSE_FILE" up -d --no-deps --wait api info "API updated and healthy." info "Updating Web..." docker compose -f "$COMPOSE_FILE" up -d --no-deps --wait web info "Web updated and healthy." info "Updating AI Services..." docker compose -f "$COMPOSE_FILE" up -d --no-deps --wait ai-services info "AI Services updated and healthy." log "Rolling update complete." # ── Step 4: Database Migrations ────────────────────────────────────────────── log "Step 4/7: Running database migrations..." docker compose -f "$COMPOSE_FILE" exec -T api npx prisma migrate deploy log "Migrations complete." # ── Step 5: Health Check Verification ──────────────────────────────────────── log "Step 5/7: Verifying deployment health..." HEALTHY=false for i in $(seq 1 "$HEALTH_RETRIES"); do if curl -sf "$HEALTH_URL" > /dev/null 2>&1; then HEALTHY=true break fi info "Waiting for health check... (${i}/${HEALTH_RETRIES})" sleep "$HEALTH_INTERVAL" done if $HEALTHY; then log "Health check passed!" else err "Health check failed after ${HEALTH_RETRIES} attempts!" if $ROLLBACK_ON_FAIL; then warn "Initiating rollback using tagged rollback images..." # Rollback: stop current and restart with explicitly tagged rollback images docker compose -f "$COMPOSE_FILE" stop api web ai-services # Restore from :rollback tags if available if docker image inspect goodgo-api:rollback > /dev/null 2>&1; then info "Restoring API from goodgo-api:rollback" docker tag goodgo-api:rollback "$PREV_API" 2>/dev/null || true fi if docker image inspect goodgo-web:rollback > /dev/null 2>&1; then info "Restoring Web from goodgo-web:rollback" docker tag goodgo-web:rollback "$PREV_WEB" 2>/dev/null || true fi if docker image inspect goodgo-ai-services:rollback > /dev/null 2>&1; then info "Restoring AI from goodgo-ai-services:rollback" docker tag goodgo-ai-services:rollback "$PREV_AI" 2>/dev/null || true fi docker compose -f "$COMPOSE_FILE" up -d --wait api web ai-services warn "Rollback complete. Verifying..." sleep 5 if curl -sf "$HEALTH_URL" > /dev/null 2>&1; then warn "Services recovered after rollback." else err "CRITICAL: Services still unhealthy after rollback!" err "Manual intervention required." fi fi exit 1 fi # ── Step 6: Smoke Tests ───────────────────────────────────────────────────── log "Step 6/7: Running smoke tests..." SMOKE_PASSED=false SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" if [ -x "$SCRIPT_DIR/smoke-test.sh" ]; then if "$SCRIPT_DIR/smoke-test.sh" "http://127.0.0.1:3001"; then SMOKE_PASSED=true log "Smoke tests passed!" else err "Smoke tests FAILED!" if $ROLLBACK_ON_FAIL; then warn "Initiating rollback due to smoke test failure..." docker compose -f "$COMPOSE_FILE" stop api web ai-services if docker image inspect goodgo-api:rollback > /dev/null 2>&1; then docker tag goodgo-api:rollback "$PREV_API" 2>/dev/null || true fi if docker image inspect goodgo-web:rollback > /dev/null 2>&1; then docker tag goodgo-web:rollback "$PREV_WEB" 2>/dev/null || true fi if docker image inspect goodgo-ai-services:rollback > /dev/null 2>&1; then docker tag goodgo-ai-services:rollback "$PREV_AI" 2>/dev/null || true fi docker compose -f "$COMPOSE_FILE" up -d --wait api web ai-services warn "Rollback complete. Verifying..." sleep 5 if curl -sf "$HEALTH_URL" > /dev/null 2>&1; then warn "Services recovered after rollback." else err "CRITICAL: Services still unhealthy after rollback!" fi fi exit 1 fi else warn "Smoke test script not found at $SCRIPT_DIR/smoke-test.sh — skipping." warn "Run manually: ./scripts/smoke-test.sh https://api.goodgo.vn" SMOKE_PASSED=true fi # ── Step 7: Cleanup (only after smoke tests pass) ─────────────────────────── log "Step 7/7: Cleaning up old images..." # Remove the :rollback tags first (they are no longer needed after a successful deploy) docker rmi goodgo-api:rollback goodgo-web:rollback goodgo-ai-services:rollback 2>/dev/null || true docker image prune -f log "Cleanup complete." # ── Summary ────────────────────────────────────────────────────────────────── echo "" log "==========================================" log " Deployment successful!" log "==========================================" log "" log " Services:" info " API: $(docker inspect --format='{{.Config.Image}}' goodgo-api)" info " Web: $(docker inspect --format='{{.Config.Image}}' goodgo-web)" info " AI: $(docker inspect --format='{{.Config.Image}}' goodgo-ai-services)" log "" log " Endpoints:" info " Web: https://platform.goodgo.vn" info " API: https://api.goodgo.vn" info " Grafana: https://grafana.goodgo.vn" log "" if $SMOKE_PASSED; then log " Smoke tests: PASSED" else log " Run smoke tests against public URL:" info " ./scripts/smoke-test.sh https://api.goodgo.vn" fi log "=========================================="