#!/bin/bash # stack-assignment-helper.sh - Helper script to analyze and assign Docker stacks to servers # Author: Shell Repository # Description: Analyze Docker stacks and suggest server assignments based on patterns 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 DOCKER_DIR="$HOME/docker" DEPLOYMENT_CONFIG_DIR="$HOME/.docker-deployment" # Stack classification patterns declare -A MEDIA_PATTERNS=( ["plex"]="media-server" ["jellyfin"]="media-server" ["emby"]="media-server" ["kodi"]="media-server" ["photoprism"]="media-server" ["immich"]="media-server" ["nextcloud"]="media-server" ) # Multi-server patterns - stacks that should run on ALL servers declare -A MULTI_SERVER_PATTERNS=( ["dozzle"]="monitoring" ["dockge"]="management" ["diun"]="monitoring" ["watchtower"]="monitoring" ["portainer"]="management" ) declare -A DOWNLOAD_PATTERNS=( ["radarr"]="download-server" ["sonarr"]="download-server" ["lidarr"]="download-server" ["readarr"]="download-server" ["prowlarr"]="download-server" ["sabnzbd"]="download-server" ["nzbget"]="download-server" ["qbittorrent"]="download-server" ["transmission"]="download-server" ["deluge"]="download-server" ["overseerr"]="download-server" ["ombi"]="download-server" ["requestrr"]="download-server" ["jackett"]="download-server" ["metube"]="download-server" ["pinchflat"]="download-server" ["pdf"]="download-server" ) declare -A UTILITY_PATTERNS=( ["traefik"]="reverse-proxy" ["nginx"]="reverse-proxy" ["caddy"]="reverse-proxy" ["nginxproxymanager"]="reverse-proxy" ["grafana"]="monitoring" ["prometheus"]="monitoring" ["uptime-kuma"]="monitoring" ["gatus"]="monitoring" ["ntfy"]="monitoring" ["adguard"]="security" ["portainer"]="management" ["watchtower"]="management" ["vaultwarden"]="security" ["authelia"]="security" ["authentik"]="security" ["duplicati"]="backup" ["restic"]="backup" ["database"]="infrastructure" ["cloudflare"]="infrastructure" ["tclip"]="utility" ["opengist"]="utility" ["newt"]="utility" ["pangolin"]="utility" ["omni-tools"]="utility" ["golinks"]="utility" ["hoarder"]="productivity" ["paperless-ng"]="productivity" ["docmost"]="productivity" ["wiki"]="productivity" ["filebrowser"]="productivity" ["n8n"]="automation" ) # Analyze Docker directory structure analyze_stacks() { echo -e "${BLUE}=== Analyzing Docker Stacks ===${NC}" echo "" if [ ! -d "$DOCKER_DIR" ]; then echo -e "${RED}Docker directory not found: $DOCKER_DIR${NC}" exit 1 fi local total_stacks=0 local classified_stacks=0 # Arrays to store classifications local media_stacks=() local download_stacks=() local utility_stacks=() local unclassified_stacks=() # Analyze each directory in docker folder for stack_dir in "$DOCKER_DIR"/*; do if [ -d "$stack_dir" ]; then local stack_name=$(basename "$stack_dir") total_stacks=$((total_stacks + 1)) local classification="" local suggested_server="" # Check against patterns - prioritize multi-server patterns first if [[ -n "${MULTI_SERVER_PATTERNS[$stack_name]}" ]]; then classification="Multi-Server Tool" suggested_server="all-servers" utility_stacks+=("$stack_name") classified_stacks=$((classified_stacks + 1)) elif [[ -n "${MEDIA_PATTERNS[$stack_name]}" ]]; then classification="Media Server" suggested_server="europa" media_stacks+=("$stack_name") classified_stacks=$((classified_stacks + 1)) elif [[ -n "${DOWNLOAD_PATTERNS[$stack_name]}" ]]; then classification="Download/Acquisition" suggested_server="io" download_stacks+=("$stack_name") classified_stacks=$((classified_stacks + 1)) elif [[ -n "${UTILITY_PATTERNS[$stack_name]}" ]]; then classification="Utility/Monitoring" case "${UTILITY_PATTERNS[$stack_name]}" in "reverse-proxy"|"productivity") suggested_server="europa" ;; "monitoring"|"backup"|"security"|"infrastructure") suggested_server="racknerd" ;; "automation"|"utility") suggested_server="io" ;; *) suggested_server="racknerd" ;; esac utility_stacks+=("$stack_name") classified_stacks=$((classified_stacks + 1)) else # Try to classify based on docker-compose.yml content local compose_file="$stack_dir/docker-compose.yml" if [ -f "$compose_file" ]; then if grep -qi "plex\|jellyfin\|emby\|photoprism\|immich" "$compose_file"; then classification="Media Server (detected)" suggested_server="europa" media_stacks+=("$stack_name") classified_stacks=$((classified_stacks + 1)) elif grep -qi "radarr\|sonarr\|sabnzbd\|qbittorrent\|transmission" "$compose_file"; then classification="Download/Acquisition (detected)" suggested_server="io" download_stacks+=("$stack_name") classified_stacks=$((classified_stacks + 1)) elif grep -qi "grafana\|prometheus\|monitoring\|uptime" "$compose_file"; then classification="Monitoring (detected)" suggested_server="racknerd" utility_stacks+=("$stack_name") classified_stacks=$((classified_stacks + 1)) else classification="Unknown" suggested_server="manual-review" unclassified_stacks+=("$stack_name") fi else classification="No compose file" suggested_server="manual-review" unclassified_stacks+=("$stack_name") fi fi # Display analysis local status_icon="" local server_color="" case "$suggested_server" in "europa") status_icon="🎬" server_color="$GREEN" ;; "io") status_icon="📥" server_color="$BLUE" ;; "racknerd") status_icon="🔧" server_color="$YELLOW" ;; "all") status_icon="🌐" server_color="$GREEN" ;; *) status_icon="❓" server_color="$RED" ;; esac printf "%-20s %-25s ${server_color}%-10s${NC} %s\n" \ "$status_icon $stack_name" \ "$classification" \ "$suggested_server" \ "$(check_stack_health "$stack_dir")" fi done echo "" echo -e "${BLUE}=== Classification Summary ===${NC}" echo "Total stacks found: $total_stacks" echo "Classified stacks: $classified_stacks" echo "Unclassified stacks: $((total_stacks - classified_stacks))" echo "" # Show server assignments if [ ${#media_stacks[@]} -gt 0 ]; then echo -e "${GREEN}🎬 Europa (Media Server):${NC}" printf '%s\n' "${media_stacks[@]}" | sed 's/^/ - /' echo "" fi if [ ${#download_stacks[@]} -gt 0 ]; then echo -e "${BLUE}📥 IO (Download Server):${NC}" printf '%s\n' "${download_stacks[@]}" | sed 's/^/ - /' echo "" fi if [ ${#utility_stacks[@]} -gt 0 ]; then echo -e "${YELLOW}🔧 Racknerd (Utility/Backup):${NC}" printf '%s\n' "${utility_stacks[@]}" | sed 's/^/ - /' echo "" fi if [ ${#unclassified_stacks[@]} -gt 0 ]; then echo -e "${RED}❓ Manual Review Required:${NC}" printf '%s\n' "${unclassified_stacks[@]}" | sed 's/^/ - /' echo "" fi } # Check stack health indicators check_stack_health() { local stack_dir="$1" local health_indicators=() # Check for docker-compose.yml if [ -f "$stack_dir/docker-compose.yml" ]; then health_indicators+=("compose") fi # Check for .env files if find "$stack_dir" -name "*.env" -o -name ".env*" | grep -q .; then health_indicators+=("env") fi # Check for data/config directories if [ -d "$stack_dir/data" ] || [ -d "$stack_dir/config" ]; then health_indicators+=("data") fi # Format output if [ ${#health_indicators[@]} -eq 0 ]; then echo "⚠️ Incomplete" else echo "✅ $(IFS=,; echo "${health_indicators[*]}")" fi } # Generate deployment configuration generate_deployment_config() { echo -e "${YELLOW}Generating deployment configuration...${NC}" # Ensure deployment config directory exists mkdir -p "$DEPLOYMENT_CONFIG_DIR/stacks" # Create individual stack configurations for stack_dir in "$DOCKER_DIR"/*; do if [ -d "$stack_dir" ]; then local stack_name=$(basename "$stack_dir") local config_file="$DEPLOYMENT_CONFIG_DIR/stacks/${stack_name}.yml" # Determine server assignment local server="" if [[ -n "${MEDIA_PATTERNS[$stack_name]}" ]]; then server="europa" elif [[ -n "${DOWNLOAD_PATTERNS[$stack_name]}" ]]; then server="io" elif [[ -n "${UTILITY_PATTERNS[$stack_name]}" ]]; then case "${UTILITY_PATTERNS[$stack_name]}" in "reverse-proxy"|"management") server="europa" ;; *) server="racknerd" ;; esac elif [[ -n "${MULTI_SERVER_PATTERNS[$stack_name]}" ]]; then server="all" else server="manual-assignment-required" fi # Generate stack configuration cat > "$config_file" << EOF # Auto-generated stack configuration for $stack_name name: "$stack_name" description: "Docker stack for $stack_name" deployment: servers: ["$server"] priority: "medium" dependencies: [] restart_policy: "unless-stopped" health_check: enabled: false timeout: 30 retries: 3 backup: enabled: true schedule: "0 2 * * *" retention_days: 7 # Review and customize this configuration as needed EOF echo -e "${GREEN}✓ Generated: $stack_name.yml${NC}" fi done echo "" echo -e "${BLUE}Configuration files generated in: $DEPLOYMENT_CONFIG_DIR/stacks/${NC}" echo -e "${YELLOW}Please review and customize the generated configurations before deployment.${NC}" } # Show resource usage estimates show_resource_estimates() { echo -e "${BLUE}=== Resource Usage Estimates ===${NC}" echo "" # Define resource estimates for common stacks declare -A CPU_ESTIMATES=( ["plex"]="2-4" ["jellyfin"]="1-3" ["radarr"]="0.5-1" ["sonarr"]="0.5-1" ["sabnzbd"]="1-2" ["qbittorrent"]="0.5-2" ["grafana"]="0.5-1" ["prometheus"]="1-2" ["traefik"]="0.5-1" ["nextcloud"]="1-2" ) declare -A MEMORY_ESTIMATES=( ["plex"]="2-4GB" ["jellyfin"]="1-2GB" ["radarr"]="256-512MB" ["sonarr"]="256-512MB" ["sabnzbd"]="512MB-1GB" ["qbittorrent"]="256-512MB" ["grafana"]="256-512MB" ["prometheus"]="512MB-1GB" ["traefik"]="128-256MB" ["nextcloud"]="512MB-1GB" ) # Aggregate by server local europa_cpu=0 local io_cpu=0 local racknerd_cpu=0 for stack_dir in "$DOCKER_DIR"/*; do if [ -d "$stack_dir" ]; then local stack_name=$(basename "$stack_dir") local cpu_est="${CPU_ESTIMATES[$stack_name]:-0.5}" local mem_est="${MEMORY_ESTIMATES[$stack_name]:-256MB}" # Extract numeric part for aggregation (use lower bound) local cpu_num=$(echo "$cpu_est" | cut -d'-' -f1) printf "%-20s CPU: %-8s Memory: %-10s\n" "$stack_name" "$cpu_est cores" "$mem_est" fi done echo "" echo -e "${YELLOW}Note: These are rough estimates. Actual usage depends on workload and configuration.${NC}" } # Suggest optimization opportunities suggest_optimizations() { echo -e "${BLUE}=== Optimization Suggestions ===${NC}" echo "" echo -e "${GREEN}💡 Recommendations:${NC}" echo "1. Co-locate Traefik with media services (Europa) for efficient reverse proxy" echo "2. Keep download services (IO) on fast storage for temporary files" echo "3. Place monitoring/backup services (Racknerd) on cost-effective hardware" echo "4. Consider resource limits in docker-compose.yml files" echo "5. Use shared volumes for media access across containers" echo "" echo -e "${YELLOW}🔧 Integration Points:${NC}" echo "1. Integrate with existing backup-env-files.sh for environment management" echo "2. Use existing crontab system for deployment automation" echo "3. Leverage ntfy notifications for deployment status" echo "4. Consider using existing logging infrastructure" echo "" } # Main function main() { case "${1:-analyze}" in analyze|--analyze|-a) analyze_stacks ;; generate|--generate|-g) generate_deployment_config ;; resources|--resources|-r) show_resource_estimates ;; optimize|--optimize|-o) suggest_optimizations ;; all|--all) analyze_stacks echo "" show_resource_estimates echo "" suggest_optimizations ;; help|--help|-h) echo "Usage: $0 [COMMAND]" echo "" echo "Commands:" echo " analyze Analyze and classify Docker stacks (default)" echo " generate Generate deployment configuration files" echo " resources Show resource usage estimates" echo " optimize Show optimization suggestions" echo " all Run all analysis commands" echo " help Show this help message" ;; *) echo -e "${RED}Unknown command: $1${NC}" echo "Use '$0 help' for usage information" exit 1 ;; esac } # Run main function main "$@"