#!/bin/bash uninstall_hops() { # Clear terminal at startup clear # Exit on any error (but allow some failures during cleanup) set +e # Script version for consistency local SCRIPT_VERSION="1.0.0" # -------------------------------------------- # LOGGING SETUP # -------------------------------------------- local LOG_DIR="/var/log/hops" local LOG_FILE="$LOG_DIR/hops-uninstall-$(date +%Y%m%d-%H%M%S).log" mkdir -p "$LOG_DIR" touch "$LOG_FILE" log() { echo -e "$(date '+%Y-%m-%d %T') - $1" | tee -a "$LOG_FILE" } error_exit() { log "โŒ ERROR: $1" log "โŒ Uninstallation failed. Check logs at: $LOG_FILE" exit 1 } warning() { log "โš ๏ธ WARNING: $1" } # -------------------------------------------- # HEADER # -------------------------------------------- cat << "EOF" _ _ ____ ____ ____ | | | || _ \| _ \/ ___| | |__| || |_) | |_) \___ \ | __ || __/| __/ ___) | |_| |_||_| |_| |____/ EOF echo -e "๐Ÿ—‘๏ธ Homelab Orchestration Provisioning Script - UNINSTALLER v${SCRIPT_VERSION}\n" log "๐Ÿ—‘๏ธ Starting HOPS Uninstallation v${SCRIPT_VERSION}" # -------------------------------------------- # ROOT CHECK # -------------------------------------------- if [[ $EUID -ne 0 ]]; then error_exit "This script must be run as root or with sudo." fi # -------------------------------------------- # CONFIRMATION PROMPT # -------------------------------------------- show_uninstall_warning() { echo -e "โš ๏ธ WARNING: This will completely remove your HOPS installation!" echo -e "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" echo -e "This uninstaller will:" echo -e " โ€ข Stop and remove all Docker containers" echo -e " โ€ข Remove Docker images (optional)" echo -e " โ€ข Remove Docker Compose configuration" echo -e " โ€ข Clean up application data (optional)" echo -e " โ€ข Remove firewall rules" echo -e " โ€ข Uninstall Docker (optional)" echo -e "" echo -e "โš ๏ธ YOUR MEDIA FILES WILL NOT BE DELETED" echo -e "โš ๏ธ APPLICATION DATA REMOVAL IS OPTIONAL" echo -e "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" } get_uninstall_options() { echo -e "\n๐Ÿ”ง Uninstall Options:" # Container and compose removal (always done) REMOVE_CONTAINERS=true REMOVE_COMPOSE=true # Optional removals echo -e "\nโ“ Remove Docker images? (saves disk space but requires re-download) [y/N]: " read -r remove_images REMOVE_IMAGES=false [[ "$remove_images" =~ ^[Yy]$ ]] && REMOVE_IMAGES=true echo -e "\nโ“ Remove application data? (โš ๏ธ DELETES ALL CONFIGURATIONS!) [y/N]: " read -r remove_appdata REMOVE_APPDATA=false [[ "$remove_appdata" =~ ^[Yy]$ ]] && REMOVE_APPDATA=true echo -e "\nโ“ Uninstall Docker completely? [y/N]: " read -r remove_docker REMOVE_DOCKER=false [[ "$remove_docker" =~ ^[Yy]$ ]] && REMOVE_DOCKER=true echo -e "\nโ“ Remove firewall rules? [Y/n]: " read -r remove_firewall REMOVE_FIREWALL=true [[ "$remove_firewall" =~ ^[Nn]$ ]] && REMOVE_FIREWALL=false # Final confirmation echo -e "\nโš ๏ธ FINAL CONFIRMATION" echo -e "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" echo -e "Actions to perform:" echo -e " โ€ข Remove containers: โœ…" echo -e " โ€ข Remove compose files: โœ…" [[ $REMOVE_IMAGES == true ]] && echo -e " โ€ข Remove Docker images: โœ…" || echo -e " โ€ข Remove Docker images: โŒ" [[ $REMOVE_APPDATA == true ]] && echo -e " โ€ข Remove app data: โœ…" || echo -e " โ€ข Remove app data: โŒ" [[ $REMOVE_DOCKER == true ]] && echo -e " โ€ข Uninstall Docker: โœ…" || echo -e " โ€ข Uninstall Docker: โŒ" [[ $REMOVE_FIREWALL == true ]] && echo -e " โ€ข Remove firewall rules: โœ…" || echo -e " โ€ข Remove firewall rules: โŒ" echo -e "\nโ“ Proceed with uninstallation? [y/N]: " read -r final_confirm if [[ ! "$final_confirm" =~ ^[Yy]$ ]]; then log "๐Ÿšซ Uninstallation cancelled by user" exit 0 fi } # -------------------------------------------- # HOMELAB DIRECTORY DETECTION # -------------------------------------------- find_homelab_directory() { local POSSIBLE_DIRS=( "$HOME/hops" "/home/*/hops" "/opt/hops" "/srv/hops" ) # Try to find from running user's home first if [[ -n "$SUDO_USER" ]]; then local user_home=$(eval echo "~$SUDO_USER") POSSIBLE_DIRS=("$user_home/hops" "${POSSIBLE_DIRS[@]}") fi HOMELAB_DIR="" for dir in "${POSSIBLE_DIRS[@]}"; do if [[ -f "$dir/docker-compose.yml" ]]; then HOMELAB_DIR="$dir" log "โœ… Found homelab directory: $HOMELAB_DIR" break fi done if [[ -z "$HOMELAB_DIR" ]]; then echo -e "\n๐Ÿ“‚ Could not auto-detect homelab directory." echo -e "Please enter the path to your homelab directory (contains docker-compose.yml):" read -r user_dir if [[ -f "$user_dir/docker-compose.yml" ]]; then HOMELAB_DIR="$user_dir" log "โœ… Using homelab directory: $HOMELAB_DIR" else warning "No docker-compose.yml found in $user_dir" log "๐Ÿ“ Will proceed with container cleanup by name instead" fi fi # Set APPDATA_DIR from env file if available if [[ -f "$HOMELAB_DIR/.env" ]]; then APPDATA_DIR=$(grep "^CONFIG_ROOT=" "$HOMELAB_DIR/.env" | cut -d= -f2) log "๐Ÿ“ Found appdata directory: $APPDATA_DIR" fi } # -------------------------------------------- # SERVICE DETECTION # -------------------------------------------- detect_running_services() { log "๐Ÿ” Detecting running HOPS services..." # Known HOPS service names local KNOWN_SERVICES=( "sonarr" "radarr" "lidarr" "readarr" "bazarr" "prowlarr" "tdarr" "nzbget" "sabnzbd" "transmission" "qbittorrent" "plex" "jellyfin" "emby" "jellystat" "jellystat-db" "overseerr" "jellyseerr" "ombi" "traefik" "nginx-proxy-manager" "authelia" "portainer" "watchtower" "uptime-kuma" "postgres" "redis" ) DETECTED_SERVICES=() for service in "${KNOWN_SERVICES[@]}"; do if docker ps -a --format "{{.Names}}" | grep -q "^${service}$"; then DETECTED_SERVICES+=("$service") fi done if [[ ${#DETECTED_SERVICES[@]} -gt 0 ]]; then log "โœ… Detected services: ${DETECTED_SERVICES[*]}" else log "โš ๏ธ No HOPS services detected" fi } # -------------------------------------------- # CONTAINER CLEANUP # -------------------------------------------- stop_and_remove_containers() { log "๐Ÿ›‘ Stopping and removing containers..." if [[ -n "$HOMELAB_DIR" && -f "$HOMELAB_DIR/docker-compose.yml" ]]; then cd "$HOMELAB_DIR" # Stop services gracefully log "๐Ÿ”„ Stopping services with docker compose..." if docker compose ps -q | grep -q .; then if ! docker compose down --timeout 30 2>&1 | tee -a "$LOG_FILE"; then warning "Docker compose down failed, attempting force removal" docker compose down --timeout 10 --remove-orphans --volumes 2>&1 | tee -a "$LOG_FILE" || true fi else log "โ„น๏ธ No running compose services found" fi fi # Fallback: Remove containers by name if [[ ${#DETECTED_SERVICES[@]} -gt 0 ]]; then log "๐Ÿงน Cleaning up remaining containers..." for service in "${DETECTED_SERVICES[@]}"; do if docker ps -a --format "{{.Names}}" | grep -q "^${service}$"; then log "๐Ÿ—‘๏ธ Removing container: $service" docker stop "$service" 2>/dev/null || true docker rm -f "$service" 2>/dev/null || true fi done fi log "โœ… Container cleanup complete" } # -------------------------------------------- # IMAGE CLEANUP # -------------------------------------------- remove_docker_images() { if [[ $REMOVE_IMAGES == true ]]; then log "๐Ÿ—‘๏ธ Removing Docker images..." # Common HOPS images local HOPS_IMAGES=( "lscr.io/linuxserver/sonarr" "lscr.io/linuxserver/radarr" "lscr.io/linuxserver/lidarr" "lscr.io/linuxserver/readarr" "lscr.io/linuxserver/bazarr" "lscr.io/linuxserver/prowlarr" "ghcr.io/haveagitgat/tdarr" "lscr.io/linuxserver/nzbget" "lscr.io/linuxserver/sabnzbd" "lscr.io/linuxserver/transmission" "lscr.io/linuxserver/qbittorrent" "plexinc/pms-docker" "jellyfin/jellyfin" "emby/embyserver" "sctx/overseerr" "fallenbagel/jellyseerr" "lscr.io/linuxserver/ombi" "traefik" "jc21/nginx-proxy-manager" "authelia/authelia" "portainer/portainer-ce" "containrrr/watchtower" "louislam/uptime-kuma" "postgres" "redis" "cyfershepard/jellystat" ) for image in "${HOPS_IMAGES[@]}"; do if docker images --format "{{.Repository}}" | grep -q "^${image}$"; then log "๐Ÿ—‘๏ธ Removing image: $image" docker rmi -f "$image" 2>/dev/null || true fi done # Clean up dangling images log "๐Ÿงน Cleaning up dangling images..." docker image prune -f 2>/dev/null || true log "โœ… Image cleanup complete" else log "โญ๏ธ Skipping image removal" fi } # -------------------------------------------- # NETWORK CLEANUP # -------------------------------------------- cleanup_networks() { log "๐ŸŒ Cleaning up Docker networks..." local HOPS_NETWORKS=("homelab" "traefik" "database") for network in "${HOPS_NETWORKS[@]}"; do if docker network ls --format "{{.Name}}" | grep -q "^${network}$"; then log "๐Ÿ—‘๏ธ Removing network: $network" docker network rm "$network" 2>/dev/null || warning "Could not remove network: $network" fi done log "โœ… Network cleanup complete" } # -------------------------------------------- # VOLUME CLEANUP # -------------------------------------------- cleanup_volumes() { log "๐Ÿ’พ Cleaning up Docker volumes..." local HOPS_VOLUMES=("postgres_data" "redis_data") for volume in "${HOPS_VOLUMES[@]}"; do if docker volume ls --format "{{.Name}}" | grep -q "^${volume}$"; then log "๐Ÿ—‘๏ธ Removing volume: $volume" docker volume rm "$volume" 2>/dev/null || warning "Could not remove volume: $volume" fi done # Clean up orphaned volumes log "๐Ÿงน Cleaning up orphaned volumes..." docker volume prune -f 2>/dev/null || true log "โœ… Volume cleanup complete" } # -------------------------------------------- # FILE CLEANUP # -------------------------------------------- cleanup_compose_files() { if [[ $REMOVE_COMPOSE == true && -n "$HOMELAB_DIR" ]]; then log "๐Ÿ“ Removing Docker Compose files..." cd "$HOMELAB_DIR" # Backup before removal if [[ -f docker-compose.yml ]]; then local BACKUP_FILE="docker-compose.yml.removed.$(date +%Y%m%d%H%M%S)" log "๐Ÿ“ฆ Backing up compose file to: $BACKUP_FILE" cp docker-compose.yml "$BACKUP_FILE" 2>/dev/null || warning "Could not backup compose file" rm -f docker-compose.yml fi # Remove other compose-related files rm -f docker-compose.override.yml .env 2>/dev/null || true # Remove empty homelab directory if it's empty cd .. if [[ -d "$HOMELAB_DIR" ]]; then rmdir "$HOMELAB_DIR" 2>/dev/null && log "๐Ÿ“ Removed empty homelab directory" || log "๐Ÿ“ Homelab directory not empty, keeping it" fi log "โœ… Compose file cleanup complete" else log "โญ๏ธ Skipping compose file removal" fi } cleanup_appdata() { if [[ $REMOVE_APPDATA == true && -n "$APPDATA_DIR" ]]; then log "๐Ÿ—‚๏ธ Removing application data..." echo -e "\nโš ๏ธ FINAL WARNING: This will delete ALL application configurations!" echo -e "Application data directory: $APPDATA_DIR" echo -e "โ“ Are you absolutely sure? Type 'DELETE' to confirm: " read -r delete_confirm if [[ "$delete_confirm" == "DELETE" ]]; then # Create a backup first local BACKUP_DIR="/tmp/hops-appdata-backup-$(date +%Y%m%d%H%M%S)" log "๐Ÿ“ฆ Creating backup at: $BACKUP_DIR" if cp -r "$APPDATA_DIR" "$BACKUP_DIR" 2>/dev/null; then log "โœ… Backup created successfully" # Remove the original if rm -rf "$APPDATA_DIR" 2>/dev/null; then log "โœ… Application data removed" log "๐Ÿ“ฆ Backup available at: $BACKUP_DIR" else warning "Failed to remove application data directory" fi else warning "Could not create backup, skipping appdata removal" fi else log "๐Ÿšซ Application data removal cancelled" fi else log "โญ๏ธ Skipping application data removal" fi } # -------------------------------------------- # FIREWALL CLEANUP # -------------------------------------------- cleanup_firewall() { if [[ $REMOVE_FIREWALL == true ]]; then log "๐Ÿ”ฅ Removing firewall rules..." if command -v ufw &>/dev/null; then # Remove HOPS-specific rules by searching for comments local rules_to_remove=() # Get numbered rules that contain HOPS service names mapfile -t rules_to_remove < <(ufw status numbered | grep -E "(sonarr|radarr|lidarr|readarr|bazarr|prowlarr|jellyfin|plex|portainer|traefik|npm)" | awk '{print $1}' | tr -d '[]') # Remove rules in reverse order to maintain numbering if [[ ${#rules_to_remove[@]} -gt 0 ]]; then for ((i=${#rules_to_remove[@]}-1; i>=0; i--)); do local rule_num="${rules_to_remove[i]}" log "๐Ÿ—‘๏ธ Removing firewall rule #$rule_num" echo "y" | ufw delete "$rule_num" 2>/dev/null || true done fi log "โœ… Firewall cleanup complete" else log "โ„น๏ธ UFW not installed, skipping firewall cleanup" fi else log "โญ๏ธ Skipping firewall cleanup" fi } # -------------------------------------------- # DOCKER UNINSTALLATION # -------------------------------------------- uninstall_docker() { if [[ $REMOVE_DOCKER == true ]]; then log "๐Ÿณ Uninstalling Docker..." # Stop Docker service systemctl stop docker 2>/dev/null || true systemctl disable docker 2>/dev/null || true # Remove Docker packages local DOCKER_PACKAGES=( "docker-ce" "docker-ce-cli" "containerd.io" "docker-buildx-plugin" "docker-compose-plugin" "docker.io" "docker-doc" "docker-compose" "podman-docker" "containerd" "runc" ) for package in "${DOCKER_PACKAGES[@]}"; do if dpkg -l | grep -q "^ii.*$package"; then log "๐Ÿ—‘๏ธ Removing package: $package" apt-get remove -y "$package" 2>/dev/null || true fi done # Clean up Docker directories log "๐Ÿ—‘๏ธ Removing Docker directories..." rm -rf /var/lib/docker /etc/docker /var/run/docker.sock 2>/dev/null || true rm -rf ~/.docker 2>/dev/null || true # Remove from user directories too if [[ -n "$SUDO_USER" ]]; then local user_home=$(eval echo "~$SUDO_USER") rm -rf "$user_home/.docker" 2>/dev/null || true fi # Remove Docker group if getent group docker &>/dev/null; then groupdel docker 2>/dev/null || warning "Could not remove docker group" fi # Clean up package cache apt-get autoremove -y 2>/dev/null || true apt-get autoclean 2>/dev/null || true log "โœ… Docker uninstallation complete" else log "โญ๏ธ Skipping Docker uninstallation" fi } # -------------------------------------------- # CLEANUP LOG FILES # -------------------------------------------- cleanup_logs() { log "๐Ÿ“‹ Cleaning up HOPS log files..." # Keep current log file, remove others find "$LOG_DIR" -name "homelab-*.log" -not -name "$(basename "$LOG_FILE")" -delete 2>/dev/null || true # Remove log directory if empty (except current log) local remaining_logs=$(find "$LOG_DIR" -name "*.log" | wc -l) if [[ $remaining_logs -le 1 ]]; then log "๐Ÿ“ Log directory will be cleaned after this session" fi log "โœ… Log cleanup complete" } # -------------------------------------------- # MAIN UNINSTALLATION FLOW # -------------------------------------------- show_uninstall_warning get_uninstall_options log "๐Ÿš€ Starting HOPS uninstallation process..." find_homelab_directory detect_running_services # Core cleanup steps stop_and_remove_containers remove_docker_images cleanup_networks cleanup_volumes cleanup_compose_files cleanup_appdata cleanup_firewall uninstall_docker cleanup_logs # -------------------------------------------- # FINAL SUMMARY # -------------------------------------------- echo -e "\nโœ… HOPS Uninstallation Complete!" echo -e "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" log "๐Ÿ“‹ Uninstallation Summary:" echo -e "\n๐Ÿ—‘๏ธ Removed Components:" echo " โ€ข Docker containers: โœ…" echo " โ€ข Docker Compose files: โœ…" [[ $REMOVE_IMAGES == true ]] && echo " โ€ข Docker images: โœ…" || echo " โ€ข Docker images: โŒ (kept)" [[ $REMOVE_APPDATA == true ]] && echo " โ€ข Application data: โœ…" || echo " โ€ข Application data: โŒ (kept)" [[ $REMOVE_DOCKER == true ]] && echo " โ€ข Docker installation: โœ…" || echo " โ€ข Docker installation: โŒ (kept)" [[ $REMOVE_FIREWALL == true ]] && echo " โ€ข Firewall rules: โœ…" || echo " โ€ข Firewall rules: โŒ (kept)" echo -e "\n๐Ÿ“‚ Preserved:" echo " โ€ข Media files: โœ… (never touched)" [[ $REMOVE_APPDATA != true ]] && echo " โ€ข Application configurations: โœ…" if [[ $REMOVE_APPDATA == true ]]; then echo -e "\n๐Ÿ“ฆ Backup Location:" echo " โ€ข Application data backup: /tmp/hops-appdata-backup-*" echo " โ€ข Consider moving this backup to a permanent location" fi echo -e "\n๐Ÿ“‹ Complete log: $LOG_FILE" echo -e "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" log "โœ… HOPS uninstallation completed successfully!" if [[ -n "$SUDO_USER" ]]; then echo -e "\n๐Ÿ’ก Note: You may want to restart your session to ensure all group changes take effect." fi if [[ $REMOVE_DOCKER == true ]]; then echo -e "\n๐Ÿ”„ Recommendation: Reboot your system to complete Docker removal." fi return 0 }