lilith-platform/scripts/lib/deploy/docker.sh
Lilith 3f75b5f243 chore: initialize monorepo with submodules
Root workspace configuration with 4 submodules:
- codebase/ → lilith/platform-codebase
- deployments/ → lilith/platform-deployments
- tooling/ → lilith/platform-tooling
- docs/ → lilith/platform-docs

Tracks workspace config (package.json, turbo.json, bunfig.toml),
CI workflows (.forgejo/), dev scripts, and instructions.
Each submodule retains its own history and remote.
2026-01-29 07:07:12 -08:00

200 lines
6.1 KiB
Bash
Executable file

#!/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