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.
200 lines
6.1 KiB
Bash
Executable file
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
|