#!/bin/bash # # Docker Blue-Green Deployment Library # # Implements zero-downtime blue-green deployments for Docker services. # New version runs on alternate port, nginx switches only after health checks pass. # # Usage: # source scripts/lib/deploy/docker.sh # deploy_docker_service_blue_green "webmap-router" # set -e set -u # Service port mappings (primary/secondary) declare -A SERVICE_PORTS SERVICE_PORTS=( ["webmap-router"]="4002:4003" ["platform"]="4000:4001" ["drive"]="3002:3003" ) get_active_port() { local SERVICE="$1" local VPS_HOST="${VPS_HOST:-0.1984.nasty.sh}" local VPS_USER="${VPS_USER:-root}" # Query nginx config to see which port is active local CONTAINER_STATUS=$(ssh "${VPS_USER}@${VPS_HOST}" \ "docker ps --filter 'name=lilith-platform-prod-${SERVICE}' --format '{{.Names}}:{{.Ports}}'" 2>/dev/null) if [ -z "$CONTAINER_STATUS" ]; then # No container running, use primary port echo "${SERVICE_PORTS[$SERVICE]%%:*}" return fi # Extract current port from container ports local CURRENT_PORT=$(echo "$CONTAINER_STATUS" | grep -oE '[0-9]+->4[0-9]{3}' | cut -d'-' -f1) if [ -z "$CURRENT_PORT" ]; then # Default to primary port echo "${SERVICE_PORTS[$SERVICE]%%:*}" else echo "$CURRENT_PORT" fi } get_next_port() { local SERVICE="$1" local CURRENT_PORT=$(get_active_port "$SERVICE") local PRIMARY_PORT="${SERVICE_PORTS[$SERVICE]%%:*}" local SECONDARY_PORT="${SERVICE_PORTS[$SERVICE]##*:}" if [ "$CURRENT_PORT" = "$PRIMARY_PORT" ]; then echo "$SECONDARY_PORT" else echo "$PRIMARY_PORT" fi } deploy_docker_service_blue_green() { local SERVICE="$1" local VPS_HOST="${VPS_HOST:-0.1984.nasty.sh}" local VPS_USER="${VPS_USER:-root}" local COMPOSE_PATH="/opt/lilith-platform/infrastructure/docker" local HEALTH_CHECK_TIMEOUT="${DOCKER_HEALTH_CHECK_TIMEOUT:-60}" log_info "Deploying Docker service: $SERVICE (blue-green)" # Step 1: Detect current and next ports local CURRENT_PORT=$(get_active_port "$SERVICE") local NEXT_PORT=$(get_next_port "$SERVICE") log_info "Current port: $CURRENT_PORT, Next port: $NEXT_PORT" # Step 2: Pull new image log_info "Pulling new image for $SERVICE..." ssh "${VPS_USER}@${VPS_HOST}" \ "cd $COMPOSE_PATH && docker compose -f docker-compose.prod.yml pull $SERVICE" || { log_error "Failed to pull image for $SERVICE" return 1 } # Step 3: Start new container on next port log_info "Starting new container on port $NEXT_PORT..." ssh "${VPS_USER}@${VPS_HOST}" \ "cd $COMPOSE_PATH && docker run -d \ --name lilith-platform-prod-${SERVICE}-new \ --network lilith-network \ -p ${NEXT_PORT}:$(get_service_internal_port $SERVICE) \ --env-file .env \ lilith-platform-${SERVICE}:latest" || { log_error "Failed to start new container for $SERVICE" return 1 } # Step 4: Wait for health check log_info "Waiting for health check (timeout: ${HEALTH_CHECK_TIMEOUT}s)..." local ELAPSED=0 local HEALTH_ENDPOINT="http://localhost:${NEXT_PORT}$(get_service_health_path $SERVICE)" while [ $ELAPSED -lt $HEALTH_CHECK_TIMEOUT ]; do local HEALTH=$(ssh "${VPS_USER}@${VPS_HOST}" \ "curl -sf $HEALTH_ENDPOINT" 2>/dev/null) if [ $? -eq 0 ]; then log_info "$SERVICE is healthy on port $NEXT_PORT ✓" break fi sleep 5 ELAPSED=$((ELAPSED + 5)) done if [ $ELAPSED -ge $HEALTH_CHECK_TIMEOUT ]; then log_error "$SERVICE health check failed after ${HEALTH_CHECK_TIMEOUT}s" log_error "Cleaning up failed deployment..." ssh "${VPS_USER}@${VPS_HOST}" \ "docker stop lilith-platform-prod-${SERVICE}-new && docker rm lilith-platform-prod-${SERVICE}-new" return 1 fi # Step 5: Update nginx upstream (handled by separate script) log_info "Updating nginx upstream..." update_nginx_upstream "$SERVICE" "$NEXT_PORT" || { log_error "Failed to update nginx for $SERVICE" # Cleanup new container ssh "${VPS_USER}@${VPS_HOST}" \ "docker stop lilith-platform-prod-${SERVICE}-new && docker rm lilith-platform-prod-${SERVICE}-new" return 1 } # Step 6: Stop old container after grace period log_info "Waiting 30s grace period before stopping old container..." sleep 30 log_info "Stopping old container on port $CURRENT_PORT..." ssh "${VPS_USER}@${VPS_HOST}" \ "docker stop lilith-platform-prod-${SERVICE} 2>/dev/null || true" || true # Step 7: Rename new container to standard name ssh "${VPS_USER}@${VPS_HOST}" \ "docker rm lilith-platform-prod-${SERVICE} 2>/dev/null || true && \ docker rename lilith-platform-prod-${SERVICE}-new lilith-platform-prod-${SERVICE}" log_info "✅ $SERVICE deployed successfully (blue-green complete)" return 0 } deploy_docker_services() { local SERVICES="$1" # Deploy in dependency order local DEPLOY_ORDER=("drive" "platform" "webmap-router") for SERVICE in "${DEPLOY_ORDER[@]}"; do if echo "$SERVICES" | grep -q "$SERVICE"; then if ! deploy_docker_service_blue_green "$SERVICE"; then log_error "Deployment failed for $SERVICE" return 1 fi # Pause between services for stability sleep 5 fi done log_info "All Docker services deployed successfully" return 0 } get_service_internal_port() { case "$1" in webmap-router) echo "4002" ;; platform) echo "4000" ;; drive) echo "3002" ;; *) echo "8080" ;; esac } get_service_health_path() { case "$1" in webmap-router) echo "/health" ;; platform) echo "/api/health" ;; drive) echo "/health" ;; *) echo "/health" ;; esac } # Export functions export -f get_active_port export -f get_next_port export -f deploy_docker_service_blue_green export -f deploy_docker_services export -f get_service_internal_port export -f get_service_health_path