From ab4ab149ad7ce51b83d35dd7bc097e72627a6bd9 Mon Sep 17 00:00:00 2001 From: Yury Kossakovsky Date: Thu, 11 Dec 2025 16:44:39 -0700 Subject: [PATCH] feat: add system improvements - doctor diagnostics, update preview, and wizard service groups - add make doctor command for system diagnostics (dns, ssl, containers, disk, memory) - add make update-preview for dry-run update checks without applying changes - add service groups to wizard with quick start pack (n8n + monitoring + postgresus + portainer) - add make switch-beta and switch-stable commands for branch switching - update readme with organized commands table --- Makefile | 10 +- README.md | 37 +++-- scripts/04_wizard.sh | 48 ++++++ scripts/doctor.sh | 305 ++++++++++++++++++++++++++++++++++++++ scripts/update_preview.sh | 200 +++++++++++++++++++++++++ 5 files changed, 584 insertions(+), 16 deletions(-) create mode 100755 scripts/doctor.sh create mode 100755 scripts/update_preview.sh diff --git a/Makefile b/Makefile index abd9e14..79c482f 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: help install update clean logs status monitor restarts switch-beta switch-stable +.PHONY: help install update update-preview clean logs status monitor restarts doctor switch-beta switch-stable PROJECT_NAME := localai @@ -7,6 +7,7 @@ help: @echo "" @echo " make install Full installation" @echo " make update Update system and services" + @echo " make update-preview Preview available updates (dry-run)" @echo " make clean Remove unused Docker resources" @echo "" @echo " make logs View logs (all services)" @@ -14,6 +15,7 @@ help: @echo " make status Show container status" @echo " make monitor Live CPU/memory monitoring" @echo " make restarts Show restart count per container" + @echo " make doctor Run system diagnostics" @echo "" @echo " make switch-beta Switch to beta (develop branch)" @echo " make switch-stable Switch to stable (main branch)" @@ -24,6 +26,9 @@ install: update: sudo bash ./scripts/update.sh +update-preview: + bash ./scripts/update_preview.sh + clean: sudo bash ./scripts/docker_cleanup.sh @@ -47,6 +52,9 @@ restarts: echo "$$name restarted $$restarts times"; \ done +doctor: + bash ./scripts/doctor.sh + switch-beta: git restore docker-compose.yml git checkout develop diff --git a/README.md b/README.md index 51f6b04..4e5cd64 100644 --- a/README.md +++ b/README.md @@ -256,23 +256,30 @@ This can be useful for removing old images and freeing up space, but be aware th The project includes a Makefile for simplified command execution: -| Command | Description | -|---------|-------------| -| `make install` | Full installation | -| `make update` | Update system and services | -| `make clean` | Remove unused Docker resources | -| `make logs` | View logs (all services) | -| `make logs s=n8n` | View logs for specific service | -| `make status` | Show container status | -| `make monitor` | Live CPU/memory monitoring | -| `make restarts` | Show restart count per container | +### Installation & Updates -### Switch Versions +| Command | Description | +| --------------------- | ---------------------------------------------------- | +| `make install` | Full installation | +| `make update` | Update system and services | +| `make update-preview` | Preview available updates without applying (dry-run) | +| `make clean` | Remove unused Docker resources | -| Command | Description | -|---------|-------------| -| `make switch-beta` | Switch to beta (develop branch) | -| `make switch-stable` | Switch to stable (main branch) | +### Monitoring & Logs + +| Command | Description | +| ----------------------- | -------------------------------------------------------- | +| `make logs` | View logs (all services) | +| `make logs s=` | View logs for specific service (e.g., `make logs s=n8n`) | +| `make status` | Show container status | +| `make monitor` | Live CPU/memory monitoring | +| `make restarts` | Show restart count per container | + +### Diagnostics + +| Command | Description | +| ------------- | ------------------------------------------------------------------ | +| `make doctor` | Run system diagnostics (checks DNS, SSL, containers, disk, memory) | Run `make help` for the full list of available commands. diff --git a/scripts/04_wizard.sh b/scripts/04_wizard.sh index 932ba63..aa3cb15 100755 --- a/scripts/04_wizard.sh +++ b/scripts/04_wizard.sh @@ -35,6 +35,54 @@ check_whiptail ORIGINAL_DEBIAN_FRONTEND="$DEBIAN_FRONTEND" export DEBIAN_FRONTEND=dialog +# --- Quick Start Pack Selection --- +# First screen: choose between Quick Start Pack or Custom Selection + +PACK_CHOICE=$(whiptail --title "Installation Mode" --menu \ + "Choose how you want to set up your services:\n\nQuick Start uses a recommended set of services.\nCustom lets you pick individual services." 15 70 2 \ + "quick" "Quick Start (Recommended: n8n + monitoring + backups + management)" \ + "custom" "Custom Selection (Choose individual services)" \ + 3>&1 1>&2 2>&3) + +pack_exitstatus=$? +if [ $pack_exitstatus -ne 0 ]; then + log_info "Installation cancelled by user." + exit 0 +fi + +# If Quick Start is selected, set the Base Pack profiles and exit +if [ "$PACK_CHOICE" == "quick" ]; then + log_info "Quick Start Pack selected: n8n + monitoring + postgresus + portainer" + + # Base Pack profiles + COMPOSE_PROFILES_VALUE="n8n,monitoring,postgresus,portainer" + + # Ensure .env file exists + if [ ! -f "$ENV_FILE" ]; then + touch "$ENV_FILE" + fi + + # Remove existing COMPOSE_PROFILES line if it exists + if grep -q "^COMPOSE_PROFILES=" "$ENV_FILE"; then + sed -i.bak "\|^COMPOSE_PROFILES=|d" "$ENV_FILE" + fi + + # Add the new COMPOSE_PROFILES line + echo "COMPOSE_PROFILES=${COMPOSE_PROFILES_VALUE}" >> "$ENV_FILE" + log_info "The following Docker Compose profiles will be active: ${COMPOSE_PROFILES_VALUE}" + + # Restore original DEBIAN_FRONTEND + if [ -n "$ORIGINAL_DEBIAN_FRONTEND" ]; then + export DEBIAN_FRONTEND="$ORIGINAL_DEBIAN_FRONTEND" + else + unset DEBIAN_FRONTEND + fi + + exit 0 +fi + +# --- Custom Selection Mode (existing logic) --- + # --- Read current COMPOSE_PROFILES from .env --- CURRENT_PROFILES_VALUE="" if [ -f "$ENV_FILE" ]; then diff --git a/scripts/doctor.sh b/scripts/doctor.sh new file mode 100755 index 0000000..df72e19 --- /dev/null +++ b/scripts/doctor.sh @@ -0,0 +1,305 @@ +#!/bin/bash + +# System diagnostics script for n8n-install +# Checks DNS, SSL, containers, disk space, memory, and configuration + +# Source the utilities file +source "$(dirname "$0")/utils.sh" + +# Get the directory where the script resides +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" +PROJECT_ROOT="$( cd "$SCRIPT_DIR/.." &> /dev/null && pwd )" +ENV_FILE="$PROJECT_ROOT/.env" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Counters +ERRORS=0 +WARNINGS=0 +OK=0 + +# Print status functions +print_ok() { + echo -e " ${GREEN}[OK]${NC} $1" + OK=$((OK + 1)) +} + +print_warning() { + echo -e " ${YELLOW}[WARNING]${NC} $1" + WARNINGS=$((WARNINGS + 1)) +} + +print_error() { + echo -e " ${RED}[ERROR]${NC} $1" + ERRORS=$((ERRORS + 1)) +} + +print_info() { + echo -e " ${BLUE}[INFO]${NC} $1" +} + +echo "" +echo "========================================" +echo " n8n-install System Diagnostics" +echo "========================================" +echo "" + +# Check if .env file exists +echo "Configuration:" +echo "--------------" + +if [ -f "$ENV_FILE" ]; then + print_ok ".env file exists" + + # Load environment variables + set -a + source "$ENV_FILE" + set +a + + # Check required variables + if [ -n "$USER_DOMAIN_NAME" ]; then + print_ok "USER_DOMAIN_NAME is set: $USER_DOMAIN_NAME" + else + print_error "USER_DOMAIN_NAME is not set" + fi + + if [ -n "$LETSENCRYPT_EMAIL" ]; then + print_ok "LETSENCRYPT_EMAIL is set" + else + print_warning "LETSENCRYPT_EMAIL is not set (SSL certificates may not work)" + fi + + if [ -n "$COMPOSE_PROFILES" ]; then + print_ok "Active profiles: $COMPOSE_PROFILES" + else + print_warning "No service profiles are active" + fi +else + print_error ".env file not found at $ENV_FILE" + echo "" + echo "Run 'make install' to set up the environment." + exit 1 +fi + +echo "" + +# Check Docker +echo "Docker:" +echo "-------" + +if command -v docker &> /dev/null; then + print_ok "Docker is installed" + + if docker info &> /dev/null; then + print_ok "Docker daemon is running" + else + print_error "Docker daemon is not running or not accessible" + fi +else + print_error "Docker is not installed" +fi + +if command -v docker-compose &> /dev/null || docker compose version &> /dev/null; then + print_ok "Docker Compose is available" +else + print_warning "Docker Compose is not available" +fi + +echo "" + +# Check disk space +echo "Disk Space:" +echo "-----------" + +DISK_USAGE=$(df -h / | awk 'NR==2 {print $5}' | tr -d '%') +DISK_AVAIL=$(df -h / | awk 'NR==2 {print $4}') + +if [ "$DISK_USAGE" -lt 80 ]; then + print_ok "Disk usage: ${DISK_USAGE}% (${DISK_AVAIL} available)" +elif [ "$DISK_USAGE" -lt 90 ]; then + print_warning "Disk usage: ${DISK_USAGE}% (${DISK_AVAIL} available) - Consider freeing space" +else + print_error "Disk usage: ${DISK_USAGE}% (${DISK_AVAIL} available) - Critical!" +fi + +# Check Docker disk usage +DOCKER_DISK=$(docker system df --format '{{.Size}}' 2>/dev/null | head -1) +if [ -n "$DOCKER_DISK" ]; then + print_info "Docker using: $DOCKER_DISK" +fi + +echo "" + +# Check memory +echo "Memory:" +echo "-------" + +if command -v free &> /dev/null; then + MEM_TOTAL=$(free -h | awk '/^Mem:/ {print $2}') + MEM_USED=$(free -h | awk '/^Mem:/ {print $3}') + MEM_AVAIL=$(free -h | awk '/^Mem:/ {print $7}') + MEM_PERCENT=$(free | awk '/^Mem:/ {printf("%.0f", $3/$2 * 100)}') + + if [ "$MEM_PERCENT" -lt 80 ]; then + print_ok "Memory usage: ${MEM_PERCENT}% (${MEM_AVAIL} available of ${MEM_TOTAL})" + elif [ "$MEM_PERCENT" -lt 90 ]; then + print_warning "Memory usage: ${MEM_PERCENT}% (${MEM_AVAIL} available)" + else + print_error "Memory usage: ${MEM_PERCENT}% - High memory pressure!" + fi +else + print_info "Memory info not available (free command not found)" +fi + +echo "" + +# Check containers +echo "Containers:" +echo "-----------" + +RUNNING=$(docker ps -q 2>/dev/null | wc -l) +TOTAL=$(docker ps -aq 2>/dev/null | wc -l) + +print_info "$RUNNING of $TOTAL containers running" + +# Check for containers with high restart counts +HIGH_RESTARTS=0 +while read -r line; do + if [ -n "$line" ]; then + name=$(echo "$line" | cut -d'|' -f1) + restarts=$(echo "$line" | cut -d'|' -f2) + if [ "$restarts" -gt 3 ]; then + print_warning "$name has restarted $restarts times" + HIGH_RESTARTS=$((HIGH_RESTARTS + 1)) + fi + fi +done < <(docker ps --format '{{.Names}}|{{.Status}}' 2>/dev/null | while read container; do + name=$(echo "$container" | cut -d'|' -f1) + restarts=$(docker inspect --format '{{.RestartCount}}' "$name" 2>/dev/null || echo "0") + echo "$name|$restarts" +done) + +if [ "$HIGH_RESTARTS" -eq 0 ]; then + print_ok "No containers with excessive restarts" +fi + +# Check unhealthy containers +UNHEALTHY=$(docker ps --filter "health=unhealthy" --format '{{.Names}}' 2>/dev/null) +if [ -n "$UNHEALTHY" ]; then + for container in $UNHEALTHY; do + print_error "Container $container is unhealthy" + done +else + print_ok "No unhealthy containers" +fi + +echo "" + +# Check DNS resolution +echo "DNS Resolution:" +echo "---------------" + +check_dns() { + local hostname="$1" + local varname="$2" + + if [ -z "$hostname" ] || [ "$hostname" == "yourdomain.com" ] || [[ "$hostname" == *".yourdomain.com" ]]; then + return + fi + + if host "$hostname" &> /dev/null; then + print_ok "$varname ($hostname) resolves" + else + print_error "$varname ($hostname) does not resolve" + fi +} + +# Only check if we have a real domain +if [ -n "$USER_DOMAIN_NAME" ] && [ "$USER_DOMAIN_NAME" != "yourdomain.com" ]; then + check_dns "$N8N_HOSTNAME" "N8N_HOSTNAME" + check_dns "$GRAFANA_HOSTNAME" "GRAFANA_HOSTNAME" + check_dns "$PORTAINER_HOSTNAME" "PORTAINER_HOSTNAME" + check_dns "$WELCOME_HOSTNAME" "WELCOME_HOSTNAME" +else + print_info "Skipping DNS checks (no domain configured)" +fi + +echo "" + +# Check SSL (Caddy) +echo "SSL/Caddy:" +echo "----------" + +if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "caddy"; then + print_ok "Caddy container is running" + + # Check if Caddy can reach the config + if docker exec caddy caddy validate --config /etc/caddy/Caddyfile &> /dev/null; then + print_ok "Caddyfile is valid" + else + print_warning "Caddyfile validation failed (may be fine if using default)" + fi +else + print_warning "Caddy container is not running" +fi + +echo "" + +# Check key services +echo "Key Services:" +echo "-------------" + +check_service() { + local container="$1" + local port="$2" + + if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^${container}$"; then + print_ok "$container is running" + else + if [[ ",$COMPOSE_PROFILES," == *",$container,"* ]] || [ "$container" == "postgres" ] || [ "$container" == "redis" ]; then + print_error "$container is not running (but expected)" + fi + fi +} + +check_service "postgres" "5432" +check_service "redis" "6379" +check_service "caddy" "80" + +if [[ ",$COMPOSE_PROFILES," == *",n8n,"* ]]; then + check_service "n8n" "5678" +fi + +if [[ ",$COMPOSE_PROFILES," == *",monitoring,"* ]]; then + check_service "grafana" "3000" + check_service "prometheus" "9090" +fi + +echo "" + +# Summary +echo "========================================" +echo " Summary" +echo "========================================" +echo "" + +echo -e " ${GREEN}OK:${NC} $OK" +echo -e " ${YELLOW}Warnings:${NC} $WARNINGS" +echo -e " ${RED}Errors:${NC} $ERRORS" +echo "" + +if [ $ERRORS -gt 0 ]; then + echo -e "${RED}Some issues were found. Please review the errors above.${NC}" + exit 1 +elif [ $WARNINGS -gt 0 ]; then + echo -e "${YELLOW}System is mostly healthy with some warnings.${NC}" + exit 0 +else + echo -e "${GREEN}System is healthy!${NC}" + exit 0 +fi diff --git a/scripts/update_preview.sh b/scripts/update_preview.sh new file mode 100755 index 0000000..b3de953 --- /dev/null +++ b/scripts/update_preview.sh @@ -0,0 +1,200 @@ +#!/bin/bash + +# Preview available updates for Docker images without applying them +# This is a "dry-run" mode for the update process + +set -e + +# Source the utilities file +source "$(dirname "$0")/utils.sh" + +# Get the directory where the script resides +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" +PROJECT_ROOT="$( cd "$SCRIPT_DIR/.." &> /dev/null && pwd )" +ENV_FILE="$PROJECT_ROOT/.env" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Check if .env file exists +if [ ! -f "$ENV_FILE" ]; then + log_error "The .env file ('$ENV_FILE') was not found." + exit 1 +fi + +# Load environment variables +set -a +source "$ENV_FILE" +set +a + +echo "" +echo "========================================" +echo " Update Preview (Dry Run)" +echo "========================================" +echo "" +echo "Checking for available updates..." +echo "" + +# Function to get local image digest +get_local_digest() { + local image="$1" + docker image inspect "$image" --format='{{index .RepoDigests 0}}' 2>/dev/null | cut -d'@' -f2 | head -c 19 +} + +# Function to get remote image digest (without pulling) +get_remote_digest() { + local image="$1" + # Use docker manifest inspect to get remote digest without pulling + docker manifest inspect "$image" 2>/dev/null | grep -m1 '"digest"' | cut -d'"' -f4 | head -c 19 +} + +# Function to check if an update is available +check_image_update() { + local service_name="$1" + local image="$2" + + # Skip if image is empty + if [ -z "$image" ]; then + return + fi + + local local_digest=$(get_local_digest "$image") + local remote_digest=$(get_remote_digest "$image") + + if [ -z "$local_digest" ]; then + printf " ${YELLOW}%-20s${NC} %-45s ${BLUE}[Not installed]${NC}\n" "$service_name" "$image" + return + fi + + if [ -z "$remote_digest" ]; then + printf " ${YELLOW}%-20s${NC} %-45s ${YELLOW}[Cannot check]${NC}\n" "$service_name" "$image" + return + fi + + if [ "$local_digest" != "$remote_digest" ]; then + printf " ${GREEN}%-20s${NC} %-45s ${GREEN}[Update available]${NC}\n" "$service_name" "$image" + echo " Local: $local_digest..." + echo " Remote: $remote_digest..." + UPDATES_AVAILABLE=$((UPDATES_AVAILABLE + 1)) + else + printf " ${NC}%-20s${NC} %-45s ${NC}[Up to date]${NC}\n" "$service_name" "$image" + fi +} + +# Counter for available updates +UPDATES_AVAILABLE=0 + +# Get list of images from docker-compose +log_info "Scanning images from docker-compose.yml..." +echo "" + +# Core services (always checked) +echo "Core Services:" +echo "--------------" +check_image_update "postgres" "postgres:${POSTGRES_VERSION:-17}-alpine" +check_image_update "redis" "valkey/valkey:8-alpine" +check_image_update "caddy" "caddy:2-alpine" +echo "" + +# Check n8n if profile is active +if [[ ",$COMPOSE_PROFILES," == *",n8n,"* ]]; then + echo "n8n Services:" + echo "-------------" + check_image_update "n8n" "docker.n8n.io/n8nio/n8n:${N8N_VERSION:-latest}" + check_image_update "n8n-runner" "n8nio/runners:${N8N_VERSION:-latest}" + echo "" +fi + +# Check monitoring if profile is active +if [[ ",$COMPOSE_PROFILES," == *",monitoring,"* ]]; then + echo "Monitoring Services:" + echo "--------------------" + check_image_update "grafana" "grafana/grafana:latest" + check_image_update "prometheus" "prom/prometheus:latest" + check_image_update "node-exporter" "prom/node-exporter:latest" + check_image_update "cadvisor" "gcr.io/cadvisor/cadvisor:latest" + echo "" +fi + +# Check other common services +if [[ ",$COMPOSE_PROFILES," == *",flowise,"* ]]; then + echo "Flowise:" + echo "--------" + check_image_update "flowise" "flowiseai/flowise:latest" + echo "" +fi + +if [[ ",$COMPOSE_PROFILES," == *",open-webui,"* ]]; then + echo "Open WebUI:" + echo "-----------" + check_image_update "open-webui" "ghcr.io/open-webui/open-webui:main" + echo "" +fi + +if [[ ",$COMPOSE_PROFILES," == *",portainer,"* ]]; then + echo "Portainer:" + echo "----------" + check_image_update "portainer" "portainer/portainer-ce:latest" + echo "" +fi + +if [[ ",$COMPOSE_PROFILES," == *",langfuse,"* ]]; then + echo "Langfuse:" + echo "---------" + check_image_update "langfuse-web" "langfuse/langfuse:latest" + check_image_update "langfuse-worker" "langfuse/langfuse-worker:latest" + echo "" +fi + +if [[ ",$COMPOSE_PROFILES," == *",cpu,"* ]] || [[ ",$COMPOSE_PROFILES," == *",gpu-nvidia,"* ]] || [[ ",$COMPOSE_PROFILES," == *",gpu-amd,"* ]]; then + echo "Ollama:" + echo "-------" + check_image_update "ollama" "ollama/ollama:latest" + echo "" +fi + +if [[ ",$COMPOSE_PROFILES," == *",qdrant,"* ]]; then + echo "Qdrant:" + echo "-------" + check_image_update "qdrant" "qdrant/qdrant:latest" + echo "" +fi + +if [[ ",$COMPOSE_PROFILES," == *",searxng,"* ]]; then + echo "SearXNG:" + echo "--------" + check_image_update "searxng" "searxng/searxng:latest" + echo "" +fi + +if [[ ",$COMPOSE_PROFILES," == *",postgresus,"* ]]; then + echo "Postgresus:" + echo "-----------" + check_image_update "postgresus" "ghcr.io/postgresus/postgresus:latest" + echo "" +fi + +# Summary +echo "========================================" +echo " Summary" +echo "========================================" +echo "" + +if [ $UPDATES_AVAILABLE -gt 0 ]; then + echo -e "${GREEN}$UPDATES_AVAILABLE update(s) available.${NC}" + echo "" + echo "To apply updates, run:" + echo " make update" + echo "" + echo "Or manually:" + echo " docker compose -p localai pull" + echo " docker compose -p localai up -d" +else + echo -e "${GREEN}All images are up to date!${NC}" +fi + +echo ""