#!/bin/bash # ROA2WEB Production Deployment Script # Zero-downtime deployment with health checks and rollback capability set -e # Configuration SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_DIR="$(dirname "$SCRIPT_DIR")" BACKUP_DIR="$PROJECT_DIR/backups" LOG_FILE="$PROJECT_DIR/deploy.log" MAX_RETRIES=3 HEALTH_CHECK_TIMEOUT=60 # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Logging function log() { local level=$1 shift local message="$*" local timestamp=$(date '+%Y-%m-%d %H:%M:%S') echo -e "[$timestamp] [$level] $message" | tee -a "$LOG_FILE" } # Error handling error_exit() { log "ERROR" "$1" exit 1 } # Success message success() { log "SUCCESS" "$1" } # Warning message warning() { log "WARNING" "$1" } # Info message info() { log "INFO" "$1" } # Check if running as root check_root() { if [[ $EUID -eq 0 ]]; then error_exit "This script should not be run as root for security reasons" fi } # Check prerequisites check_prerequisites() { info "Checking prerequisites..." # Check if Docker is installed and running if ! command -v docker &> /dev/null; then error_exit "Docker is not installed" fi if ! docker info &> /dev/null; then error_exit "Docker daemon is not running" fi # Check if Docker Compose is installed if ! command -v docker-compose &> /dev/null; then error_exit "Docker Compose is not installed" fi # Check if required files exist if [[ ! -f "$PROJECT_DIR/.env.production" ]]; then error_exit "Production environment file (.env.production) not found" fi if [[ ! -f "$PROJECT_DIR/docker-compose.yml" ]]; then error_exit "Docker Compose file not found" fi if [[ ! -f "$PROJECT_DIR/docker-compose.production.yml" ]]; then error_exit "Production Docker Compose file not found" fi success "Prerequisites check passed" } # Create backup directory create_backup_dir() { if [[ ! -d "$BACKUP_DIR" ]]; then mkdir -p "$BACKUP_DIR" info "Created backup directory: $BACKUP_DIR" fi } # Create backup of current deployment create_backup() { info "Creating backup of current deployment..." local backup_name="backup_$(date +%Y%m%d_%H%M%S)" local backup_path="$BACKUP_DIR/$backup_name" # Create backup directory mkdir -p "$backup_path" # Backup Docker images info "Backing up Docker images..." docker save -o "$backup_path/images.tar" \ roa2web/backend:latest \ roa2web/frontend:latest \ roa2web/nginx-gateway:latest 2>/dev/null || warning "Some images may not exist yet" # Backup configuration files cp -r "$PROJECT_DIR"/.env* "$backup_path/" 2>/dev/null || true cp -r "$PROJECT_DIR"/docker-compose*.yml "$backup_path/" cp -r "$PROJECT_DIR"/nginx/conf "$backup_path/" 2>/dev/null || true # Backup volumes data info "Backing up volume data..." docker run --rm -v roa2web_nginx-logs:/data -v "$backup_path":/backup alpine tar czf /backup/nginx-logs.tar.gz -C /data . 2>/dev/null || warning "Nginx logs backup failed" docker run --rm -v roa2web_ssl-certs:/data -v "$backup_path":/backup alpine tar czf /backup/ssl-certs.tar.gz -C /data . 2>/dev/null || warning "SSL certs backup failed" docker run --rm -v roa2web_redis-data:/data -v "$backup_path":/backup alpine tar czf /backup/redis-data.tar.gz -C /data . 2>/dev/null || warning "Redis data backup failed" echo "$backup_name" > "$PROJECT_DIR/.last_backup" success "Backup created: $backup_name" } # Health check function health_check() { local service=$1 local url=$2 local timeout=${3:-30} info "Performing health check for $service..." local count=0 while [[ $count -lt $timeout ]]; do if curl -f -s "$url" > /dev/null 2>&1; then success "$service health check passed" return 0 fi sleep 1 ((count++)) done error_exit "$service health check failed after ${timeout}s" } # Wait for services to be healthy wait_for_services() { info "Waiting for services to be healthy..." # Wait for containers to be running sleep 10 # Check backend health health_check "Backend API" "http://localhost/api/health" $HEALTH_CHECK_TIMEOUT # Check frontend health health_check "Frontend" "http://localhost/health" $HEALTH_CHECK_TIMEOUT # Check gateway health health_check "Gateway" "http://localhost/health" $HEALTH_CHECK_TIMEOUT success "All services are healthy" } # Deploy function deploy() { info "Starting deployment..." cd "$PROJECT_DIR" # Load production environment set -a source .env.production set +a # Build images info "Building Docker images..." docker-compose -f docker-compose.yml -f docker-compose.production.yml build --no-cache # Pull any updated base images info "Pulling base images..." docker-compose -f docker-compose.yml -f docker-compose.production.yml pull # Deploy with zero downtime info "Deploying services..." docker-compose -f docker-compose.yml -f docker-compose.production.yml up -d --force-recreate # Wait for services to be ready wait_for_services # Clean up old images info "Cleaning up old images..." docker image prune -f success "Deployment completed successfully!" } # Rollback function rollback() { local backup_name=$1 if [[ -z "$backup_name" ]]; then if [[ -f "$PROJECT_DIR/.last_backup" ]]; then backup_name=$(cat "$PROJECT_DIR/.last_backup") else error_exit "No backup specified and no last backup found" fi fi local backup_path="$BACKUP_DIR/$backup_name" if [[ ! -d "$backup_path" ]]; then error_exit "Backup not found: $backup_name" fi warning "Rolling back to backup: $backup_name" # Stop current services docker-compose -f docker-compose.yml -f docker-compose.production.yml down # Restore images if [[ -f "$backup_path/images.tar" ]]; then docker load -i "$backup_path/images.tar" fi # Restore configuration cp "$backup_path"/.env* "$PROJECT_DIR/" # Start services docker-compose -f docker-compose.yml -f docker-compose.production.yml up -d # Wait for services wait_for_services success "Rollback completed successfully!" } # Main deployment workflow main() { local action=${1:-deploy} case $action in "deploy") info "=== ROA2WEB Production Deployment ===" check_root check_prerequisites create_backup_dir create_backup deploy ;; "rollback") info "=== ROA2WEB Rollback ===" check_root rollback "$2" ;; "health-check") info "=== ROA2WEB Health Check ===" wait_for_services ;; *) echo "Usage: $0 {deploy|rollback [backup_name]|health-check}" echo "" echo "Commands:" echo " deploy - Deploy the application to production" echo " rollback - Rollback to the last backup or specified backup" echo " health-check - Perform health check on running services" exit 1 ;; esac } # Run main function with all arguments main "$@"