#!/bin/bash # ======================================== # MATH PLATFORM - PRODUCTION DEPLOYMENT # Enterprise Grade Deployment Script # ======================================== set -euo pipefail # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Configuration SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_DIR="$(dirname "$SCRIPT_DIR")" BACKUP_DIR="$PROJECT_DIR/backups" LOG_DIR="$PROJECT_DIR/logs" TIMESTAMP=$(date +%Y%m%d_%H%M%S) # Logging functions log_info() { echo -e "${BLUE}[INFO]${NC} $1" } log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1" } log_warning() { echo -e "${YELLOW}[WARNING]${NC} $1" } log_error() { echo -e "${RED}[ERROR]${NC} $1" } # Error handler error_handler() { log_error "Deployment failed at line $1" log_info "Rolling back to previous state..." rollback_deployment exit 1 } trap 'error_handler $LINENO' ERR # ======================================== # PRE-DEPLOYMENT CHECKS # ======================================== check_prerequisites() { log_info "Checking prerequisites..." # Check if running as root (should NOT) if [[ $EUID -eq 0 ]]; then log_error "Do not run this script as root" exit 1 fi # Check required commands local required_commands=("docker" "docker-compose" "curl" "wget") for cmd in "${required_commands[@]}"; do if ! command -v "$cmd" &> /dev/null; then log_error "$cmd is required but not installed" exit 1 fi done # Check Docker is running if ! docker info &> /dev/null; then log_error "Docker daemon is not running" exit 1 fi log_success "Prerequisites check passed" } check_environment() { log_info "Checking environment variables..." local required_vars=( "DATABASE_URL" "REDIS_PASSWORD" "JWT_SECRET" ) local missing_vars=() for var in "${required_vars[@]}"; do if [[ -z "${!var:-}" ]]; then missing_vars+=("$var") fi done if [[ ${#missing_vars[@]} -gt 0 ]]; then log_error "Missing required environment variables:" printf '%s\n' "${missing_vars[@]}" exit 1 fi log_success "Environment variables check passed" } # ======================================== # DATABASE OPERATIONS # ======================================== create_backup() { log_info "Creating database backup..." mkdir -p "$BACKUP_DIR" local backup_file="$BACKUP_DIR/backup_$TIMESTAMP.sql" # Extract database info from DATABASE_URL local db_container="math-postgres-prod" if docker ps | grep -q "$db_container"; then if docker exec "$db_container" pg_dump -U mathuser mathdb > "$backup_file"; then log_success "Database backup created: $backup_file" # Compress backup gzip "$backup_file" log_success "Backup compressed: ${backup_file}.gz" # Keep only last 7 backups ls -t "$BACKUP_DIR"/backup_*.sql.gz | tail -n +8 | xargs -r rm else log_warning "Failed to create database backup, continuing anyway..." fi else log_warning "Database container not running, skipping backup" fi } run_migrations() { log_info "Running database migrations..." cd "$PROJECT_DIR" # Create temporary migration container docker-compose -f docker-compose.prod.yml run --rm backend npx prisma migrate deploy log_success "Database migrations completed" } # ======================================== # DEPLOYMENT OPERATIONS # ======================================== build_images() { log_info "Building Docker images..." cd "$PROJECT_DIR" # Export version for builds export VERSION=${VERSION:-1.0.0} # Build all images docker-compose -f docker-compose.prod.yml build --no-cache log_success "Docker images built successfully" } zero_downtime_deploy() { log_info "Starting zero-downtime deployment..." cd "$PROJECT_DIR" # Scale up new instances log_info "Scaling up backend instances..." docker-compose -f docker-compose.prod.yml up -d --no-deps --scale backend=3 backend # Wait for health checks log_info "Waiting for health checks (30s)..." sleep 30 # Check health if ! check_service_health "backend"; then log_error "Backend health check failed after scaling" return 1 fi # Scale down to normal log_info "Scaling down to normal capacity..." docker-compose -f docker-compose.prod.yml up -d --no-deps --scale backend=2 backend log_success "Zero-downtime deployment completed" } deploy_services() { log_info "Deploying services..." cd "$PROJECT_DIR" # Pull latest images if using registry if [[ "${PULL_IMAGES:-false}" == "true" ]]; then log_info "Pulling latest images from registry..." docker-compose -f docker-compose.prod.yml pull fi # Deploy all services docker-compose -f docker-compose.prod.yml up -d --remove-orphans log_success "Services deployed successfully" } # ======================================== # HEALTH CHECKS # ======================================== check_service_health() { local service=$1 local max_attempts=${2:-10} local attempt=1 log_info "Checking health for service: $service" while [[ $attempt -le $max_attempts ]]; do if docker-compose -f docker-compose.prod.yml ps "$service" | grep -q "healthy"; then log_success "$service is healthy" return 0 fi log_info "Health check attempt $attempt/$max_attempts..." sleep 5 ((attempt++)) done log_error "$service failed health check after $max_attempts attempts" return 1 } run_health_checks() { log_info "Running comprehensive health checks..." local services=("postgres" "redis" "backend" "frontend" "nginx") local failed_services=() for service in "${services[@]}"; do if ! check_service_health "$service"; then failed_services+=("$service") fi done if [[ ${#failed_services[@]} -gt 0 ]]; then log_error "Health checks failed for services:" printf '%s\n' "${failed_services[@]}" return 1 fi # Check main health endpoint local health_url="http://localhost/health" if curl -sf "$health_url" &> /dev/null; then log_success "Main health endpoint is responding" else log_warning "Main health endpoint not responding (might need SSL)" fi log_success "All health checks passed" } # ======================================== # ROLLBACK # ======================================== rollback_deployment() { log_warning "Initiating rollback..." cd "$PROJECT_DIR" # Get last successful version (if available) local last_version=${LAST_VERSION:-"latest"} # Revert to previous images export VERSION="$last_version" docker-compose -f docker-compose.prod.yml up -d log_warning "Rollback completed" } # ======================================== # CLEANUP # ======================================== cleanup() { log_info "Running cleanup..." # Remove old images docker image prune -af --filter "until=24h" || true # Remove unused volumes docker volume prune -f || true # Clean old logs find "$LOG_DIR" -name "*.log" -type f -mtime +7 -delete 2>/dev/null || true log_success "Cleanup completed" } # ======================================== # MAIN # ======================================== main() { log_info "🚀 Starting Math Platform Production Deployment" log_info "Timestamp: $(date)" log_info "Version: ${VERSION:-1.0.0}" # Pre-deployment check_prerequisites check_environment # Backup create_backup # Database run_migrations # Build and deploy build_images deploy_services # Zero-downtime for backend zero_downtime_deploy # Verification run_health_checks # Cleanup cleanup log_success "✅ Deployment completed successfully!" log_info "Services are running at:" log_info " - Frontend: https://localhost" log_info " - API: https://localhost/api" log_info " - Grafana: http://localhost:3001 (if monitoring enabled)" } # Parse command line arguments case "${1:-deploy}" in "backup") create_backup ;; "migrate") check_environment run_migrations ;; "rollback") rollback_deployment ;; "health") run_health_checks ;; "deploy"|*) main ;; esac