Rename scripts for clarity and add Huntarr service

- Renamed all scripts to descriptive names without prefixes:
  • hops.sh → hops (main script)
  • hops_installer_enhanced.sh → install
  • hops_uninstaller_fixed.sh → uninstall
  • hops_service_definitions.sh → services
  • hops_install.sh → setup
  • hops_privileged_setup.sh → privileged-setup
  • hops_user_operations.sh → user-operations
  • hops_service_definitions_improved.sh → services-improved

- Added Huntarr service support:
  • Docker image: ghcr.io/plexguide/huntarr:latest
  • Port: 9705 with /health endpoint
  • Missing media discovery and automation
  • Integrates with *arr stack services
  • Added to installer menu as option 8

- Updated all script references and documentation
- Updated service categories in README and CLAUDE.md

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Stephen Klein
2025-07-17 21:52:22 -04:00
parent 721f7d7a75
commit 5affcd2e26
17 changed files with 1077 additions and 3678 deletions
+686 -13
View File
@@ -5,8 +5,8 @@
# Version: 3.1.0
# Source common functions
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/common.sh"
LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$LIB_DIR/common.sh"
# Global variables for system info
OS_NAME=""
@@ -17,6 +17,16 @@ OS_NAME_LOWER=""
detect_os() {
info "🔍 Detecting operating system..."
# Check if we're on macOS
if [[ "$(uname -s)" == "Darwin" ]]; then
OS_NAME="macOS"
OS_VERSION=$(sw_vers -productVersion)
OS_NAME_LOWER="macos"
success "Detected supported OS: $OS_NAME $OS_VERSION"
return 0
fi
# Linux detection
if command_exists lsb_release; then
OS_NAME=$(lsb_release -is)
OS_VERSION=$(lsb_release -rs)
@@ -35,7 +45,7 @@ detect_os() {
success "Detected supported OS: $OS_NAME $OS_VERSION"
;;
*)
error_exit "Unsupported OS: $OS_NAME $OS_VERSION. Only Ubuntu/Debian/Linux Mint are supported."
error_exit "Unsupported OS: $OS_NAME $OS_VERSION. Only Ubuntu/Debian/Linux Mint/macOS are supported."
;;
esac
}
@@ -50,13 +60,24 @@ check_system_requirements() {
# Check architecture
local arch=$(uname -m)
if [[ "$arch" != "x86_64" ]]; then
error_exit "Unsupported architecture: $arch. Only x86_64 is supported."
if [[ "$OS_NAME_LOWER" == "macos" ]]; then
# macOS supports both x86_64 and arm64 (Apple Silicon)
if [[ "$arch" != "x86_64" && "$arch" != "arm64" ]]; then
error_exit "Unsupported architecture: $arch. Only x86_64 and arm64 are supported on macOS."
fi
else
# Linux only supports x86_64
if [[ "$arch" != "x86_64" ]]; then
error_exit "Unsupported architecture: $arch. Only x86_64 is supported."
fi
fi
# Check RAM
local ram_gb
if command_exists free; then
if [[ "$OS_NAME_LOWER" == "macos" ]]; then
# macOS memory check
ram_gb=$(sysctl -n hw.memsize | awk '{print int($1/1024/1024/1024)}')
elif command_exists free; then
ram_gb=$(free -g | awk '/^Mem:/{print $2}')
else
ram_gb=$(awk '/MemTotal/ {print int($2/1024/1024)}' /proc/meminfo)
@@ -68,7 +89,10 @@ check_system_requirements() {
# Check disk space
local disk_avail_gb
if command_exists df; then
if [[ "$OS_NAME_LOWER" == "macos" ]]; then
# macOS disk space check
disk_avail_gb=$(df -g "$target_dir" | tail -n 1 | awk '{print $4}')
elif command_exists df; then
disk_avail_gb=$(df -BG --output=avail "$target_dir" | tail -n 1 | tr -d 'G')
else
error_exit "Unable to check disk space - 'df' command not available"
@@ -79,7 +103,13 @@ check_system_requirements() {
fi
# Check CPU cores
local cpu_cores=$(nproc)
local cpu_cores
if [[ "$OS_NAME_LOWER" == "macos" ]]; then
cpu_cores=$(sysctl -n hw.ncpu)
else
cpu_cores=$(nproc)
fi
if [[ $cpu_cores -lt 2 ]]; then
warning "Only ${cpu_cores} CPU core(s) detected. 2+ cores recommended for optimal performance."
fi
@@ -189,7 +219,12 @@ check_sudo() {
# Get system timezone
get_system_timezone() {
if [[ -f /etc/timezone ]]; then
if [[ "$OS_NAME_LOWER" == "macos" ]]; then
# macOS timezone detection
readlink /etc/localtime | sed 's|/var/db/timezone/zoneinfo/||' 2>/dev/null || \
ls -la /etc/localtime | awk '{print $NF}' | sed 's|/var/db/timezone/zoneinfo/||' 2>/dev/null || \
echo "UTC"
elif [[ -f /etc/timezone ]]; then
cat /etc/timezone
elif [[ -L /etc/localtime ]]; then
readlink /etc/localtime | sed 's|/usr/share/zoneinfo/||'
@@ -206,8 +241,16 @@ validate_timezone() {
return 1
fi
if [[ -f "/usr/share/zoneinfo/$timezone" ]]; then
return 0
if [[ "$OS_NAME_LOWER" == "macos" ]]; then
# macOS timezone validation
if [[ -f "/var/db/timezone/zoneinfo/$timezone" ]]; then
return 0
fi
else
# Linux timezone validation
if [[ -f "/usr/share/zoneinfo/$timezone" ]]; then
return 0
fi
fi
return 1
@@ -221,7 +264,12 @@ check_storage_space() {
# Create directory if it doesn't exist
mkdir -p "$path" 2>/dev/null || true
local available_gb=$(df -BG --output=avail "$path" | tail -n 1 | tr -d 'G')
local available_gb
if [[ "$OS_NAME_LOWER" == "macos" ]]; then
available_gb=$(df -g "$path" | tail -n 1 | awk '{print $4}')
else
available_gb=$(df -BG --output=avail "$path" | tail -n 1 | tr -d 'G')
fi
if [[ $available_gb -lt $required_gb ]]; then
error_exit "Insufficient storage space in $path: ${available_gb}GB available, ${required_gb}GB required"
@@ -273,6 +321,13 @@ get_user_info() {
check_firewall() {
info "🔥 Checking firewall status..."
if [[ "$OS_NAME_LOWER" == "macos" ]]; then
# macOS uses pfctl/firewall, but we'll skip automatic configuration
warning "macOS firewall detected. Automatic firewall configuration skipped."
info "💡 You may need to manually configure firewall rules if needed."
return 0
fi
if command_exists ufw; then
local ufw_status=$(ufw status | head -n1 | awk '{print $2}')
@@ -309,10 +364,628 @@ run_system_checks() {
check_system_requirements "$min_ram_gb" "$min_disk_gb" "$target_dir"
check_internet
check_docker_requirements
check_firewall
# Skip firewall check for macOS (handled differently)
if [[ "$OS_NAME_LOWER" != "macos" ]]; then
check_firewall
fi
# Check for container environment (warning only)
check_container_environment
success "All system checks passed"
}
# Get platform-specific default paths
get_default_media_path() {
if [[ "$OS_NAME_LOWER" == "macos" ]]; then
echo "/Users/$USER/homelab/media"
else
echo "/mnt/media"
fi
}
get_default_config_path() {
if [[ "$OS_NAME_LOWER" == "macos" ]]; then
echo "/Users/$USER/homelab/config"
else
echo "/opt/appdata"
fi
}
get_default_homelab_path() {
if [[ "$OS_NAME_LOWER" == "macos" ]]; then
echo "/Users/$USER/homelab"
else
echo "/home/$USER/homelab"
fi
}
# Get Docker socket path for current platform
get_docker_socket_path() {
if [[ "$OS_NAME_LOWER" == "macos" ]]; then
echo "/var/run/docker.sock"
else
echo "/var/run/docker.sock"
fi
}
# Package management abstraction
install_package() {
local package="$1"
if [[ -z "$package" ]]; then
error_exit "install_package requires a package name"
fi
info "📦 Installing package: $package"
case "$OS_NAME_LOWER" in
"macos")
if ! command_exists brew; then
error_exit "Homebrew not found. Please install Homebrew first: https://brew.sh/"
fi
# Get the actual user (not root) to run brew commands
local actual_user
if [[ -n "$SUDO_USER" ]]; then
actual_user="$SUDO_USER"
else
actual_user="$(whoami)"
fi
sudo -u "$actual_user" brew install "$package"
;;
"ubuntu"|"debian"|"linuxmint"|"mint")
apt-get update && apt-get install -y "$package"
;;
*)
error_exit "Unsupported OS for package installation: $OS_NAME"
;;
esac
success "Package installed: $package"
}
# Service management abstraction
start_service() {
local service="$1"
if [[ -z "$service" ]]; then
error_exit "start_service requires a service name"
fi
info "🚀 Starting service: $service"
case "$OS_NAME_LOWER" in
"macos")
if [[ "$service" == "docker" ]]; then
# On macOS, Docker Desktop handles this
info "Docker Desktop should be started manually or via Docker Desktop app"
else
# Use launchctl for other services
launchctl start "$service" 2>/dev/null || true
fi
;;
"ubuntu"|"debian"|"linuxmint"|"mint")
systemctl start "$service"
;;
*)
error_exit "Unsupported OS for service management: $OS_NAME"
;;
esac
success "Service started: $service"
}
# Check if service is running
is_service_running() {
local service="$1"
if [[ -z "$service" ]]; then
return 1
fi
case "$OS_NAME_LOWER" in
"macos")
if [[ "$service" == "docker" ]]; then
# Check if Docker daemon is responding
docker info >/dev/null 2>&1
else
# Check with launchctl
launchctl list | grep -q "$service"
fi
;;
"ubuntu"|"debian"|"linuxmint"|"mint")
systemctl is-active --quiet "$service"
;;
*)
return 1
;;
esac
}
# Enable service to start on boot
enable_service() {
local service="$1"
if [[ -z "$service" ]]; then
error_exit "enable_service requires a service name"
fi
info "⚙️ Enabling service: $service"
case "$OS_NAME_LOWER" in
"macos")
if [[ "$service" == "docker" ]]; then
info "Docker Desktop auto-start should be configured in Docker Desktop settings"
else
# Use launchctl for other services
launchctl enable "$service" 2>/dev/null || true
fi
;;
"ubuntu"|"debian"|"linuxmint"|"mint")
systemctl enable "$service"
;;
*)
error_exit "Unsupported OS for service management: $OS_NAME"
;;
esac
success "Service enabled: $service"
}
# Get network interface IP address
get_primary_ip() {
local ip=""
case "$OS_NAME_LOWER" in
"macos")
# macOS network interface detection
ip=$(route get default | grep interface | awk '{print $2}' | head -1)
if [[ -n "$ip" ]]; then
ip=$(ifconfig "$ip" | grep 'inet ' | awk '{print $2}' | head -1)
fi
;;
"ubuntu"|"debian"|"linuxmint"|"mint")
# Linux network interface detection
ip=$(hostname -I | awk '{print $1}')
;;
*)
# Fallback method
ip=$(ip route get 8.8.8.8 2>/dev/null | grep -oP 'src \K\S+' | head -1)
;;
esac
# Validate IP address
if is_valid_ip "$ip"; then
echo "$ip"
else
echo "localhost"
fi
}
# Remove existing Docker installation on Linux
remove_docker_linux() {
info "🗑️ Removing existing Docker installation..."
# Stop Docker service if running
if systemctl is-active --quiet docker; then
info "🛑 Stopping Docker service..."
systemctl stop docker
fi
# Stop Docker socket if running
if systemctl is-active --quiet docker.socket; then
info "🛑 Stopping Docker socket..."
systemctl stop docker.socket
fi
# Disable Docker service
if systemctl is-enabled --quiet docker; then
info "🔧 Disabling Docker service..."
systemctl disable docker
fi
# Remove Docker packages
info "🗑️ Removing Docker packages..."
apt-get remove -y docker docker-engine docker.io containerd runc docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin 2>/dev/null || true
apt-get purge -y docker docker-engine docker.io containerd runc docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin 2>/dev/null || true
# Remove Docker Compose standalone if installed
if [[ -f "/usr/local/bin/docker-compose" ]]; then
info "🗑️ Removing Docker Compose standalone..."
rm -f "/usr/local/bin/docker-compose"
fi
# Remove Docker data directories
info "🗑️ Removing Docker data directories..."
local docker_dirs=(
"/var/lib/docker"
"/var/lib/containerd"
"/etc/docker"
"/etc/containerd"
"/run/docker"
"/run/containerd"
"/opt/containerd"
)
for dir in "${docker_dirs[@]}"; do
if [[ -d "$dir" ]]; then
rm -rf "$dir"
fi
done
# Remove Docker group
if getent group docker >/dev/null 2>&1; then
info "🗑️ Removing Docker group..."
groupdel docker 2>/dev/null || true
fi
# Remove Docker repository
if [[ -f "/etc/apt/sources.list.d/docker.list" ]]; then
info "🗑️ Removing Docker repository..."
rm -f "/etc/apt/sources.list.d/docker.list"
fi
# Remove Docker GPG key
if [[ -f "/etc/apt/keyrings/docker.gpg" ]]; then
rm -f "/etc/apt/keyrings/docker.gpg"
fi
# Remove any remaining Docker processes
pkill -f docker 2>/dev/null || true
pkill -f containerd 2>/dev/null || true
# Clean up package manager cache
apt-get autoremove -y 2>/dev/null || true
apt-get autoclean 2>/dev/null || true
success "Docker removal completed"
}
# Remove existing Docker installation on macOS
remove_docker_macos() {
info "🗑️ Removing existing Docker installation..."
# Get the actual user (not root) for operations
local actual_user
if [[ -n "$SUDO_USER" ]]; then
actual_user="$SUDO_USER"
else
actual_user="$(whoami)"
fi
# Stop Docker Desktop if running
if pgrep -f "Docker Desktop" >/dev/null 2>&1; then
info "🛑 Stopping Docker Desktop..."
sudo -u "$actual_user" osascript -e 'quit app "Docker Desktop"' 2>/dev/null || true
sleep 3
fi
# Remove Docker Desktop application
if [[ -d "/Applications/Docker.app" ]]; then
info "🗑️ Removing Docker Desktop application..."
rm -rf "/Applications/Docker.app"
fi
# Remove Docker CLI tools installed via Homebrew
if command_exists brew; then
info "🗑️ Removing Docker via Homebrew..."
sudo -u "$actual_user" brew uninstall --cask docker 2>/dev/null || true
sudo -u "$actual_user" brew uninstall docker 2>/dev/null || true
sudo -u "$actual_user" brew uninstall docker-compose 2>/dev/null || true
sudo -u "$actual_user" brew uninstall docker-machine 2>/dev/null || true
sudo -u "$actual_user" brew uninstall docker-buildx 2>/dev/null || true
sudo -u "$actual_user" brew uninstall containerd 2>/dev/null || true
fi
# Remove Docker data directories
local docker_dirs=(
"/Users/$actual_user/.docker"
"/Users/$actual_user/Library/Preferences/com.docker.docker.plist"
"/Users/$actual_user/Library/Saved Application State/com.electron.docker-frontend.savedState"
"/Users/$actual_user/Library/Group Containers/group.com.docker"
"/Users/$actual_user/Library/Containers/com.docker.docker"
"/Users/$actual_user/Library/Application Support/Docker Desktop"
"/Users/$actual_user/Library/Logs/Docker Desktop"
"/Users/$actual_user/Library/Preferences/com.electron.docker-frontend.plist"
"/Users/$actual_user/Library/Caches/com.docker.docker"
)
for dir in "${docker_dirs[@]}"; do
if [[ -e "$dir" ]]; then
info "🗑️ Removing: $dir"
rm -rf "$dir"
fi
done
# Remove Docker symlinks and binaries
local docker_links=(
"/usr/local/bin/docker"
"/usr/local/bin/docker-compose"
"/usr/local/bin/docker-machine"
"/usr/local/bin/docker-buildx"
"/usr/local/bin/containerd"
"/usr/local/bin/containerd-shim"
"/usr/local/bin/containerd-shim-runc-v2"
"/usr/local/bin/ctr"
"/usr/local/bin/runc"
"/usr/local/bin/docker-credential-desktop"
"/usr/local/bin/docker-credential-ecr-login"
"/usr/local/bin/docker-credential-osxkeychain"
"/usr/local/bin/kubectl"
"/usr/local/bin/kubectl.docker"
"/usr/local/bin/vpnkit"
"/usr/local/bin/com.docker.cli"
)
for link in "${docker_links[@]}"; do
if [[ -L "$link" ]] || [[ -f "$link" ]]; then
info "🗑️ Removing: $link"
rm -f "$link"
fi
done
# Kill any remaining Docker processes
pkill -f docker 2>/dev/null || true
pkill -f com.docker 2>/dev/null || true
pkill -f containerd 2>/dev/null || true
success "Docker removal completed"
}
# Check existing Docker installation on macOS
check_existing_docker_macos() {
local docker_version=""
local docker_desktop_version=""
local installation_method=""
# Check for Docker command
if command_exists docker; then
docker_version=$(docker --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n1)
fi
# Check for Docker Desktop
if [[ -d "/Applications/Docker.app" ]]; then
docker_desktop_version=$(defaults read /Applications/Docker.app/Contents/Info.plist CFBundleShortVersionString 2>/dev/null || echo "unknown")
installation_method="Docker Desktop"
fi
# Check if installed via Homebrew
if command_exists brew; then
local actual_user
if [[ -n "$SUDO_USER" ]]; then
actual_user="$SUDO_USER"
else
actual_user="$(whoami)"
fi
if sudo -u "$actual_user" brew list --cask docker >/dev/null 2>&1; then
installation_method="Homebrew Cask"
elif sudo -u "$actual_user" brew list docker >/dev/null 2>&1; then
installation_method="Homebrew"
fi
fi
# Return results
echo "docker_version=$docker_version"
echo "docker_desktop_version=$docker_desktop_version"
echo "installation_method=$installation_method"
}
# Install Docker for the current platform
install_docker() {
info "🐳 Installing Docker..."
case "$OS_NAME_LOWER" in
"macos")
# Check for existing Docker installation
local docker_info
docker_info=$(check_existing_docker_macos)
local docker_version=$(echo "$docker_info" | grep "docker_version=" | cut -d'=' -f2)
local docker_desktop_version=$(echo "$docker_info" | grep "docker_desktop_version=" | cut -d'=' -f2)
local installation_method=$(echo "$docker_info" | grep "installation_method=" | cut -d'=' -f2)
# If Docker is already installed, ask for confirmation to reinstall
if [[ -n "$docker_version" ]] || [[ -n "$docker_desktop_version" ]] || [[ -n "$installation_method" ]]; then
warning "Existing Docker installation detected:"
if [[ -n "$docker_version" ]]; then
info " Docker CLI version: $docker_version"
fi
if [[ -n "$docker_desktop_version" ]]; then
info " Docker Desktop version: $docker_desktop_version"
fi
if [[ -n "$installation_method" ]]; then
info " Installation method: $installation_method"
fi
echo
warning "⚠️ To ensure a clean HOPS installation, we recommend removing the existing Docker installation."
warning " This will remove all Docker data, containers, images, and volumes."
echo
read -p "❓ Do you want to remove the existing Docker installation and reinstall? (y/N): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
remove_docker_macos
# Double-check removal was successful
sleep 2
if command_exists docker && docker info >/dev/null 2>&1; then
error_exit "Docker removal failed. Please manually remove Docker and try again."
fi
else
info "Keeping existing Docker installation. Checking if it's compatible..."
# Check if existing Docker is compatible
if ! docker info >/dev/null 2>&1; then
error_exit "Existing Docker installation is not running. Please start Docker Desktop manually or choose to reinstall."
fi
# Check Docker Compose
if ! docker compose version >/dev/null 2>&1; then
error_exit "Docker Compose not available in existing installation. Please reinstall Docker Desktop."
fi
success "Existing Docker installation is compatible"
return 0
fi
fi
# Install fresh Docker Desktop
info "📦 Installing Docker Desktop for Mac..."
# Check if Homebrew is available
if ! command_exists brew; then
warning "Homebrew not found. Installing Homebrew first..."
# Get the actual user (not root) to install Homebrew
local actual_user
if [[ -n "$SUDO_USER" ]]; then
actual_user="$SUDO_USER"
else
actual_user="$(whoami)"
fi
# Install Homebrew as the actual user, not root
sudo -u "$actual_user" /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# Add Homebrew to PATH for current session
if [[ -f "/opt/homebrew/bin/brew" ]]; then
eval "$(/opt/homebrew/bin/brew shellenv)"
elif [[ -f "/usr/local/bin/brew" ]]; then
eval "$(/usr/local/bin/brew shellenv)"
fi
fi
# Install Docker Desktop via Homebrew Cask
info "📦 Installing Docker Desktop via Homebrew..."
# Get the actual user (not root) to run brew commands
local actual_user
if [[ -n "$SUDO_USER" ]]; then
actual_user="$SUDO_USER"
else
actual_user="$(whoami)"
fi
# Remove conflicting compose-bridge binary if it exists
if [[ -f "/usr/local/bin/compose-bridge" ]]; then
info "🗑️ Removing conflicting compose-bridge binary..."
rm -f "/usr/local/bin/compose-bridge" 2>/dev/null || true
fi
sudo -u "$actual_user" brew install --cask docker
# Start Docker Desktop
info "🚀 Starting Docker Desktop..."
open -a Docker
# Wait for Docker to start
info "⏳ Waiting for Docker Desktop to start (this may take a few minutes)..."
local max_wait=120
local wait_time=0
while ! docker info >/dev/null 2>&1; do
if [[ $wait_time -ge $max_wait ]]; then
error_exit "Docker Desktop failed to start within $max_wait seconds. Please start it manually and try again."
fi
sleep 5
((wait_time += 5))
echo -n "."
done
echo
success "Docker Desktop installed and started successfully"
;;
"ubuntu"|"debian"|"linuxmint"|"mint")
# Check for existing Docker installation
if command_exists docker; then
local docker_version=$(docker --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n1)
warning "Existing Docker installation detected:"
if [[ -n "$docker_version" ]]; then
info " Docker version: $docker_version"
fi
echo
warning "⚠️ To ensure a clean HOPS installation, we recommend removing the existing Docker installation."
warning " This will remove all Docker data, containers, images, and volumes."
echo
read -p "❓ Do you want to remove the existing Docker installation and reinstall? (y/N): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
remove_docker_linux
# Double-check removal was successful
sleep 2
if command_exists docker && docker info >/dev/null 2>&1; then
error_exit "Docker removal failed. Please manually remove Docker and try again."
fi
else
info "Keeping existing Docker installation. Checking if it's compatible..."
# Check if existing Docker is compatible
if ! docker info >/dev/null 2>&1; then
error_exit "Existing Docker installation is not running. Please start Docker service manually or choose to reinstall."
fi
# Check Docker Compose
if ! docker compose version >/dev/null 2>&1; then
error_exit "Docker Compose not available in existing installation. Please reinstall Docker."
fi
success "Existing Docker installation is compatible"
return 0
fi
fi
# Install fresh Docker using the official script
info "📦 Installing Docker Engine..."
curl -fsSL https://get.docker.com | sh
# Add user to docker group if we're running with sudo
if [[ -n "$SUDO_USER" ]]; then
usermod -aG docker "$SUDO_USER"
fi
# Start and enable Docker service
start_service docker
enable_service docker
success "Docker installed and configured"
;;
*)
error_exit "Unsupported OS for Docker installation: $OS_NAME"
;;
esac
}
# Check if Docker is properly installed and running
check_docker_installation() {
info "🐳 Checking Docker installation..."
# Check if Docker command exists
if ! command_exists docker; then
return 1
fi
# Check if Docker daemon is running
if ! docker info >/dev/null 2>&1; then
return 1
fi
# Check Docker Compose
if ! docker compose version >/dev/null 2>&1; then
return 1
fi
success "Docker is properly installed and running"
return 0
}