#!/bin/bash set -e # 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 ENVIRONMENT=${1:-production} COMPOSE_FILE="docker-compose.production.yml" BACKUP_DIR="./backups/$(date +%Y%m%d_%H%M%S)" ROLLBACK_DIR="./rollback-backups" MAX_BACKUPS=10 # Function to print colored messages print_info() { echo -e "${BLUE}ℹ️ $1${NC}" } print_success() { echo -e "${GREEN}✅ $1${NC}" } print_warning() { echo -e "${YELLOW}⚠️ $1${NC}" } print_error() { echo -e "${RED}❌ $1${NC}" } # Function to check prerequisites check_prerequisites() { print_info "Checking prerequisites..." command -v docker >/dev/null 2>&1 || { print_error "Docker is required but not installed. Aborting."; exit 1; } command -v docker-compose >/dev/null 2>&1 || { print_error "Docker Compose is required but not installed. Aborting."; exit 1; } if ! docker info >/dev/null 2>&1; then print_error "Docker daemon is not running. Please start Docker and try again."; exit 1; fi if [ ! -f "$COMPOSE_FILE" ]; then print_error "$COMPOSE_FILE not found. Aborting."; exit 1; fi print_success "Prerequisites check passed" } # Function to create backup create_backup() { print_info "Creating backup..." mkdir -p "${BACKUP_DIR}" mkdir -p "${ROLLBACK_DIR}" # Backup database print_info "Backing up database..." if docker-compose -f "$COMPOSE_FILE" ps postgres | grep -q "Up"; then docker-compose -f "$COMPOSE_FILE" exec -T postgres pg_dump -U "${POSTGRES_USER:-veza_user}" "${POSTGRES_DB:-veza_db}" > "${BACKUP_DIR}/database.sql" 2>/dev/null || { print_warning "Database backup failed (may not be critical if database is empty)" } else print_warning "PostgreSQL container is not running, skipping database backup" fi # Backup Redis data print_info "Backing up Redis data..." if docker-compose -f "$COMPOSE_FILE" ps redis | grep -q "Up"; then docker-compose -f "$COMPOSE_FILE" exec -T redis redis-cli --rdb - > "${BACKUP_DIR}/redis.rdb" 2>/dev/null || { print_warning "Redis backup failed (may not be critical)" } else print_warning "Redis container is not running, skipping Redis backup" fi # Save current image tags print_info "Saving current image tags..." docker-compose -f "$COMPOSE_FILE" config | grep "image:" > "${BACKUP_DIR}/image-tags.txt" || true # Copy current docker-compose file cp "$COMPOSE_FILE" "${BACKUP_DIR}/docker-compose.production.yml" # Create rollback backup cp -r "${BACKUP_DIR}" "${ROLLBACK_DIR}/latest" print_success "Backup created at ${BACKUP_DIR}" } # Function to cleanup old backups cleanup_old_backups() { print_info "Cleaning up old backups (keeping last ${MAX_BACKUPS})..." if [ -d "./backups" ]; then cd ./backups ls -t | tail -n +$((MAX_BACKUPS + 1)) | xargs rm -rf 2>/dev/null || true cd .. fi print_success "Old backups cleaned up" } # Function to pull latest images pull_latest_images() { print_info "Pulling latest images..." docker-compose -f "$COMPOSE_FILE" pull || { print_error "Failed to pull images"; exit 1; } print_success "Images pulled successfully" } # Function to deploy services deploy_services() { print_info "Deploying services with zero-downtime strategy..." # Deploy services one by one to minimize downtime SERVICES=("backend-api" "chat-server" "stream-server" "frontend") for service in "${SERVICES[@]}"; do if docker-compose -f "$COMPOSE_FILE" ps "$service" | grep -q "Up"; then print_info "Updating ${service}..." docker-compose -f "$COMPOSE_FILE" up -d --no-deps "$service" || { print_error "Failed to deploy ${service}"; return 1; } sleep 5 fi done print_success "Services deployed successfully" } # Function to check health check_health() { local service=$1 local url=$2 local name=$3 local max_attempts=${4:-30} local attempt=0 print_info "Checking ${name} health..." while [ $attempt -lt $max_attempts ]; do if curl -sf "$url" >/dev/null 2>&1; then print_success "${name} is healthy" return 0 fi attempt=$((attempt + 1)) sleep 2 done print_error "${name} health check failed after ${max_attempts} attempts" return 1 } # Function to verify deployment verify_deployment() { print_info "Verifying deployment..." local failed=0 # Check container status if docker-compose -f "$COMPOSE_FILE" ps | grep -q "unhealthy\|Exited\|Restarting"; then print_error "Some containers are unhealthy or stopped" docker-compose -f "$COMPOSE_FILE" ps failed=1 fi # Check health endpoints check_health "backend-api" "http://localhost:8080/health" "Backend API" 15 || failed=1 check_health "chat-server" "http://localhost:8081/health" "Chat Server" 15 || failed=1 check_health "stream-server" "http://localhost:8082/health" "Stream Server" 15 || failed=1 check_health "frontend" "http://localhost:80/health" "Frontend" 15 || failed=1 if [ $failed -eq 1 ]; then return 1 fi print_success "Deployment verification passed" return 0 } # Function to rollback rollback() { print_error "Deployment failed! Initiating rollback..." if [ ! -d "${ROLLBACK_DIR}/latest" ]; then print_error "No rollback backup found. Manual intervention required." exit 1 fi print_info "Rolling back to previous version..." # Stop current services docker-compose -f "$COMPOSE_FILE" down || true # Restore docker-compose file if [ -f "${ROLLBACK_DIR}/latest/docker-compose.production.yml" ]; then cp "${ROLLBACK_DIR}/latest/docker-compose.production.yml" "$COMPOSE_FILE" fi # Restore database if [ -f "${ROLLBACK_DIR}/latest/database.sql" ]; then print_info "Restoring database..." docker-compose -f "$COMPOSE_FILE" up -d postgres sleep 10 docker-compose -f "$COMPOSE_FILE" exec -T postgres psql -U "${POSTGRES_USER:-veza_user}" -d "${POSTGRES_DB:-veza_db}" < "${ROLLBACK_DIR}/latest/database.sql" || true fi # Start services with previous images docker-compose -f "$COMPOSE_FILE" up -d print_warning "Rollback completed. Please verify services manually." exit 1 } # Main deployment flow main() { print_info "Starting production deployment to ${ENVIRONMENT}..." # Trap errors and rollback trap rollback ERR check_prerequisites create_backup cleanup_old_backups pull_latest_images deploy_services # Wait for services to start print_info "Waiting for services to start..." sleep 30 # Verify deployment if ! verify_deployment; then rollback fi # Disable trap on success trap - ERR print_success "Deployment to ${ENVIRONMENT} completed successfully!" print_info "Backup location: ${BACKUP_DIR}" print_info "To rollback, run: ${ROLLBACK_DIR}/latest" } # Run main function main