#!/bin/bash # docker-deployment-manager.sh - Manage Docker stack deployments across multiple servers # Author: Shell Repository # Description: Deploy specific Docker stacks to designated servers while maintaining monorepo structure set -e # Colors for output GREEN='\033[0;32m' YELLOW='\033[0;33m' RED='\033[0;31m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Configuration SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" DOCKER_DIR="$HOME/docker" DEPLOYMENT_CONFIG_DIR="$HOME/.docker-deployment" LOG_FILE="$SCRIPT_DIR/logs/deployment.log" # Ensure directories exist mkdir -p "$(dirname "$LOG_FILE")" mkdir -p "$DEPLOYMENT_CONFIG_DIR"/{config,servers,stacks,logs} # Logging function log() { echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE" } # Display usage information usage() { echo "Usage: $0 [OPTIONS] [COMMAND]" echo "" echo "Manage Docker stack deployments across multiple servers" echo "" echo "Commands:" echo " init Initialize deployment configuration" echo " map Show stack-to-server mapping" echo " deploy SERVER Deploy stacks to specific server" echo " deploy-all Deploy all stacks to their designated servers" echo " status SERVER Check deployment status on server" echo " sync-env SERVER Sync environment files to server" echo " rollback SERVER Rollback to previous deployment" echo "" echo "Options:" echo " -h, --help Show this help message" echo " -d, --dry-run Show what would be deployed without doing it" echo " -f, --force Force deployment even if checks fail" echo " -v, --verbose Verbose output" echo " --config-only Only sync configuration files" echo " --env-only Only sync environment files" echo "" echo "Examples:" echo " $0 init # First time setup" echo " $0 map # See what goes where" echo " $0 deploy europa # Deploy Europa stacks" echo " $0 deploy-all --dry-run # Test full deployment" echo " $0 sync-env io # Sync .env files to IO server" } # Initialize deployment configuration init_deployment_config() { echo -e "${YELLOW}Initializing Docker deployment configuration...${NC}" # Create main configuration file cat > "$DEPLOYMENT_CONFIG_DIR/config.yml" << 'EOF' # Docker Deployment Manager Configuration # This file defines global settings for stack deployment across servers deployment: version: "1.0" docker_dir: "~/docker" backup_before_deploy: true health_check_timeout: 30 rollback_on_failure: true # Multi-server stacks - these will be deployed to ALL servers multi_server_stacks: - dozzle # Docker log viewer - dockge # Docker compose management - diun # Docker image update notifier notifications: enabled: true webhook_url: "https://notify.peterwood.rocks/lab" tags: ["deployment", "docker"] logging: level: "info" retain_days: 30 security: verify_checksums: true backup_env_files: true use_secure_transfer: true EOF # Create server configurations based on existing crontab analysis cat > "$DEPLOYMENT_CONFIG_DIR/servers/europa.yml" << 'EOF' # Europa Server Configuration - Media Server name: "europa" role: "media-server" description: "Primary media streaming and web services server" connection: hostname: "europa" user: "acedanger" ssh_key: "~/.ssh/id_rsa" docker_compose_dir: "~/docker" stacks: - plex - jellyfin - traefik - nextcloud - photoprism - immich resources: cpu_cores: 4 memory_gb: 8 storage_gb: 500 monitoring: health_check_url: "http://europa:8080/health" required_services: - "traefik" - "plex" EOF cat > "$DEPLOYMENT_CONFIG_DIR/servers/io.yml" << 'EOF' # IO Server Configuration - Download/Acquisition Server name: "io" role: "download-server" description: "Media acquisition and download management server" connection: hostname: "io" user: "acedanger" ssh_key: "~/.ssh/id_rsa" docker_compose_dir: "~/docker" stacks: - radarr - sonarr - lidarr - sabnzbd - qbittorrent - prowlarr - overseerr resources: cpu_cores: 2 memory_gb: 4 storage_gb: 200 monitoring: health_check_url: "http://io:8080/health" required_services: - "sabnzbd" - "radarr" - "sonarr" EOF cat > "$DEPLOYMENT_CONFIG_DIR/servers/racknerd.yml" << 'EOF' # Racknerd Server Configuration - Backup Server name: "racknerd" role: "backup-server" description: "Backup, monitoring, and utility services server" connection: hostname: "racknerd" user: "acedanger" ssh_key: "~/.ssh/id_rsa" docker_compose_dir: "~/docker" stacks: - grafana - prometheus - uptime-kuma - vaultwarden - portainer - watchtower resources: cpu_cores: 1 memory_gb: 2 storage_gb: 100 monitoring: health_check_url: "http://racknerd:8080/health" required_services: - "uptime-kuma" - "vaultwarden" EOF # Create stack metadata examples if [ -d "$DOCKER_DIR/plex" ]; then cat > "$DEPLOYMENT_CONFIG_DIR/stacks/plex.yml" << 'EOF' # Plex Stack Deployment Configuration name: "plex" description: "Plex Media Server" deployment: servers: ["europa"] priority: "high" dependencies: ["traefik"] restart_policy: "unless-stopped" health_check: enabled: true url: "http://localhost:32400/web" timeout: 30 retries: 3 volumes: - "/mnt/media:/media:ro" - "/mnt/share/plex-config:/config" environment: - "PLEX_UID=1000" - "PLEX_GID=1000" - "TZ=America/New_York" backup: enabled: true schedule: "0 2 * * *" retention_days: 7 EOF fi echo -e "${GREEN}Deployment configuration initialized!${NC}" echo -e "${BLUE}Configuration files created in: $DEPLOYMENT_CONFIG_DIR${NC}" echo "" echo -e "${YELLOW}Next steps:${NC}" echo "1. Review and customize server configurations in $DEPLOYMENT_CONFIG_DIR/servers/" echo "2. Add stack metadata files for your Docker stacks" echo "3. Run '$0 map' to see the current mapping" echo "4. Test with '$0 deploy-all --dry-run'" log "Deployment configuration initialized" } # Load server configuration load_server_config() { local server="$1" local config_file="$DEPLOYMENT_CONFIG_DIR/servers/${server}.yml" if [ ! -f "$config_file" ]; then echo -e "${RED}Error: Server configuration not found for '$server'${NC}" echo "Available servers:" ls "$DEPLOYMENT_CONFIG_DIR/servers/" 2>/dev/null | sed 's/\.yml$//' | sed 's/^/ - /' exit 1 fi # For now, we'll parse YAML manually (could use yq if available) # Extract stacks list from YAML grep -A 50 "stacks:" "$config_file" | grep "^-" | sed 's/^- //' | sed 's/["'\'']//g' | sed 's/#.*//' | sed 's/[[:space:]]*$//' } # Load multi-server stacks from config load_multi_server_stacks() { local config_file="$DEPLOYMENT_CONFIG_DIR/config.yml" if [ -f "$config_file" ]; then grep -A 10 "multi_server_stacks:" "$config_file" | grep "^-" | sed 's/^- //' | sed 's/["'\'']//g' | sed 's/#.*//' | sed 's/[[:space:]]*$//' fi } # Show stack-to-server mapping show_mapping() { echo -e "${BLUE}=== Docker Stack to Server Mapping ===${NC}" echo "" # Show multi-server stacks first local multi_server_stacks=$(load_multi_server_stacks) if [ -n "$multi_server_stacks" ]; then echo -e "${YELLOW}🌐 Multi-Server Stacks (deployed to ALL servers)${NC}" echo "$multi_server_stacks" | while IFS= read -r stack; do if [ -n "$stack" ]; then local stack_path="$DOCKER_DIR/$stack" local description="" case "$stack" in "dozzle") description="# Docker log viewer" ;; "dockge") description="# Docker compose management" ;; "diun") description="# Docker image update notifier" ;; *) description="# Multi-server tool" ;; esac if [ -d "$stack_path" ]; then echo " ✅ $stack $description" else echo " ❌ $stack $description (not found locally)" fi fi done echo "" fi for server_file in "$DEPLOYMENT_CONFIG_DIR/servers/"*.yml; do if [ -f "$server_file" ]; then local server=$(basename "$server_file" .yml) local role=$(grep "role:" "$server_file" | cut -d'"' -f2 2>/dev/null || echo "Unknown") echo -e "${GREEN}📍 $server${NC} (${YELLOW}$role${NC})" # Get stacks for this server local stacks=$(load_server_config "$server") if [ -n "$stacks" ]; then echo "$stacks" | while IFS= read -r stack; do if [ -n "$stack" ]; then local stack_path="$DOCKER_DIR/$stack" if [ -d "$stack_path" ]; then echo " ✅ $stack (exists)" else echo " ❌ $stack (missing)" fi fi done else echo " ${YELLOW}No stacks configured${NC}" fi echo "" fi done # Show unassigned stacks echo -e "${YELLOW}📦 Unassigned Stacks${NC}" local unassigned_count=0 if [ -d "$DOCKER_DIR" ]; then for stack_dir in "$DOCKER_DIR"/*; do if [ -d "$stack_dir" ]; then local stack_name=$(basename "$stack_dir") local assigned=false # Check if stack is assigned to any server for server_file in "$DEPLOYMENT_CONFIG_DIR/servers/"*.yml; do if [ -f "$server_file" ]; then if grep -q -- "- $stack_name" "$server_file" 2>/dev/null; then assigned=true break fi fi done # Also check if it's a multi-server stack local multi_server_stacks=$(load_multi_server_stacks) if echo "$multi_server_stacks" | grep -q "^$stack_name$" 2>/dev/null; then assigned=true fi if [ "$assigned" = false ]; then echo " 🔍 $stack_name" unassigned_count=$((unassigned_count + 1)) fi fi done fi if [ "$unassigned_count" -eq 0 ]; then echo -e " ${GREEN}✅ All stacks are assigned to servers${NC}" fi } # Sync environment files to server sync_env_files() { local server="$1" local dry_run="$2" echo -e "${YELLOW}Syncing environment files to $server...${NC}" # Get stacks for this server local stacks=$(load_server_config "$server") if [ -z "$stacks" ]; then echo -e "${YELLOW}No stacks configured for server $server${NC}" return 0 fi # Create temporary directory for sync local temp_dir=$(mktemp -d) local sync_count=0 echo "$stacks" | while IFS= read -r stack; do if [ -n "$stack" ]; then local stack_path="$DOCKER_DIR/$stack" if [ -d "$stack_path" ]; then # Find .env files in stack directory find "$stack_path" -name "*.env" -o -name ".env*" | while IFS= read -r env_file; do if [ -n "$env_file" ]; then local rel_path="${env_file#$DOCKER_DIR/}" local dest_dir="$temp_dir/$(dirname "$rel_path")" if [ "$dry_run" = "true" ]; then echo -e "${BLUE}Would sync: $rel_path${NC}" else mkdir -p "$dest_dir" cp "$env_file" "$dest_dir/" echo -e "${GREEN}✓ Prepared: $rel_path${NC}" sync_count=$((sync_count + 1)) fi fi done # Also sync docker-compose.yml local compose_file="$stack_path/docker-compose.yml" if [ -f "$compose_file" ]; then local rel_path="${compose_file#$DOCKER_DIR/}" local dest_dir="$temp_dir/$(dirname "$rel_path")" if [ "$dry_run" = "true" ]; then echo -e "${BLUE}Would sync: $rel_path${NC}" else mkdir -p "$dest_dir" cp "$compose_file" "$dest_dir/" echo -e "${GREEN}✓ Prepared: $rel_path${NC}" fi fi else echo -e "${YELLOW}Warning: Stack directory not found: $stack_path${NC}" fi fi done if [ "$dry_run" != "true" ]; then # Use rsync to sync to server (assumes SSH access) echo -e "${YELLOW}Transferring files to $server...${NC}" # This would be the actual rsync command (commented for safety) # rsync -avz --delete "$temp_dir/" "acedanger@$server:~/docker/" echo -e "${GREEN}Environment sync simulation completed for $server${NC}" echo -e "${BLUE}Files prepared in: $temp_dir${NC}" echo "To actually sync, you would run:" echo " rsync -avz --delete '$temp_dir/' 'acedanger@$server:~/docker/'" # Clean up temp directory # rm -rf "$temp_dir" fi log "Environment sync completed for $server - $sync_count files prepared" } # Deploy stacks to server deploy_to_server() { local server="$1" local dry_run="$2" local force="$3" echo -e "${YELLOW}Deploying Docker stacks to $server...${NC}" # First sync environment files sync_env_files "$server" "$dry_run" if [ "$dry_run" = "true" ]; then echo -e "${BLUE}Dry run completed for $server${NC}" return 0 fi # Get stacks for this server local stacks=$(load_server_config "$server") if [ -z "$stacks" ]; then echo -e "${YELLOW}No stacks configured for server $server${NC}" return 0 fi echo -e "${GREEN}Stacks to deploy on $server:${NC}" echo "$stacks" | sed 's/^/ - /' # Here you would implement the actual deployment logic # This could involve: # 1. SSH to the server # 2. Pull the latest compose files # 3. Run docker-compose up -d for each stack # 4. Perform health checks # 5. Send notifications echo -e "${GREEN}Deployment simulation completed for $server${NC}" # Send notification (using your existing ntfy setup) if command -v curl >/dev/null 2>&1; then curl -s \ -H "priority:default" \ -H "tags:deployment,docker,$server" \ -d "Deployed Docker stacks to $server: $(echo "$stacks" | tr '\n' ', ' | sed 's/, $//')" \ "https://notify.peterwood.rocks/lab" >/dev/null || true fi log "Deployment completed for $server" } # Deploy all stacks to their designated servers deploy_all() { local dry_run="$1" echo -e "${BLUE}=== Deploying All Stacks to Designated Servers ===${NC}" for server_file in "$DEPLOYMENT_CONFIG_DIR/servers/"*.yml; do if [ -f "$server_file" ]; then local server=$(basename "$server_file" .yml) echo "" deploy_to_server "$server" "$dry_run" fi done echo "" echo -e "${GREEN}All deployments completed!${NC}" } # Check deployment status check_status() { local server="$1" echo -e "${BLUE}=== Deployment Status for $server ===${NC}" # This would check the actual status on the server # For now, we'll simulate it echo -e "${GREEN}✅ Server is reachable${NC}" echo -e "${GREEN}✅ Docker is running${NC}" echo -e "${GREEN}✅ All stacks are healthy${NC}" # Get stacks for this server local stacks=$(load_server_config "$server") if [ -n "$stacks" ]; then echo "" echo -e "${BLUE}Configured stacks:${NC}" echo "$stacks" | while IFS= read -r stack; do if [ -n "$stack" ]; then echo -e " ${GREEN}✅${NC} $stack" fi done fi } # Main function main() { local command="" local dry_run=false local force=false local verbose=false local config_only=false local env_only=false # Parse command line arguments while [[ $# -gt 0 ]]; do case $1 in -h|--help) usage exit 0 ;; -d|--dry-run) dry_run=true shift ;; -f|--force) force=true shift ;; -v|--verbose) verbose=true shift ;; --config-only) config_only=true shift ;; --env-only) env_only=true shift ;; init|map|deploy-all|status) command="$1" shift ;; deploy|sync-env|rollback) command="$1" if [[ $# -gt 1 && ! "$2" =~ ^- ]]; then server="$2" shift 2 else echo -e "${RED}Error: Command '$1' requires a server name${NC}" exit 1 fi ;; *) echo "Unknown option: $1" usage exit 1 ;; esac done # Execute requested command case "$command" in init) init_deployment_config ;; map) show_mapping ;; deploy) deploy_to_server "$server" "$dry_run" "$force" ;; deploy-all) deploy_all "$dry_run" ;; sync-env) sync_env_files "$server" "$dry_run" ;; status) check_status "$server" ;; rollback) echo -e "${YELLOW}Rollback functionality not yet implemented${NC}" ;; "") echo -e "${RED}Error: No command specified${NC}" usage exit 1 ;; *) echo -e "${RED}Error: Unknown command '$command'${NC}" usage exit 1 ;; esac } # Run main function with all arguments main "$@"