diff --git a/scripts/deploy-homelab.sh b/scripts/deploy-homelab.sh index 24e6597..b3535a6 100755 --- a/scripts/deploy-homelab.sh +++ b/scripts/deploy-homelab.sh @@ -30,198 +30,131 @@ log_error() { echo -e "${RED}[ERROR]${NC} $1" } -# Check if running as root -if [ "$EUID" -ne 0 ]; then - log_error "Please run as root (use: sudo ./deploy-homelab.sh)" - exit 1 -fi +#========================================== +# VALIDATION FUNCTIONS +#========================================== -# Get the actual user who invoked sudo -ACTUAL_USER="${SUDO_USER:-$USER}" -if [ "$ACTUAL_USER" = "root" ]; then - log_error "Please run this script with sudo, not as root user" - exit 1 -fi - -# Get script directory (AI-Homelab/scripts) -SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -REPO_DIR="$( cd "$SCRIPT_DIR/.." && pwd )" - -log_info "AI-Homelab Deployment Script" -log_info "Running as user: $ACTUAL_USER" -echo "" - -# Check if .env file exists -if [ ! -f "$REPO_DIR/.env" ]; then - log_error ".env file not found!" - log_info "Please create and configure your .env file first:" - echo " cd $REPO_DIR" - echo " cp .env.example .env" - echo " nano .env" - exit 1 -fi - -# Check if Docker is installed and running -log_info "Validating Docker installation..." - -if ! command -v docker &> /dev/null; then - log_error "Docker is not installed" - log_info "Please run the setup script first:" - log_info " cd ~/AI-Homelab/scripts" - log_info " sudo ./setup-homelab.sh" - exit 1 -fi - -if ! docker info &> /dev/null 2>&1; then - log_error "Docker daemon is not running or not accessible" - echo "" - log_info "Troubleshooting steps:" - log_info " 1. Start Docker: sudo systemctl start docker" - log_info " 2. Enable Docker on boot: sudo systemctl enable docker" - log_info " 3. Check Docker status: sudo systemctl status docker" - log_info " 4. If recently added to docker group, log out and back in" - log_info " 5. Test access: docker ps" - echo "" - log_info "Current user: $ACTUAL_USER" - log_info "Docker group membership: $(groups $ACTUAL_USER | grep -o docker || echo 'NOT IN DOCKER GROUP')" - exit 1 -fi - -log_success "Docker is available and running" -log_info "Docker version: $(docker --version | cut -d' ' -f3 | tr -d ',')" -echo "" - -# Load environment variables for domain check -source "$REPO_DIR/.env" - -if [ -z "$DOMAIN" ]; then - log_error "DOMAIN is not set in .env file" - log_info "Please edit .env and set your DuckDNS domain" - exit 1 -fi - -log_info "Using domain: $DOMAIN" -echo "" - -# Step 1: Create required directories -log_info "Step 1/5: Creating required directories..." -mkdir -p /opt/stacks/core -mkdir -p /opt/stacks/infrastructure -mkdir -p /opt/dockge/data -log_success "Directories created" -echo "" - -# Step 2: Create Docker networks (if they don't exist) -log_info "Step 2/5: Creating Docker networks..." -docker network create homelab-network 2>/dev/null && log_success "Created homelab-network" || log_info "homelab-network already exists" -docker network create traefik-network 2>/dev/null && log_success "Created traefik-network" || log_info "traefik-network already exists" -docker network create dockerproxy-network 2>/dev/null && log_success "Created dockerproxy-network" || log_info "dockerproxy-network already exists" -docker network create media-network 2>/dev/null && log_success "Created media-network" || log_info "media-network already exists" -echo "" - -# Step 3: Deploy core infrastructure (DuckDNS, Traefik, Authelia, Gluetun) -log_info "Step 3/5: Deploying core infrastructure stack..." -log_info " - DuckDNS (Dynamic DNS)" -log_info " - Traefik (Reverse Proxy with SSL)" -log_info " - Authelia (Single Sign-On)" -log_info " - Gluetun (VPN Client)" -echo "" - -# Copy core stack files -log_info "Preparing core stack configuration files..." - -# Safety: Stop existing core stack if running (prevents file conflicts) -if [ -f "/opt/stacks/core/docker-compose.yml" ]; then - log_info "Stopping existing core stack for safe reconfiguration..." - cd /opt/stacks/core && docker compose down 2>/dev/null || true - sleep 2 -fi - -# Clean up any incorrect directory structure from previous runs -if [ -d "/opt/stacks/core/traefik/acme.json" ]; then - log_warning "Removing incorrectly created acme.json directory" - rm -rf /opt/stacks/core/traefik/acme.json -fi -if [ -d "/opt/stacks/core/traefik/traefik.yml" ]; then - log_warning "Removing incorrectly created traefik.yml directory" - rm -rf /opt/stacks/core/traefik/traefik.yml -fi - -# Copy compose file -cp "$REPO_DIR/docker-compose/core.yml" /opt/stacks/core/docker-compose.yml - -# Safely remove and replace config directories -if [ -d "/opt/stacks/core/traefik" ]; then - rm -rf /opt/stacks/core/traefik -fi -if [ -d "/opt/stacks/core/authelia" ]; then - rm -rf /opt/stacks/core/authelia -fi - -cp -r "$REPO_DIR/config-templates/traefik" /opt/stacks/core/ -cp -r "$REPO_DIR/config-templates/authelia" /opt/stacks/core/ - -cp "$REPO_DIR/.env" /opt/stacks/core/.env - -# Create acme.json as a file (not directory) with correct permissions -log_info "Creating acme.json for SSL certificates..." -touch /opt/stacks/core/traefik/acme.json -chmod 600 /opt/stacks/core/traefik/acme.json -log_success "acme.json created with correct permissions" - -# Replace email placeholder in traefik.yml -log_info "Configuring Traefik with email: $ACME_EMAIL..." -sed -i "s/ACME_EMAIL_PLACEHOLDER/${ACME_EMAIL}/g" /opt/stacks/core/traefik/traefik.yml -log_success "Traefik email configured" - -# Replace domain placeholder in authelia configuration -log_info "Configuring Authelia for domain: $DOMAIN..." -sed -i "s/your-domain.duckdns.org/${DOMAIN}/g" /opt/stacks/core/authelia/configuration.yml - -# Configure Authelia admin user from setup script -if [ -f /opt/stacks/.setup-temp/authelia_admin_credentials.tmp ] && [ -f /opt/stacks/.setup-temp/authelia_password_hash.tmp ]; then - log_info "Loading Authelia admin credentials from setup temp files..." - source /opt/stacks/.setup-temp/authelia_admin_credentials.tmp -elif [ -n "${AUTHELIA_ADMIN_USER}" ] && [ -n "${AUTHELIA_ADMIN_EMAIL}" ] && [ -n "${AUTHELIA_ADMIN_PASSWORD}" ]; then - log_info "Loading Authelia admin credentials from .env file..." - ADMIN_USER="${AUTHELIA_ADMIN_USER}" - ADMIN_EMAIL="${AUTHELIA_ADMIN_EMAIL}" - ADMIN_PASSWORD="${AUTHELIA_ADMIN_PASSWORD}" - - # Generate password hash from the password in .env - log_info "Generating password hash from .env credentials..." - docker run --rm authelia/authelia:4.37 authelia crypto hash generate argon2 --password "$ADMIN_PASSWORD" > /tmp/authelia_password_hash_from_env.tmp 2>/dev/null - - if [ $? -eq 0 ]; then - # Create temp directory and files for the rest of the script - mkdir -p /opt/stacks/.setup-temp - echo "ADMIN_USER=$ADMIN_USER" > /opt/stacks/.setup-temp/authelia_admin_credentials.tmp - echo "ADMIN_EMAIL=$ADMIN_EMAIL" >> /opt/stacks/.setup-temp/authelia_admin_credentials.tmp - echo "ADMIN_PASSWORD=$ADMIN_PASSWORD" >> /opt/stacks/.setup-temp/authelia_admin_credentials.tmp - chmod 600 /opt/stacks/.setup-temp/authelia_admin_credentials.tmp - - # Extract just the hash (remove "Digest: " prefix if present) - sed 's/^Digest: //' /tmp/authelia_password_hash_from_env.tmp | grep '^\$argon2' > /opt/stacks/.setup-temp/authelia_password_hash.tmp - chmod 600 /opt/stacks/.setup-temp/authelia_password_hash.tmp - rm -f /tmp/authelia_password_hash_from_env.tmp - - log_success "Credentials loaded from .env file" - else - log_error "Failed to generate password hash from .env credentials" - ADMIN_USER="" - ADMIN_EMAIL="" +validate_prerequisites() { + # Check if .env file exists + if [ ! -f "$REPO_DIR/.env" ]; then + log_error ".env file not found!" + log_info "Please create and configure your .env file first:" + echo " cd $REPO_DIR" + echo " cp .env.example .env" + echo " nano .env" + exit 1 fi -fi -if [ -f /opt/stacks/.setup-temp/authelia_admin_credentials.tmp ] && [ -f /opt/stacks/.setup-temp/authelia_password_hash.tmp ]; then - source /opt/stacks/.setup-temp/authelia_admin_credentials.tmp - - if [ -n "$ADMIN_USER" ] && [ -n "$ADMIN_EMAIL" ]; then - log_success "Using credentials: $ADMIN_USER ($ADMIN_EMAIL)" + # Check if Docker is installed and running + log_info "Validating Docker installation..." + + if ! command -v docker &> /dev/null; then + log_error "Docker is not installed" + log_info "Please run the setup script first:" + log_info " cd $REPO_DIR" + log_info " sudo ./scripts/setup-homelab.sh" + exit 1 + fi + + if ! docker info &> /dev/null 2>&1; then + log_error "Docker daemon is not running or not accessible" + echo "" + log_info "Troubleshooting steps:" + log_info " 1. Start Docker: sudo systemctl start docker" + log_info " 2. Enable Docker on boot: sudo systemctl enable docker" + log_info " 3. Check Docker status: sudo systemctl status docker" + log_info " 4. If recently added to docker group, log out and back in" + log_info " 5. Test access: docker ps" + echo "" + log_info "Current user: $ACTUAL_USER" + log_info "Docker group membership: $(groups $ACTUAL_USER | grep -o docker || echo 'NOT IN DOCKER GROUP')" + exit 1 + fi + + log_success "Docker is available and running" + log_info "Docker version: $(docker --version | cut -d' ' -f3 | tr -d ',')" + echo "" + + # Load environment variables for domain check + source "$REPO_DIR/.env" + + if [ -z "$DOMAIN" ]; then + log_error "DOMAIN is not set in .env file" + log_info "Please edit .env and set your DuckDNS domain" + exit 1 + fi + + log_info "Using domain: $DOMAIN" + echo "" +} + +#========================================== +# DEPLOYMENT STEP FUNCTIONS +#========================================== + +step_1_create_directories() { + log_info "Step 1/7: Creating required directories..." + mkdir -p /opt/stacks/core + mkdir -p /opt/stacks/infrastructure + mkdir -p /opt/dockge/data + log_success "Directories created" + echo "" +} + +step_2_create_networks() { + log_info "Step 2/7: Creating Docker networks..." + docker network create homelab-network 2>/dev/null && log_success "Created homelab-network" || log_info "homelab-network already exists" + docker network create traefik-network 2>/dev/null && log_success "Created traefik-network" || log_info "traefik-network already exists" + docker network create dockerproxy-network 2>/dev/null && log_success "Created dockerproxy-network" || log_info "dockerproxy-network already exists" + docker network create media-network 2>/dev/null && log_success "Created media-network" || log_info "media-network already exists" + echo "" +} + +configure_authelia() { + # Configure Authelia admin user from setup script + if [ -f /opt/stacks/.setup-temp/authelia_admin_credentials.tmp ] && [ -f /opt/stacks/.setup-temp/authelia_password_hash.tmp ]; then + log_info "Loading Authelia admin credentials from setup temp files..." + source /opt/stacks/.setup-temp/authelia_admin_credentials.tmp + elif [ -n "${AUTHELIA_ADMIN_USER}" ] && [ -n "${AUTHELIA_ADMIN_EMAIL}" ] && [ -n "${AUTHELIA_ADMIN_PASSWORD}" ]; then + log_info "Loading Authelia admin credentials from .env file..." + ADMIN_USER="${AUTHELIA_ADMIN_USER}" + ADMIN_EMAIL="${AUTHELIA_ADMIN_EMAIL}" + ADMIN_PASSWORD="${AUTHELIA_ADMIN_PASSWORD}" - # Create users_database.yml with credentials from setup - # Use single quotes in heredoc to prevent variable expansion issues with $ in hash - cat > /opt/stacks/core/authelia/users_database.yml << 'EOF' + # Generate password hash from the password in .env + log_info "Generating password hash from .env credentials..." + docker run --rm authelia/authelia:4.37 authelia crypto hash generate argon2 --password "$ADMIN_PASSWORD" > /tmp/authelia_password_hash_from_env.tmp 2>/dev/null + + if [ $? -eq 0 ]; then + # Create temp directory and files for the rest of the script + mkdir -p /opt/stacks/.setup-temp + echo "ADMIN_USER=$ADMIN_USER" > /opt/stacks/.setup-temp/authelia_admin_credentials.tmp + echo "ADMIN_EMAIL=$ADMIN_EMAIL" >> /opt/stacks/.setup-temp/authelia_admin_credentials.tmp + echo "ADMIN_PASSWORD=$ADMIN_PASSWORD" >> /opt/stacks/.setup-temp/authelia_admin_credentials.tmp + chmod 600 /opt/stacks/.setup-temp/authelia_admin_credentials.tmp + + # Extract just the hash (remove "Digest: " prefix if present) + sed 's/^Digest: //' /tmp/authelia_password_hash_from_env.tmp | grep '^\$argon2' > /opt/stacks/.setup-temp/authelia_password_hash.tmp + chmod 600 /opt/stacks/.setup-temp/authelia_password_hash.tmp + rm -f /tmp/authelia_password_hash_from_env.tmp + + log_success "Credentials loaded from .env file" + else + log_error "Failed to generate password hash from .env credentials" + ADMIN_USER="" + ADMIN_EMAIL="" + fi + fi + + if [ -f /opt/stacks/.setup-temp/authelia_admin_credentials.tmp ] && [ -f /opt/stacks/.setup-temp/authelia_password_hash.tmp ]; then + source /opt/stacks/.setup-temp/authelia_admin_credentials.tmp + + if [ -n "$ADMIN_USER" ] && [ -n "$ADMIN_EMAIL" ]; then + log_success "Using credentials: $ADMIN_USER ($ADMIN_EMAIL)" + + # Create users_database.yml with credentials from setup + # Use single quotes in heredoc to prevent variable expansion issues with $ in hash + cat > /opt/stacks/core/authelia/users_database.yml << 'EOF' ############################################################### # Users Database # ############################################################### @@ -235,12 +168,12 @@ users: - admins - users EOF - # Now safely replace placeholders - # Read hash from file (not bash variable) to avoid shell expansion - # The hash file was written directly from Docker output in setup script - export ADMIN_USER - export ADMIN_EMAIL - python3 << 'PYTHON_EOF' + # Now safely replace placeholders + # Read hash from file (not bash variable) to avoid shell expansion + # The hash file was written directly from Docker output in setup script + export ADMIN_USER + export ADMIN_EMAIL + python3 << 'PYTHON_EOF' # Read password hash from file to completely avoid bash variable expansion with open('/opt/stacks/.setup-temp/authelia_password_hash.tmp', 'r') as f: password_hash = f.read().strip() @@ -266,250 +199,363 @@ users: with open('/opt/stacks/core/authelia/users_database.yml', 'w') as f: f.write(content) PYTHON_EOF - - log_success "Authelia admin user configured from setup script" - echo "" - echo "===========================================" - log_info "Authelia Login Credentials:" - echo " Username: $ADMIN_USER" - echo " Password: $ADMIN_PASSWORD" - echo " Email: $ADMIN_EMAIL" - echo "===========================================" - echo "" - log_warning "SAVE THESE CREDENTIALS!" - - # Save password to file for reference - echo "$ADMIN_PASSWORD" > /opt/stacks/core/authelia/ADMIN_PASSWORD.txt - chmod 600 /opt/stacks/core/authelia/ADMIN_PASSWORD.txt - chown $ACTUAL_USER:$ACTUAL_USER /opt/stacks/core/authelia/ADMIN_PASSWORD.txt - log_info "Password also saved to: /opt/stacks/core/authelia/ADMIN_PASSWORD.txt" - echo "" - - # Clean up credentials files from setup script - rm -f /opt/stacks/.setup-temp/authelia_admin_credentials.tmp - rm -f /opt/stacks/.setup-temp/authelia_password_hash.tmp - rmdir /opt/stacks/.setup-temp 2>/dev/null || true - log_info "Cleaned up temporary setup files" + + log_success "Authelia admin user configured from setup script" + echo "" + echo "===========================================" + log_info "Authelia Login Credentials:" + echo " Username: $ADMIN_USER" + echo " Password: $ADMIN_PASSWORD" + echo " Email: $ADMIN_EMAIL" + echo "===========================================" + echo "" + log_warning "SAVE THESE CREDENTIALS!" + + # Save password to file for reference + echo "$ADMIN_PASSWORD" > /opt/stacks/core/authelia/ADMIN_PASSWORD.txt + chmod 600 /opt/stacks/core/authelia/ADMIN_PASSWORD.txt + chown $ACTUAL_USER:$ACTUAL_USER /opt/stacks/core/authelia/ADMIN_PASSWORD.txt + log_info "Password also saved to: /opt/stacks/core/authelia/ADMIN_PASSWORD.txt" + + # Save full credentials for later reference + { + echo "Username: $ADMIN_USER" + echo "Password: $ADMIN_PASSWORD" + echo "Email: $ADMIN_EMAIL" + } > /opt/stacks/core/authelia/ADMIN_CREDENTIALS.txt + chmod 600 /opt/stacks/core/authelia/ADMIN_CREDENTIALS.txt + chown $ACTUAL_USER:$ACTUAL_USER /opt/stacks/core/authelia/ADMIN_CREDENTIALS.txt + echo "" + + # Clean up credentials files from setup script + rm -f /opt/stacks/.setup-temp/authelia_admin_credentials.tmp + rm -f /opt/stacks/.setup-temp/authelia_password_hash.tmp + rmdir /opt/stacks/.setup-temp 2>/dev/null || true + log_info "Cleaned up temporary setup files" + else + log_warning "Incomplete credentials from setup script" + log_info "Using template users_database.yml - please configure manually" + fi else - log_warning "Incomplete credentials from setup script" - log_info "Using template users_database.yml - please configure manually" + log_warning "No credentials file found from setup script" + log_info "Using template users_database.yml from config-templates" + log_info "Please run setup-homelab.sh first or configure manually" fi -else - log_warning "No credentials file found from setup script" - log_info "Using template users_database.yml from config-templates" - log_info "Please run setup-homelab.sh first or configure manually" -fi - -# Clean up old Authelia database if encryption key changed -# This prevents "encryption key does not appear to be valid" errors -if [ -d "/var/lib/docker/volumes/core_authelia-data/_data" ]; then - log_info "Checking for existing Authelia database..." - # Check if database exists and might have encryption key mismatch - if [ -f "/var/lib/docker/volumes/core_authelia-data/_data/db.sqlite3" ]; then - log_warning "Existing Authelia database found from previous deployment" - log_info "If deployment fails with encryption key errors, run: sudo ./scripts/reset-test-environment.sh" - fi -fi - -# Deploy core stack -cd /opt/stacks/core -docker compose up -d - -log_success "Core infrastructure deployed" -echo "" - -# Wait for Traefik to be ready -log_info "Waiting for Traefik to initialize..." -sleep 10 - -# Check if Traefik is healthy -if docker ps | grep -q "traefik.*Up"; then - log_success "Traefik is running" -else - log_warning "Traefik container check inconclusive, continuing..." -fi -echo "" - -# Step 4: Deploy infrastructure stack (Dockge and monitoring tools) -log_info "Step 4/6: Deploying infrastructure stack..." -log_info " - Dockge (Docker Compose Manager)" -log_info " - Pi-hole (DNS Ad Blocker)" -log_info " - Dozzle (Log Viewer)" -log_info " - Glances (System Monitor)" -log_info " - Docker Proxy (Security)" -log_info " - Watchtower (Automatic Updates)" -echo "" - -# Copy infrastructure stack -cp "$REPO_DIR/docker-compose/infrastructure.yml" /opt/stacks/infrastructure/docker-compose.yml -cp "$REPO_DIR/.env" /opt/stacks/infrastructure/.env - -# Deploy infrastructure stack -cd /opt/stacks/infrastructure -docker compose up -d - -log_success "Infrastructure stack deployed" -echo "" - -# Step 5: Deploy dashboards stack (Homepage and Homarr) -log_info "Step 5/6: Deploying dashboards stack..." -log_info " - Homepage (AI-configurable Dashboard)" -log_info " - Homarr (Modern Dashboard)" -echo "" - -# Copy dashboards stack -mkdir -p /opt/stacks/dashboards -cp "$REPO_DIR/docker-compose/dashboards.yml" /opt/stacks/dashboards/docker-compose.yml -cp "$REPO_DIR/.env" /opt/stacks/dashboards/.env - -# Copy and configure homepage templates -if [ -d "$REPO_DIR/config-templates/homepage" ]; then - cp -r "$REPO_DIR/config-templates/homepage" /opt/stacks/dashboards/ - - # Replace HOMEPAGE_VAR_DOMAIN with actual domain in all homepage config files - # Homepage doesn't support environment variables in configs - find /opt/stacks/dashboards/homepage -type f \( -name "*.yaml" -o -name "*.yml" \) -exec sed -i "s/{{HOMEPAGE_VAR_DOMAIN}}/${DOMAIN}/g" {} \; - - log_info "Homepage configuration templates copied and configured" -fi - -# Deploy dashboards stack -cd /opt/stacks/dashboards -docker compose up -d - -log_success "Dashboards stack deployed" -echo "" - -# Step 6: Deploy additional stacks to Dockge (not started) -log_info "Step 6/7: Preparing additional stacks for Dockge..." -echo "" -log_info "The following stacks can be deployed through Dockge's web UI:" -log_info " - media.yml (Jellyfin, Calibre-web, qBittorrent)" -log_info " - media-management.yml (Sonarr, Radarr, *arr apps)" -log_info " - homeassistant.yml (Home Assistant and accessories)" -log_info " - productivity.yml (Nextcloud, Gitea, wikis)" -log_info " - monitoring.yml (Grafana, Prometheus, etc.)" -log_info " - utilities.yml (Backups, code editors, etc.)" -log_info " - alternatives.yml (Portainer, Authentik)" -echo "" - -# Ask user if they want to pre-pull images for additional stacks -read -p "Pre-pull Docker images for additional stacks? This will take time but speeds up first deployment (y/N): " PULL_IMAGES -PULL_IMAGES=${PULL_IMAGES:-n} - -# Copy additional stacks to Dockge directory -ADDITIONAL_STACKS=("media" "media-management" "homeassistant" "productivity" "monitoring" "utilities" "alternatives") - -for stack in "${ADDITIONAL_STACKS[@]}"; do - mkdir -p "/opt/stacks/$stack" - cp "$REPO_DIR/docker-compose/${stack}.yml" "/opt/stacks/$stack/docker-compose.yml" - cp "$REPO_DIR/.env" "/opt/stacks/$stack/.env" - - # Pre-pull images if requested - if [[ "$PULL_IMAGES" =~ ^[Yy]$ ]]; then - log_info "Pulling images for $stack stack..." - cd "/opt/stacks/$stack" - docker compose pull 2>&1 | grep -E '(Pulling|Downloaded|Already exists|up to date)' || true - fi -done - -log_success "Additional stacks prepared in Dockge" -log_info "These stacks are NOT started - deploy them via Dockge web UI as needed" -echo "" - -# Step 7: Wait for Dockge to be ready and open browser -log_info "Step 7/7: Waiting for Dockge web UI to be ready..." - -DOCKGE_URL="https://dockge.${DOMAIN}" -MAX_WAIT=60 # Maximum wait time in seconds -WAITED=0 - -# Function to check if Dockge is accessible -check_dockge() { - # Try to connect to Dockge (ignore SSL cert warnings for self-signed during startup) - curl -k -s -o /dev/null -w "%{http_code}" "$DOCKGE_URL" 2>/dev/null } -# Wait for Dockge to respond -while [ $WAITED -lt $MAX_WAIT ]; do - HTTP_CODE=$(check_dockge) - if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "302" ] || [ "$HTTP_CODE" = "401" ]; then - log_success "Dockge web UI is ready!" - break - fi - echo -n "." - sleep 2 - WAITED=$((WAITED + 2)) -done - -echo "" -echo "" - -if [ $WAITED -ge $MAX_WAIT ]; then - log_warning "Dockge did not respond within ${MAX_WAIT} seconds" - log_info "It may still be starting up. Check manually at: $DOCKGE_URL" -else - # Try to open browser - log_info "Opening Dockge in your browser..." +step_3_deploy_core() { + log_info "Step 3/7: Deploying core infrastructure stack..." + log_info " - DuckDNS (Dynamic DNS)" + log_info " - Traefik (Reverse Proxy with SSL)" + log_info " - Authelia (Single Sign-On)" + log_info " - Gluetun (VPN Client)" + echo "" - # Detect and use available browser - if command -v xdg-open &> /dev/null; then - xdg-open "$DOCKGE_URL" &> /dev/null & - log_success "Browser opened" - elif command -v gnome-open &> /dev/null; then - gnome-open "$DOCKGE_URL" &> /dev/null & - log_success "Browser opened" - elif command -v firefox &> /dev/null; then - firefox "$DOCKGE_URL" &> /dev/null & - log_success "Browser opened" - elif command -v google-chrome &> /dev/null; then - google-chrome "$DOCKGE_URL" &> /dev/null & - log_success "Browser opened" - else - log_warning "No browser detected. Please manually open: $DOCKGE_URL" + # Copy core stack files + log_info "Preparing core stack configuration files..." + + # Safety: Stop existing core stack if running (prevents file conflicts) + if [ -f "/opt/stacks/core/docker-compose.yml" ]; then + log_info "Stopping existing core stack for safe reconfiguration..." + cd /opt/stacks/core && docker compose down 2>/dev/null || true + sleep 2 fi + + # Clean up any incorrect directory structure from previous runs + if [ -d "/opt/stacks/core/traefik/acme.json" ]; then + log_warning "Removing incorrectly created acme.json directory" + rm -rf /opt/stacks/core/traefik/acme.json + fi + if [ -d "/opt/stacks/core/traefik/traefik.yml" ]; then + log_warning "Removing incorrectly created traefik.yml directory" + rm -rf /opt/stacks/core/traefik/traefik.yml + fi + + # Copy compose file + cp "$REPO_DIR/docker-compose/core.yml" /opt/stacks/core/docker-compose.yml + + # Safely remove and replace config directories + if [ -d "/opt/stacks/core/traefik" ]; then + rm -rf /opt/stacks/core/traefik + fi + if [ -d "/opt/stacks/core/authelia" ]; then + rm -rf /opt/stacks/core/authelia + fi + + cp -r "$REPO_DIR/config-templates/traefik" /opt/stacks/core/ + cp -r "$REPO_DIR/config-templates/authelia" /opt/stacks/core/ + cp "$REPO_DIR/.env" /opt/stacks/core/.env + + # Create acme.json as a file (not directory) with correct permissions + log_info "Creating acme.json for SSL certificates..." + touch /opt/stacks/core/traefik/acme.json + chmod 600 /opt/stacks/core/traefik/acme.json + log_success "acme.json created with correct permissions" + + # Replace email placeholder in traefik.yml + log_info "Configuring Traefik with email: $ACME_EMAIL..." + sed -i "s/ACME_EMAIL_PLACEHOLDER/${ACME_EMAIL}/g" /opt/stacks/core/traefik/traefik.yml + log_success "Traefik email configured" + + # Replace domain placeholder in authelia configuration + log_info "Configuring Authelia for domain: $DOMAIN..." + sed -i "s/your-domain.duckdns.org/${DOMAIN}/g" /opt/stacks/core/authelia/configuration.yml + + # Configure Authelia admin user + configure_authelia + + # Clean up old Authelia database if encryption key changed + # This prevents "encryption key does not appear to be valid" errors + if [ -d "/var/lib/docker/volumes/core_authelia-data/_data" ]; then + log_info "Checking for existing Authelia database..." + # Check if database exists and might have encryption key mismatch + if [ -f "/var/lib/docker/volumes/core_authelia-data/_data/db.sqlite3" ]; then + log_warning "Existing Authelia database found from previous deployment" + log_info "If deployment fails with encryption key errors, run: sudo ./scripts/reset-test-environment.sh" + fi + fi + + # Deploy core stack + cd /opt/stacks/core + docker compose up -d + + log_success "Core infrastructure deployed" + echo "" + + # Wait for Traefik to be ready + log_info "Waiting for Traefik to initialize..." + sleep 10 + + # Check if Traefik is healthy + if docker ps | grep -q "traefik.*Up"; then + log_success "Traefik is running" + else + log_warning "Traefik container check inconclusive, continuing..." + fi + echo "" +} + +step_4_deploy_infrastructure() { + log_info "Step 4/7: Deploying infrastructure stack..." + log_info " - Dockge (Docker Compose Manager)" + log_info " - Pi-hole (DNS Ad Blocker)" + log_info " - Dozzle (Log Viewer)" + log_info " - Glances (System Monitor)" + log_info " - Docker Proxy (Security)" + log_info " - Watchtower (Automatic Updates)" + echo "" + + # Copy infrastructure stack + cp "$REPO_DIR/docker-compose/infrastructure.yml" /opt/stacks/infrastructure/docker-compose.yml + cp "$REPO_DIR/.env" /opt/stacks/infrastructure/.env + + # Deploy infrastructure stack + cd /opt/stacks/infrastructure + docker compose up -d + + log_success "Infrastructure stack deployed" + echo "" +} + +step_5_deploy_dashboards() { + log_info "Step 5/7: Deploying dashboards stack..." + log_info " - Homepage (AI-configurable Dashboard)" + log_info " - Homarr (Modern Dashboard)" + echo "" + + # Copy dashboards stack + mkdir -p /opt/stacks/dashboards + cp "$REPO_DIR/docker-compose/dashboards.yml" /opt/stacks/dashboards/docker-compose.yml + cp "$REPO_DIR/.env" /opt/stacks/dashboards/.env + + # Copy and configure homepage templates + if [ -d "$REPO_DIR/config-templates/homepage" ]; then + cp -r "$REPO_DIR/config-templates/homepage" /opt/stacks/dashboards/ + + # Replace HOMEPAGE_VAR_DOMAIN with actual domain in all homepage config files + # Homepage doesn't support environment variables in configs + find /opt/stacks/dashboards/homepage -type f \( -name "*.yaml" -o -name "*.yml" \) -exec sed -i "s/{{HOMEPAGE_VAR_DOMAIN}}/${DOMAIN}/g" {} \; + + log_info "Homepage configuration templates copied and configured" + fi + + # Deploy dashboards stack + cd /opt/stacks/dashboards + docker compose up -d + + log_success "Dashboards stack deployed" + echo "" +} + +step_6_prepare_additional_stacks() { + log_info "Step 6/7: Preparing additional stacks for Dockge..." + echo "" + log_info "The following stacks can be deployed through Dockge's web UI:" + log_info " - media.yml (Jellyfin, Calibre-web, qBittorrent)" + log_info " - media-management.yml (Sonarr, Radarr, *arr apps)" + log_info " - homeassistant.yml (Home Assistant and accessories)" + log_info " - productivity.yml (Nextcloud, Gitea, wikis)" + log_info " - monitoring.yml (Grafana, Prometheus, etc.)" + log_info " - utilities.yml (Backups, code editors, etc.)" + log_info " - alternatives.yml (Portainer, Authentik)" + echo "" + + # Ask user if they want to pre-pull images for additional stacks + read -p "Pre-pull Docker images for additional stacks? This will take time but speeds up first deployment (y/N): " PULL_IMAGES + PULL_IMAGES=${PULL_IMAGES:-n} + + # Copy additional stacks to Dockge directory + ADDITIONAL_STACKS=("media" "media-management" "homeassistant" "productivity" "monitoring" "utilities" "alternatives") + + for stack in "${ADDITIONAL_STACKS[@]}"; do + mkdir -p "/opt/stacks/$stack" + cp "$REPO_DIR/docker-compose/${stack}.yml" "/opt/stacks/$stack/docker-compose.yml" + cp "$REPO_DIR/.env" "/opt/stacks/$stack/.env" + + # Pre-pull images if requested + if [[ "$PULL_IMAGES" =~ ^[Yy]$ ]]; then + log_info "Pulling images for $stack stack..." + cd "/opt/stacks/$stack" + docker compose pull 2>&1 | grep -E '(Pulling|Downloaded|Already exists|up to date)' || true + fi + done + + log_success "Additional stacks prepared in Dockge" + log_info "These stacks are NOT started - deploy them via Dockge web UI as needed" + echo "" +} + +step_7_wait_for_dockge() { + log_info "Step 7/7: Waiting for Dockge web UI to be ready..." + + DOCKGE_URL="https://dockge.${DOMAIN}" + MAX_WAIT=60 # Maximum wait time in seconds + WAITED=0 + + # Function to check if Dockge is accessible + check_dockge() { + # Try to connect to Dockge (ignore SSL cert warnings for self-signed during startup) + curl -k -s -o /dev/null -w "%{http_code}" "$DOCKGE_URL" 2>/dev/null + } + + # Wait for Dockge to respond + while [ $WAITED -lt $MAX_WAIT ]; do + HTTP_CODE=$(check_dockge) + if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "302" ] || [ "$HTTP_CODE" = "401" ]; then + log_success "Dockge web UI is ready!" + break + fi + echo -n "." + sleep 2 + WAITED=$((WAITED + 2)) + done + + echo "" + echo "" + + if [ $WAITED -ge $MAX_WAIT ]; then + log_warning "Dockge did not respond within ${MAX_WAIT} seconds" + log_info "It may still be starting up. Check manually at: $DOCKGE_URL" + else + # Try to open browser + log_info "Opening Dockge in your browser..." + + # Detect and use available browser + if command -v xdg-open &> /dev/null; then + xdg-open "$DOCKGE_URL" &> /dev/null & + log_success "Browser opened" + elif command -v gnome-open &> /dev/null; then + gnome-open "$DOCKGE_URL" &> /dev/null & + log_success "Browser opened" + elif command -v firefox &> /dev/null; then + firefox "$DOCKGE_URL" &> /dev/null & + log_success "Browser opened" + elif command -v google-chrome &> /dev/null; then + google-chrome "$DOCKGE_URL" &> /dev/null & + log_success "Browser opened" + else + log_warning "No browser detected. Please manually open: $DOCKGE_URL" + fi + fi + echo "" +} + +show_final_summary() { + echo "==========================================" + log_success "Deployment completed successfully!" + echo "==========================================" + echo "" + log_info "Access your services:" + echo "" + echo " 🚀 Dockge: https://dockge.${DOMAIN}" + echo " 🔒 Authelia: https://auth.${DOMAIN}" + echo " 🔀 Traefik: https://traefik.${DOMAIN}" + echo "" + log_info "SSL Certificates:" + echo " 📝 Let's Encrypt certificates will be acquired automatically within 2-5 minutes" + echo " ⚠️ Initial access uses self-signed certs (browser warning is normal)" + echo " 🔓 Ensure ports 80/443 are accessible from internet for Let's Encrypt" + echo " 💾 Admin credentials saved to: /opt/stacks/core/authelia/ADMIN_CREDENTIALS.txt" + echo "" + log_info "Next steps:" + echo "" + echo " 1. Log in to Dockge using your Authelia credentials" + echo " (saved in /opt/stacks/core/authelia/ADMIN_CREDENTIALS.txt)" + echo "" + echo " 2. Deploy additional stacks through Dockge's web UI:" + echo " - media.yml (Jellyfin, Calibre-web, qBittorrent)" + echo " - media-management.yml (Sonarr, Radarr, *arr apps)" + echo " - homeassistant.yml (Home Assistant and accessories)" + echo " - productivity.yml (Nextcloud, Gitea, wikis)" + echo " - monitoring.yml (Grafana, Prometheus, etc.)" + echo " - utilities.yml (Backups, code editors, etc.)" + echo " - alternatives.yml (Portainer, Authentik - optional)" + echo "" + echo " 3. Access your dashboards:" + echo " 🏠 Homepage: https://home.${DOMAIN}" + echo " 📊 Homarr: https://homarr.${DOMAIN}" + echo "" + echo " 4. Configure services via the AI assistant in VS Code" + echo "" + echo "==========================================" + echo "" + log_info "For documentation, see: $REPO_DIR/docs/" + log_info "For troubleshooting, see: $REPO_DIR/docs/quick-reference.md" + echo "" +} + +#========================================== +# MAIN EXECUTION +#========================================== + +# Check if running as root +if [ "$EUID" -ne 0 ]; then + log_error "Please run as root (use: sudo ./deploy-homelab.sh)" + exit 1 fi +# Get the actual user who invoked sudo +ACTUAL_USER="${SUDO_USER:-$USER}" +if [ "$ACTUAL_USER" = "root" ]; then + log_error "Please run this script with sudo, not as root user" + exit 1 +fi + +# Get script directory (AI-Homelab/scripts) +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +REPO_DIR="$( cd "$SCRIPT_DIR/.." && pwd )" + +log_info "AI-Homelab Deployment Script" +log_info "Running as user: $ACTUAL_USER" echo "" -echo "==========================================" -log_success "Deployment completed successfully!" -echo "==========================================" -echo "" -log_info "Access your services:" -echo "" -echo " 🚀 Dockge: $DOCKGE_URL" -echo " 🔒 Authelia: https://auth.${DOMAIN}" -echo " 🔀 Traefik: https://traefik.${DOMAIN}" -echo "" -log_info "SSL Certificates:" -echo " 📝 Let's Encrypt certificates will be acquired automatically within 2-5 minutes" -echo " ⚠️ Initial access uses self-signed certs (browser warning is normal)" -echo " 🔓 Ensure ports 80/443 are accessible from internet for Let's Encrypt" -echo " 💾 Admin password saved to: /opt/stacks/core/authelia/ADMIN_PASSWORD.txt" -echo "" -log_info "Next steps:" -echo "" -echo " 1. Log in to Dockge using your Authelia credentials" -echo " Username: admin" -echo " Password: (saved in /opt/stacks/core/authelia/ADMIN_PASSWORD.txt)" -echo "" -echo " 2. Deploy additional stacks through Dockge's web UI:" -echo " - media.yml (Jellyfin, Calibre-web, qBittorrent)" -echo " - media-management.yml (Sonarr, Radarr, *arr apps)" -echo " - homeassistant.yml (Home Assistant and accessories)" -echo " - productivity.yml (Nextcloud, Gitea, wikis)" -echo " - monitoring.yml (Grafana, Prometheus, etc.)" -echo " - utilities.yml (Backups, code editors, etc.)" -echo " - alternatives.yml (Portainer, Authentik - optional)" -echo "" -echo " 3. Access your dashboards:" -echo " \ud83c\udfe0 Homepage: https://home.${DOMAIN}" -echo " \ud83d\udcca Homarr: https://homarr.${DOMAIN}" -echo "" -echo " 4. Configure services via the AI assistant in VS Code" -echo "" -echo "==========================================" -echo "" -log_info "For documentation, see: $REPO_DIR/docs/" -log_info "For troubleshooting, see: $REPO_DIR/docs/quick-reference.md" -echo "" + +# Execute deployment steps +validate_prerequisites +step_1_create_directories +step_2_create_networks +step_3_deploy_core +step_4_deploy_infrastructure +step_5_deploy_dashboards +step_6_prepare_additional_stacks +step_7_wait_for_dockge +show_final_summary diff --git a/scripts/setup-homelab.sh b/scripts/setup-homelab.sh index 35d8310..182e927 100755 --- a/scripts/setup-homelab.sh +++ b/scripts/setup-homelab.sh @@ -1,15 +1,44 @@ #!/bin/bash # AI-Homelab First-Run Setup Script # This script prepares a fresh Debian installation for homelab deployment -# Run as: sudo ./setup-homelab.sh +# Run as: sudo ./setup-homelab.sh [--yes] +# Options: +# --yes Skip all confirmation prompts (for automated deployments) set -e # Exit on error +# Parse command line arguments +AUTO_YES=false +for arg in "$@"; do + case $arg in + --yes|-y) + AUTO_YES=true + shift + ;; + --help|-h) + echo "Usage: sudo ./setup-homelab.sh [OPTIONS]" + echo "" + echo "Options:" + echo " --yes, -y Skip all confirmation prompts" + echo " --help, -h Show this help message" + exit 0 + ;; + *) + echo "Unknown option: $arg" + echo "Run with --help for usage information" + exit 1 + ;; + esac +done + # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' +CYAN='\033[0;36m' +MAGENTA='\033[0;35m' +BOLD='\033[1m' NC='\033[0m' # No Color # Log functions @@ -29,234 +58,548 @@ log_error() { echo -e "${RED}[ERROR]${NC} $1" } -# Check if running as root +log_progress() { + echo -e "${CYAN}[PROGRESS]${NC} $1" +} + +# Colorized prompt function +prompt_user() { + local prompt_text="$1" + local default="${2:-}" + echo -e "${MAGENTA}${BOLD}[PROMPT]${NC} ${prompt_text}" + if [ -n "$default" ]; then + echo -e "${CYAN}(default: $default)${NC}" + fi +} + +# Confirmation function that respects --yes flag +confirm() { + local prompt="$1" + if [ "$AUTO_YES" = true ]; then + log_info "Auto-confirmed: $prompt" + return 0 + fi + prompt_user "$prompt" + read -p "Continue? (y/N): " -n 1 -r + echo "" + [[ $REPLY =~ ^[Yy]$ ]] +} + +# Check if running with elevated privileges if [ "$EUID" -ne 0 ]; then - log_error "Please run as root (use: sudo ./setup-homelab.sh)" + log_error "Please run with sudo: sudo ./setup-homelab.sh" exit 1 fi # Get the actual user who invoked sudo ACTUAL_USER="${SUDO_USER:-$USER}" -if [ "$ACTUAL_USER" = "root" ]; then - log_error "Please run this script with sudo, not as root user" - exit 1 -fi -# Add trap for cleanup on error -cleanup_on_error() { - local exit_code=$? - if [ $exit_code -ne 0 ]; then - log_error "Script failed with exit code: $exit_code" +#========================================== +# STEP FUNCTIONS +#========================================== + +# Handle root user scenario - grant sudo and exit +handle_root_user() { + log_info "Running as root - checking for non-root users..." + echo "" + + # Find non-root users with home directories and UID >= 1000 + AVAILABLE_USERS=$(awk -F: '$3 >= 1000 && $3 < 65534 && $1 != "nobody" {print $1}' /etc/passwd) + + if [ -z "$AVAILABLE_USERS" ]; then + log_error "No non-root user found on this system" + log_info "Please create a user first:" + log_info " adduser " + exit 1 + fi + + # If only one user, use that one + USER_COUNT=$(echo "$AVAILABLE_USERS" | wc -l) + if [ "$USER_COUNT" -eq 1 ]; then + TARGET_USER="$AVAILABLE_USERS" + log_info "Found user: $TARGET_USER" + else + # Multiple users found, let root choose + log_info "Multiple users found:" + echo "$AVAILABLE_USERS" | nl echo "" - log_info "Partial setup may have occurred. To resume:" - log_info " 1. Review error messages above" - log_info " 2. Fix the issue if possible" - log_info " 3. Re-run: sudo ./setup-homelab.sh" + read -p "Enter username to grant sudo access: " TARGET_USER + + # Validate the entered username + if ! echo "$AVAILABLE_USERS" | grep -q "^${TARGET_USER}$"; then + log_error "Invalid username: $TARGET_USER" + exit 1 + fi + fi + + # Check if user already has sudo + if groups "$TARGET_USER" | grep -q '\bsudo\b'; then + log_success "User $TARGET_USER already has sudo access" + else + # Confirm before granting sudo + echo "" + read -p "Grant sudo access to $TARGET_USER? (y/N): " CONFIRM + if [[ "$CONFIRM" =~ ^[Yy]$ ]]; then + usermod -aG sudo "$TARGET_USER" + log_success "Sudo access granted to $TARGET_USER" + else + log_error "Cannot proceed without sudo access" + exit 1 + fi + fi + + # Exit with instructions + echo "" + echo "==========================================" + log_success "Setup preparation complete!" + echo "==========================================" + echo "" + log_info "Next steps:" + echo "" + log_info " 1. Log in as $TARGET_USER:" + echo " su - $TARGET_USER" + echo "" + log_info " 2. Navigate to the AI-Homelab directory and run setup again:" + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + REPO_DIR="$(dirname "$SCRIPT_DIR")" + echo " cd $REPO_DIR" + echo " sudo ./scripts/setup-homelab.sh" + echo "" + log_warning "Note: You may need to log out and back in for sudo access to take effect" + echo "" + exit 0 +} + +step_0_preflight_checks() { + log_info "Step 0/$STEPS_TOTAL: Running pre-flight checks..." + log_progress "Starting setup process" + + # Check internet connectivity + if ! ping -c 1 -W 2 8.8.8.8 &> /dev/null && ! ping -c 1 -W 2 1.1.1.1 &> /dev/null; then + log_error "No internet connectivity detected" + log_info "Internet access is required for:" + log_info " - Installing packages" + log_info " - Downloading Docker images" + log_info " - Accessing Docker Hub" + exit 1 + fi + + # Check disk space (require at least 5GB free) + AVAILABLE_SPACE=$(df / | tail -1 | awk '{print $4}') + REQUIRED_SPACE=5000000 # 5GB in KB + if [ "$AVAILABLE_SPACE" -lt "$REQUIRED_SPACE" ]; then + log_error "Insufficient disk space on root partition" + log_info "Available: $(($AVAILABLE_SPACE / 1024 / 1024))GB" + log_info "Required: $(($REQUIRED_SPACE / 1024 / 1024))GB" + exit 1 + fi + + log_success "Pre-flight checks passed" + log_info "Internet: Connected" + log_info "Disk space: $(($AVAILABLE_SPACE / 1024 / 1024))GB available" + STEPS_COMPLETED=$((STEPS_COMPLETED + 1)) + log_progress "Completed: $STEPS_COMPLETED/$STEPS_TOTAL steps" + echo "" +} + +step_1_update_system() { + log_info "Step 1/$STEPS_TOTAL: Updating system packages..." + apt-get update && apt-get upgrade -y + log_success "System updated successfully" + STEPS_COMPLETED=$((STEPS_COMPLETED + 1)) + log_progress "Completed: $STEPS_COMPLETED/$STEPS_TOTAL steps" + echo "" +} + +step_2_install_packages() { + log_info "Step 2/$STEPS_TOTAL: Installing required packages..." + apt-get install -y \ + apt-transport-https \ + ca-certificates \ + curl \ + gnupg \ + lsb-release \ + software-properties-common \ + git \ + openssh-server \ + sudo \ + pciutils \ + net-tools \ + ufw + + log_success "Required packages installed" + STEPS_COMPLETED=$((STEPS_COMPLETED + 1)) + log_progress "Completed: $STEPS_COMPLETED/$STEPS_TOTAL steps" + echo "" +} + +step_3_install_docker() { + log_info "Step 3/$STEPS_TOTAL: Installing Docker..." + if command -v docker &> /dev/null && docker --version &> /dev/null; then + log_warning "Docker is already installed ($(docker --version))" + else + # Add Docker's official GPG key + install -m 0755 -d /etc/apt/keyrings + curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc + chmod a+r /etc/apt/keyrings/docker.asc + + # Add the repository to Apt sources + echo \ + "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \ + $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ + tee /etc/apt/sources.list.d/docker.list > /dev/null + + # Update and install Docker + apt-get update + apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin + + log_success "Docker installed successfully ($(docker --version))" + fi + STEPS_COMPLETED=$((STEPS_COMPLETED + 1)) + log_progress "Completed: $STEPS_COMPLETED/$STEPS_TOTAL steps" + echo "" +} + +step_4_configure_user_groups() { + log_info "Step 4/$STEPS_TOTAL: Configuring user groups..." + + # Add user to sudo group if not already + if groups "$ACTUAL_USER" | grep -q '\bsudo\b'; then + log_warning "User $ACTUAL_USER is already in sudo group" + else + usermod -aG sudo "$ACTUAL_USER" + log_success "User $ACTUAL_USER added to sudo group" + fi + + # Add user to docker group + if groups "$ACTUAL_USER" | grep -q '\bdocker\b'; then + log_warning "User $ACTUAL_USER is already in docker group" + else + usermod -aG docker "$ACTUAL_USER" + log_success "User $ACTUAL_USER added to docker group" + fi + STEPS_COMPLETED=$((STEPS_COMPLETED + 1)) + log_progress "Completed: $STEPS_COMPLETED/$STEPS_TOTAL steps" + echo "" +} + +step_5_configure_firewall() { + log_info "Step 5/$STEPS_TOTAL: Configuring firewall..." + # Enable UFW if not already enabled + if ufw status | grep -q "Status: active"; then + log_warning "Firewall is already active" + else + ufw --force enable + log_success "Firewall enabled" + fi + + # Allow SSH if not already allowed + if ufw status | grep -q "22/tcp"; then + log_warning "SSH port 22 is already allowed" + else + ufw allow ssh + log_success "SSH port allowed in firewall" + fi + + # Allow HTTP/HTTPS for web services + ufw allow 80/tcp + ufw allow 443/tcp + log_success "HTTP/HTTPS ports allowed in firewall" + STEPS_COMPLETED=$((STEPS_COMPLETED + 1)) + log_progress "Completed: $STEPS_COMPLETED/$STEPS_TOTAL steps" + echo "" +} + +step_6_configure_ssh() { + log_info "Step 6/$STEPS_TOTAL: Configuring SSH server..." + systemctl enable ssh + systemctl start ssh + + # Check if SSH is running + if systemctl is-active --quiet ssh; then + SSH_PORT=$(grep "^Port" /etc/ssh/sshd_config | awk '{print $2}') + SSH_PORT=${SSH_PORT:-22} + log_success "SSH server is running on port $SSH_PORT" + else + log_warning "SSH server failed to start, check configuration" + fi + STEPS_COMPLETED=$((STEPS_COMPLETED + 1)) + log_progress "Completed: $STEPS_COMPLETED/$STEPS_TOTAL steps" + echo "" +} + +step_7_generate_authelia_secrets() { + log_info "Step 7/$STEPS_TOTAL: Generating Authelia authentication secrets..." + echo "" + + # Validate Docker is available for password hash generation + if ! docker info &> /dev/null 2>&1; then + log_error "Docker is not available for password hash generation" + log_info "Docker must be running to generate Authelia password hashes." + log_info "Please ensure:" + log_info " 1. Docker daemon is started: sudo systemctl start docker" + log_info " 2. User can access Docker: docker ps" + log_info " 3. Log out and log back in if recently added to docker group" + echo "" + log_info "After fixing, re-run: sudo ./setup-homelab.sh" + exit 1 + fi + + log_success "Docker is available for password operations" + echo "" + + # Check if .env file exists in the repo + REPO_ENV_FILE="$REPO_DIR/.env" + if [ ! -f "$REPO_ENV_FILE" ]; then + log_error ".env file not found at $REPO_ENV_FILE" + log_info "Please create .env file from .env.example first" + exit 1 + fi + + # Check if secrets are already set (not placeholder values) + CURRENT_JWT=$(grep "^AUTHELIA_JWT_SECRET=" "$REPO_ENV_FILE" | cut -d'=' -f2) + if [ -n "$CURRENT_JWT" ] && [ "$CURRENT_JWT" != "your-jwt-secret-here" ] && [ "$CURRENT_JWT" != "generate-with-openssl-rand-hex-64" ] && [ ${#CURRENT_JWT} -ge 64 ]; then + log_warning "Authelia secrets appear to already be set in .env" + if [ "$AUTO_YES" = true ]; then + log_info "Auto-confirmed: Keeping existing secrets (--yes mode)" + elif confirm "Regenerate Authelia secrets?"; then + generate_new_secrets + else + log_info "Keeping existing secrets" + fi + else + generate_new_secrets + fi + + # Prompt for admin password + echo "" + log_info "Setting up Authelia admin user..." + echo "" + prompt_user "Enter admin username" "admin" + read -p "> " ADMIN_USER + ADMIN_USER=${ADMIN_USER:-admin} + + while true; do + read -sp "Enter password for $ADMIN_USER: " ADMIN_PASSWORD + echo "" + read -sp "Confirm password: " ADMIN_PASSWORD_CONFIRM + echo "" + + if [ "$ADMIN_PASSWORD" = "$ADMIN_PASSWORD_CONFIRM" ]; then + if [ ${#ADMIN_PASSWORD} -lt 8 ]; then + log_warning "Password should be at least 8 characters long" + continue + fi + break + else + log_warning "Passwords do not match, please try again" + fi + done + + # Generate password hash using Docker + log_info "Generating password hash (this may take 30-60 seconds)..." + log_info "Pulling Authelia image if not already present..." + + # Pull image first to show progress + if ! docker pull authelia/authelia:4.37 2>&1 | grep -E '(Pulling|Downloaded|Already exists|Status)'; then + log_error "Failed to pull Authelia Docker image" + log_info "Please check:" + log_info " 1. Internet connectivity: ping docker.io" + log_info " 2. Docker Hub access: docker search authelia" + log_info " 3. Disk space: df -h" + exit 1 + fi + + echo "" + log_info "Generating password hash..." + + # Generate hash and write DIRECTLY to file + timeout 60 docker run --rm authelia/authelia:4.37 authelia crypto hash generate argon2 --password "$ADMIN_PASSWORD" 2>&1 | \ + grep -oP 'Digest: \K\$argon2.*' > /tmp/authelia_password_hash.tmp + + HASH_EXIT_CODE=$? + + if [ $HASH_EXIT_CODE -eq 124 ]; then + log_error "Password hash generation timed out after 60 seconds" + exit 1 + elif [ $HASH_EXIT_CODE -ne 0 ] || [ ! -s /tmp/authelia_password_hash.tmp ]; then + log_error "Failed to generate password hash" + exit 1 + fi + + chmod 600 /tmp/authelia_password_hash.tmp + log_success "Password hash generated successfully" + + # Read admin email from .env or prompt + ADMIN_EMAIL=$(grep "^ADMIN_EMAIL=" "$REPO_ENV_FILE" | cut -d'=' -f2) + if [ -z "$ADMIN_EMAIL" ] || [ "$ADMIN_EMAIL" = "admin@example.com" ] || [ "$ADMIN_EMAIL" = "your-email@example.com" ]; then + prompt_user "Enter admin email address" + read -p "> " ADMIN_EMAIL + sed -i "s|^ADMIN_EMAIL=.*|ADMIN_EMAIL=${ADMIN_EMAIL}|" "$REPO_ENV_FILE" + fi + + log_success "Admin user configured: $ADMIN_USER" + log_success "Password hash generated and will be applied during deployment" + + # Store credentials + mkdir -p /opt/stacks/.setup-temp + { + echo "ADMIN_USER=$ADMIN_USER" + echo "ADMIN_EMAIL=$ADMIN_EMAIL" + echo "ADMIN_PASSWORD=$ADMIN_PASSWORD" + } > /opt/stacks/.setup-temp/authelia_admin_credentials.tmp + chmod 600 /opt/stacks/.setup-temp/authelia_admin_credentials.tmp + + cp /tmp/authelia_password_hash.tmp /opt/stacks/.setup-temp/authelia_password_hash.tmp + chmod 600 /opt/stacks/.setup-temp/authelia_password_hash.tmp + + # Save to .env file for persistence + log_info "Saving credentials to .env file for persistence..." + sed -i "/^AUTHELIA_ADMIN_USER=/d" "$REPO_ENV_FILE" + sed -i "/^AUTHELIA_ADMIN_EMAIL=/d" "$REPO_ENV_FILE" + sed -i "/^AUTHELIA_ADMIN_PASSWORD=/d" "$REPO_ENV_FILE" + echo "" >> "$REPO_ENV_FILE" + echo "# Authelia Admin Credentials (generated by setup script)" >> "$REPO_ENV_FILE" + echo "AUTHELIA_ADMIN_USER=$ADMIN_USER" >> "$REPO_ENV_FILE" + echo "AUTHELIA_ADMIN_EMAIL=$ADMIN_EMAIL" >> "$REPO_ENV_FILE" + echo "AUTHELIA_ADMIN_PASSWORD=$ADMIN_PASSWORD" >> "$REPO_ENV_FILE" + log_success "Credentials saved to .env file" + + log_info "Credentials saved for deployment script" + STEPS_COMPLETED=$((STEPS_COMPLETED + 1)) + log_progress "Completed: $STEPS_COMPLETED/$STEPS_TOTAL steps" + echo "" +} + +step_8_create_directories() { + log_info "Step 8/$STEPS_TOTAL: Creating directory structure..." + mkdir -p /opt/stacks + mkdir -p /opt/dockge/data + mkdir -p /mnt/media/{movies,tv,music,books,photos} + mkdir -p /mnt/downloads/{complete,incomplete} + mkdir -p /mnt/backups + mkdir -p /mnt/surveillance + mkdir -p /mnt/git + + # Set ownership + chown -R "$ACTUAL_USER:$ACTUAL_USER" /opt/stacks + chown -R "$ACTUAL_USER:$ACTUAL_USER" /opt/dockge + chown -R "$ACTUAL_USER:$ACTUAL_USER" /mnt/media + chown -R "$ACTUAL_USER:$ACTUAL_USER" /mnt/downloads + chown -R "$ACTUAL_USER:$ACTUAL_USER" /mnt/backups + chown -R "$ACTUAL_USER:$ACTUAL_USER" /mnt/surveillance + chown -R "$ACTUAL_USER:$ACTUAL_USER" /mnt/git + + log_success "Directory structure created" + STEPS_COMPLETED=$((STEPS_COMPLETED + 1)) + log_progress "Completed: $STEPS_COMPLETED/$STEPS_TOTAL steps" + echo "" +} + +step_9_create_networks() { + log_info "Step 9/$STEPS_TOTAL: Creating Docker networks..." + su - "$ACTUAL_USER" -c "docker network create homelab-network 2>/dev/null || true" + su - "$ACTUAL_USER" -c "docker network create traefik-network 2>/dev/null || true" + su - "$ACTUAL_USER" -c "docker network create media-network 2>/dev/null || true" + su - "$ACTUAL_USER" -c "docker network create dockerproxy-network 2>/dev/null || true" + log_success "Docker networks created" + STEPS_COMPLETED=$((STEPS_COMPLETED + 1)) + log_progress "Completed: $STEPS_COMPLETED/$STEPS_TOTAL steps (core setup)" + echo "" +} + +step_10_nvidia_drivers() { + log_info "Step 10/$STEPS_TOTAL (Optional): Checking for NVIDIA GPU..." + + # Detect NVIDIA GPU + if lspci | grep -i nvidia > /dev/null; then + log_info "NVIDIA GPU detected:" + GPU_INFO=$(lspci | grep -i nvidia) + echo "$GPU_INFO" + echo "" + + # Check if NVIDIA drivers are already installed + if nvidia-smi &> /dev/null; then + log_warning "NVIDIA drivers are already installed" + nvidia-smi + NVIDIA_INSTALLED=true + NVIDIA_REBOOT_NEEDED=false + else + log_warning "NVIDIA GPU detected but drivers not installed" + echo "" + + if confirm "Install NVIDIA drivers now?"; then + install_nvidia_drivers "$GPU_INFO" + else + log_info "Skipping NVIDIA driver installation" + log_info "To install later, visit: https://www.nvidia.com/Download/index.aspx" + NVIDIA_INSTALLED=false + NVIDIA_REBOOT_NEEDED=false + fi + fi + echo "" + else + log_info "No NVIDIA GPU detected, skipping driver installation" + NVIDIA_REBOOT_NEEDED=false echo "" - log_info "The script is designed to be idempotent (safe to re-run)" fi } -trap cleanup_on_error EXIT - -log_info "Setting up AI-Homelab for user: $ACTUAL_USER" -echo "" - -# Step 0: Pre-flight validation -log_info "Step 0/10: Running pre-flight checks..." - -# Check internet connectivity -if ! ping -c 1 -W 2 8.8.8.8 &> /dev/null && ! ping -c 1 -W 2 1.1.1.1 &> /dev/null; then - log_error "No internet connectivity detected" - log_info "Internet access is required for:" - log_info " - Installing packages" - log_info " - Downloading Docker images" - log_info " - Accessing Docker Hub" - exit 1 -fi - -# Check disk space (require at least 5GB free) -AVAILABLE_SPACE=$(df / | tail -1 | awk '{print $4}') -REQUIRED_SPACE=5000000 # 5GB in KB -if [ "$AVAILABLE_SPACE" -lt "$REQUIRED_SPACE" ]; then - log_error "Insufficient disk space on root partition" - log_info "Available: $(($AVAILABLE_SPACE / 1024 / 1024))GB" - log_info "Required: $(($REQUIRED_SPACE / 1024 / 1024))GB" - exit 1 -fi - -log_success "Pre-flight checks passed" -log_info "Internet: Connected" -log_info "Disk space: $(($AVAILABLE_SPACE / 1024 / 1024))GB available" -echo "" - -# Step 1: System Update -log_info "Step 1/10: Updating system packages..." -apt-get update && apt-get upgrade -y -log_success "System updated successfully" -echo "" - -# Step 2: Install Required Packages -log_info "Step 2/10: Installing required packages..." -apt-get install -y \ - apt-transport-https \ - ca-certificates \ - curl \ - gnupg \ - lsb-release \ - software-properties-common \ - git \ - openssh-server \ - sudo \ - pciutils \ - net-tools \ - ufw - -log_success "Required packages installed" -echo "" - -# Step 3: Install Docker -log_info "Step 3/10: Installing Docker..." -if command -v docker &> /dev/null && docker --version &> /dev/null; then - log_warning "Docker is already installed ($(docker --version))" -else - # Add Docker's official GPG key - install -m 0755 -d /etc/apt/keyrings - curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc - chmod a+r /etc/apt/keyrings/docker.asc - - # Add the repository to Apt sources - echo \ - "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \ - $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ - tee /etc/apt/sources.list.d/docker.list > /dev/null - - # Update and install Docker - apt-get update - apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin - - log_success "Docker installed successfully ($(docker --version))" -fi -echo "" - -# Step 4: Configure User Groups -log_info "Step 4/10: Configuring user groups..." - -# Add user to sudo group if not already -if groups "$ACTUAL_USER" | grep -q '\bsudo\b'; then - log_warning "User $ACTUAL_USER is already in sudo group" -else - usermod -aG sudo "$ACTUAL_USER" - log_success "User $ACTUAL_USER added to sudo group" -fi - -# Add user to docker group -if groups "$ACTUAL_USER" | grep -q '\bdocker\b'; then - log_warning "User $ACTUAL_USER is already in docker group" -else - usermod -aG docker "$ACTUAL_USER" - log_success "User $ACTUAL_USER added to docker group" -fi -echo "" - -# Step 5: Configure Firewall -log_info "Step 5/10: Configuring firewall..." -# Enable UFW if not already enabled -if ufw status | grep -q "Status: active"; then - log_warning "Firewall is already active" -else - ufw --force enable - log_success "Firewall enabled" -fi - -# Allow SSH if not already allowed -if ufw status | grep -q "22/tcp"; then - log_warning "SSH port 22 is already allowed" -else - ufw allow ssh - log_success "SSH port allowed in firewall" -fi - -# Allow HTTP/HTTPS for web services -ufw allow 80/tcp -ufw allow 443/tcp -log_success "HTTP/HTTPS ports allowed in firewall" -echo "" - -# Step 6: Configure SSH -log_info "Step 6/10: Configuring SSH server..." -systemctl enable ssh -systemctl start ssh - -# Check if SSH is running -if systemctl is-active --quiet ssh; then - SSH_PORT=$(grep "^Port" /etc/ssh/sshd_config | awk '{print $2}') - SSH_PORT=${SSH_PORT:-22} - log_success "SSH server is running on port $SSH_PORT" -else - log_warning "SSH server failed to start, check configuration" -fi -echo "" -# Step 7: Generate Authelia Secrets -log_info "Step 7/10: Generating Authelia authentication secrets..." -echo "" - -# Validate Docker is available for password hash generation -if ! docker info &> /dev/null 2>&1; then - log_error "Docker is not available for password hash generation" - log_info "Docker must be running to generate Authelia password hashes." - log_info "Please ensure:" - log_info " 1. Docker daemon is started: sudo systemctl start docker" - log_info " 2. User can access Docker: docker ps" - log_info " 3. Log out and log back in if recently added to docker group" +show_final_summary() { + echo "" + echo "==========================================" + log_success "AI-Homelab setup completed successfully!" + log_progress "All $STEPS_TOTAL core steps completed!" + echo "==========================================" echo "" - log_info "After fixing, re-run: sudo ./setup-homelab.sh" - exit 1 -fi -log_success "Docker is available for password operations" -echo "" + if [ "${NVIDIA_REBOOT_NEEDED:-false}" = true ]; then + log_warning "⚠️ REBOOT REQUIRED FOR NVIDIA DRIVERS ⚠️" + echo "" + log_info "NVIDIA drivers were installed and require a system reboot." + log_info "Please reboot before running the deployment script." + echo "" + echo " After reboot, verify drivers with: nvidia-smi" + echo "" + echo "==========================================" + echo "" + fi -# Function to generate a secure random secret + log_info "Next steps:" + echo "" + if [ "${NVIDIA_REBOOT_NEEDED:-false}" = true ]; then + echo " 1. REBOOT YOUR SYSTEM for NVIDIA drivers to load" + echo " Run: sudo reboot" + echo "" + echo " 2. After reboot, deploy your homelab services" + else + echo " 1. Deploy your homelab services" + fi + echo "" + echo "==========================================" + echo "" + log_info "Setup complete!" + echo "" + + # Instructions for deployment + if [ "${NVIDIA_REBOOT_NEEDED:-false}" = true ]; then + log_info "Please reboot your system for NVIDIA drivers, then run:" + else + log_info "Next step - deploy your homelab services:" + fi + echo "" + echo " cd ~/AI-Homelab" + echo " sudo ./scripts/deploy-homelab.sh" + echo "" +} + +# Helper function to generate secrets generate_secret() { openssl rand -hex 64 } -# Check if .env file exists in the repo -ACTUAL_USER_HOME=$(eval echo ~$ACTUAL_USER) -REPO_ENV_FILE="$ACTUAL_USER_HOME/AI-Homelab/.env" -if [ ! -f "$REPO_ENV_FILE" ]; then - log_error ".env file not found at $REPO_ENV_FILE" - log_info "Please create .env file from .env.example first" - exit 1 -fi - -# Check if secrets are already set (not placeholder values) -CURRENT_JWT=$(grep "^AUTHELIA_JWT_SECRET=" "$REPO_ENV_FILE" | cut -d'=' -f2) -if [ -n "$CURRENT_JWT" ] && [ "$CURRENT_JWT" != "your-jwt-secret-here" ] && [ ${#CURRENT_JWT} -ge 64 ]; then - log_warning "Authelia secrets appear to already be set in .env" - read -p "Do you want to regenerate them? (y/N): " -n 1 -r - echo "" - if [[ ! $REPLY =~ ^[Yy]$ ]]; then - log_info "Keeping existing secrets" - else - # Generate new secrets - log_info "Generating new JWT secret..." - JWT_SECRET=$(generate_secret) - log_info "Generating new session secret..." - SESSION_SECRET=$(generate_secret) - log_info "Generating new storage encryption key..." - ENCRYPTION_KEY=$(generate_secret) - - # Update .env file - sed -i "s|^AUTHELIA_JWT_SECRET=.*|AUTHELIA_JWT_SECRET=${JWT_SECRET}|" "$REPO_ENV_FILE" - sed -i "s|^AUTHELIA_SESSION_SECRET=.*|AUTHELIA_SESSION_SECRET=${SESSION_SECRET}|" "$REPO_ENV_FILE" - sed -i "s|^AUTHELIA_STORAGE_ENCRYPTION_KEY=.*|AUTHELIA_STORAGE_ENCRYPTION_KEY=${ENCRYPTION_KEY}|" "$REPO_ENV_FILE" - - log_success "New secrets generated and saved to .env" - fi -else - # Generate secrets for first time +# Helper function to generate new Authelia secrets +generate_new_secrets() { log_info "Generating new JWT secret..." JWT_SECRET=$(generate_secret) log_info "Generating new session secret..." @@ -269,249 +612,84 @@ else sed -i "s|^AUTHELIA_SESSION_SECRET=.*|AUTHELIA_SESSION_SECRET=${SESSION_SECRET}|" "$REPO_ENV_FILE" sed -i "s|^AUTHELIA_STORAGE_ENCRYPTION_KEY=.*|AUTHELIA_STORAGE_ENCRYPTION_KEY=${ENCRYPTION_KEY}|" "$REPO_ENV_FILE" - log_success "Secrets generated and saved to .env" -fi + log_success "New secrets generated and saved to .env" +} -# Prompt for admin password -echo "" -log_info "Setting up Authelia admin user..." -echo "" -read -p "Enter admin username (default: admin): " ADMIN_USER -ADMIN_USER=${ADMIN_USER:-admin} - -while true; do - read -sp "Enter password for $ADMIN_USER: " ADMIN_PASSWORD - echo "" - read -sp "Confirm password: " ADMIN_PASSWORD_CONFIRM +# NVIDIA Driver Installation Function +install_nvidia_drivers() { + local GPU_INFO="$1" + + log_info "Installing NVIDIA drivers using official installer..." echo "" - if [ "$ADMIN_PASSWORD" = "$ADMIN_PASSWORD_CONFIRM" ]; then - if [ ${#ADMIN_PASSWORD} -lt 8 ]; then - log_warning "Password should be at least 8 characters long" - continue - fi - break - else - log_warning "Passwords do not match, please try again" - fi -done - -# Generate password hash using Docker -log_info "Generating password hash (this may take 30-60 seconds)..." -log_info "Pulling Authelia image if not already present..." - -# Pull image first to show progress -if ! docker pull authelia/authelia:4.37 2>&1 | grep -E '(Pulling|Downloaded|Already exists|Status)'; then - log_error "Failed to pull Authelia Docker image" - log_info "Please check:" - log_info " 1. Internet connectivity: ping docker.io" - log_info " 2. Docker Hub access: docker search authelia" - log_info " 3. Disk space: df -h" - exit 1 -fi - -echo "" -log_info "Generating password hash..." - -# Generate hash and write DIRECTLY to file to avoid bash variable expansion of $ characters -# The argon2 hash contains multiple $ characters that bash would try to expand as variables -timeout 60 docker run --rm authelia/authelia:4.37 authelia crypto hash generate argon2 --password "$ADMIN_PASSWORD" 2>&1 | \ - grep -oP 'Digest: \K\$argon2.*' > /tmp/authelia_password_hash.tmp - -HASH_EXIT_CODE=$? - -if [ $HASH_EXIT_CODE -eq 124 ]; then - log_error "Password hash generation timed out after 60 seconds" - log_info "This is unusual. Please check:" - log_info " 1. System resources: top or htop" - log_info " 2. Docker status: docker ps" - log_info " 3. Try manually: docker run --rm authelia/authelia:4.37 authelia crypto hash generate argon2" - exit 1 -elif [ $HASH_EXIT_CODE -ne 0 ] || [ ! -s /tmp/authelia_password_hash.tmp ]; then - log_error "Failed to generate password hash" - log_info "You can generate the hash manually after setup:" - log_info " docker run --rm authelia/authelia:4.37 authelia crypto hash generate argon2" - log_info " Then edit: /opt/stacks/core/authelia/users_database.yml" - exit 1 -fi - -chmod 600 /tmp/authelia_password_hash.tmp -log_success "Password hash generated successfully" - -# Read admin email from .env or prompt -ADMIN_EMAIL=$(grep "^ADMIN_EMAIL=" "$REPO_ENV_FILE" | cut -d'=' -f2) -if [ -z "$ADMIN_EMAIL" ] || [ "$ADMIN_EMAIL" = "admin@example.com" ]; then - read -p "Enter admin email address: " ADMIN_EMAIL - sed -i "s|^ADMIN_EMAIL=.*|ADMIN_EMAIL=${ADMIN_EMAIL}|" "$REPO_ENV_FILE" -fi - -log_success "Admin user configured: $ADMIN_USER" -log_success "Password hash generated and will be applied during deployment" - -# Store the admin credentials for the deployment script -# Password hash is already in /tmp/authelia_password_hash.tmp (written directly from Docker) -# This avoids bash variable expansion issues with $ characters in argon2 hashes -# Store in /opt/stacks/ which is accessible across user contexts -mkdir -p /opt/stacks/.setup-temp -{ - echo "ADMIN_USER=$ADMIN_USER" - echo "ADMIN_EMAIL=$ADMIN_EMAIL" - echo "ADMIN_PASSWORD=$ADMIN_PASSWORD" -} > /opt/stacks/.setup-temp/authelia_admin_credentials.tmp -chmod 600 /opt/stacks/.setup-temp/authelia_admin_credentials.tmp - -# Copy password hash to persistent location -cp /tmp/authelia_password_hash.tmp /opt/stacks/.setup-temp/authelia_password_hash.tmp -chmod 600 /opt/stacks/.setup-temp/authelia_password_hash.tmp - -# Also save to .env file for persistence across reboots -log_info "Saving credentials to .env file for persistence..." -sed -i "/^AUTHELIA_ADMIN_USER=/d" "$REPO_ENV_FILE" -sed -i "/^AUTHELIA_ADMIN_EMAIL=/d" "$REPO_ENV_FILE" -sed -i "/^AUTHELIA_ADMIN_PASSWORD=/d" "$REPO_ENV_FILE" -echo "" >> "$REPO_ENV_FILE" -echo "# Authelia Admin Credentials (generated by setup script)" >> "$REPO_ENV_FILE" -echo "AUTHELIA_ADMIN_USER=$ADMIN_USER" >> "$REPO_ENV_FILE" -echo "AUTHELIA_ADMIN_EMAIL=$ADMIN_EMAIL" >> "$REPO_ENV_FILE" -echo "AUTHELIA_ADMIN_PASSWORD=$ADMIN_PASSWORD" >> "$REPO_ENV_FILE" -log_success "Credentials saved to .env file" - -log_info "Credentials saved for deployment script" -echo "" - -# Step 8: Create Directory Structure -log_info "Step 8/10: Creating directory structure..." -mkdir -p /opt/stacks -mkdir -p /opt/dockge/data -mkdir -p /mnt/media/{movies,tv,music,books,photos} -mkdir -p /mnt/downloads/{complete,incomplete} -mkdir -p /mnt/backups -mkdir -p /mnt/surveillance -mkdir -p /mnt/git - -# Set ownership -chown -R "$ACTUAL_USER:$ACTUAL_USER" /opt/stacks -chown -R "$ACTUAL_USER:$ACTUAL_USER" /opt/dockge -chown -R "$ACTUAL_USER:$ACTUAL_USER" /mnt/media -chown -R "$ACTUAL_USER:$ACTUAL_USER" /mnt/downloads -chown -R "$ACTUAL_USER:$ACTUAL_USER" /mnt/backups -chown -R "$ACTUAL_USER:$ACTUAL_USER" /mnt/surveillance -chown -R "$ACTUAL_USER:$ACTUAL_USER" /mnt/git - - -log_success "Directory structure created" -echo "" - -# Step 9: Create Docker Networks -log_info "Step 9/10: Creating Docker networks..." -su - "$ACTUAL_USER" -c "docker network create homelab-network 2>/dev/null || true" -su - "$ACTUAL_USER" -c "docker network create traefik-network 2>/dev/null || true" -su - "$ACTUAL_USER" -c "docker network create media-network 2>/dev/null || true" -su - "$ACTUAL_USER" -c "docker network create dockerproxy-network 2>/dev/null || true" -log_success "Docker networks created" -echo "" - -# Step 10: Optional - Detect and Install NVIDIA Drivers -log_info "Step 10/10 (Optional): Checking for NVIDIA GPU..." - -# Detect NVIDIA GPU -if lspci | grep -i nvidia > /dev/null; then - log_info "NVIDIA GPU detected:" - GPU_INFO=$(lspci | grep -i nvidia) - echo "$GPU_INFO" - echo "" + # Extract GPU model for driver selection + GPU_MODEL=$(echo "$GPU_INFO" | grep -oP 'NVIDIA.*' | head -1) + log_info "GPU Model: $GPU_MODEL" - # Check if NVIDIA drivers are already installed - if nvidia-smi &> /dev/null; then - log_warning "NVIDIA drivers are already installed" - nvidia-smi - NVIDIA_INSTALLED=true - NVIDIA_REBOOT_NEEDED=false - else - log_warning "NVIDIA GPU detected but drivers not installed" - echo "" - read -p "Do you want to install NVIDIA drivers now? (y/N): " -n 1 -r - echo "" - - if [[ $REPLY =~ ^[Yy]$ ]]; then - log_info "Installing NVIDIA drivers using official installer..." - echo "" - - # Extract GPU model for driver selection - GPU_MODEL=$(echo "$GPU_INFO" | grep -oP 'NVIDIA.*' | head -1) - log_info "GPU Model: $GPU_MODEL" - - # Determine recommended driver version - log_info "Determining recommended driver version..." - - # Install prerequisites for NVIDIA installer - log_info "Installing build prerequisites..." - apt-get install -y build-essential linux-headers-$(uname -r) pkg-config libglvnd-dev 2>&1 | tee /tmp/nvidia-prereq.log - - # Disable nouveau driver - log_info "Disabling nouveau driver..." - if [ ! -f /etc/modprobe.d/blacklist-nouveau.conf ]; then - cat > /etc/modprobe.d/blacklist-nouveau.conf << EOF + # Determine recommended driver version + log_info "Determining recommended driver version..." + + # Install prerequisites for NVIDIA installer + log_info "Installing build prerequisites..." + apt-get install -y build-essential linux-headers-$(uname -r) pkg-config libglvnd-dev 2>&1 | tee /tmp/nvidia-prereq.log + + # Disable nouveau driver + log_info "Disabling nouveau driver..." + if [ ! -f /etc/modprobe.d/blacklist-nouveau.conf ]; then + cat > /etc/modprobe.d/blacklist-nouveau.conf << EOF blacklist nouveau options nouveau modeset=0 EOF - update-initramfs -u - log_success "Nouveau driver blacklisted (reboot required)" - fi - - # Download latest NVIDIA driver - NVIDIA_VERSION="550.127.05" # Latest Production Branch as of script creation - DRIVER_URL="https://us.download.nvidia.com/XFree86/Linux-x86_64/${NVIDIA_VERSION}/NVIDIA-Linux-x86_64-${NVIDIA_VERSION}.run" - DRIVER_FILE="/tmp/NVIDIA-Linux-x86_64-${NVIDIA_VERSION}.run" - - log_info "Downloading NVIDIA driver version ${NVIDIA_VERSION}..." - log_info "URL: $DRIVER_URL" - - if curl -fL -o "$DRIVER_FILE" "$DRIVER_URL" 2>&1 | tee /tmp/nvidia-download.log; then - log_success "Driver downloaded successfully" - chmod +x "$DRIVER_FILE" - - log_info "Running NVIDIA installer (this may take several minutes)..." - log_info "The installer will compile kernel modules and configure the driver." - echo "" - - # Run NVIDIA installer with appropriate flags - if "$DRIVER_FILE" --silent --dkms --no-questions 2>&1 | tee /tmp/nvidia-install.log; then - log_success "NVIDIA drivers installed successfully" - NVIDIA_INSTALLED=true - NVIDIA_REBOOT_NEEDED=true - else - log_error "NVIDIA driver installation failed" - log_info "Installation log saved to: /tmp/nvidia-install.log" - log_info "Common issues:" - log_info " - Secure Boot may need to be disabled in BIOS" - log_info " - You may need to sign the kernel modules for Secure Boot" - log_info " - Try running installer manually: sudo $DRIVER_FILE" - log_info "" - log_info "For latest driver, visit: https://www.nvidia.com/Download/index.aspx" - NVIDIA_INSTALLED=false - NVIDIA_REBOOT_NEEDED=true # Still need reboot for nouveau blacklist - fi - - # Cleanup installer - rm -f "$DRIVER_FILE" - else - log_error "Failed to download NVIDIA driver" - log_info "Download log saved to: /tmp/nvidia-download.log" - log_info "You can download and install manually from:" - log_info " https://www.nvidia.com/Download/index.aspx" - NVIDIA_INSTALLED=false - NVIDIA_REBOOT_NEEDED=false - fi - else - log_info "Skipping NVIDIA driver installation" - log_info "To install later, visit: https://www.nvidia.com/Download/index.aspx" - NVIDIA_INSTALLED=false - NVIDIA_REBOOT_NEEDED=false - fi + update-initramfs -u + log_success "Nouveau driver blacklisted (reboot required)" fi - # Check if NVIDIA Container Toolkit is installed + # Download latest NVIDIA driver + NVIDIA_VERSION="550.127.05" # Latest Production Branch as of script creation + DRIVER_URL="https://us.download.nvidia.com/XFree86/Linux-x86_64/${NVIDIA_VERSION}/NVIDIA-Linux-x86_64-${NVIDIA_VERSION}.run" + DRIVER_FILE="/tmp/NVIDIA-Linux-x86_64-${NVIDIA_VERSION}.run" + + log_info "Downloading NVIDIA driver version ${NVIDIA_VERSION}..." + log_info "URL: $DRIVER_URL" + + if curl -fL -o "$DRIVER_FILE" "$DRIVER_URL" 2>&1 | tee /tmp/nvidia-download.log; then + log_success "Driver downloaded successfully" + chmod +x "$DRIVER_FILE" + + log_info "Running NVIDIA installer (this may take several minutes)..." + log_info "The installer will compile kernel modules and configure the driver." + echo "" + + # Run NVIDIA installer with appropriate flags + if "$DRIVER_FILE" --silent --dkms --no-questions 2>&1 | tee /tmp/nvidia-install.log; then + log_success "NVIDIA drivers installed successfully" + NVIDIA_INSTALLED=true + NVIDIA_REBOOT_NEEDED=true + else + log_error "NVIDIA driver installation failed" + log_info "Installation log saved to: /tmp/nvidia-install.log" + log_info "Common issues:" + log_info " - Secure Boot may need to be disabled in BIOS" + log_info " - You may need to sign the kernel modules for Secure Boot" + log_info " - Try running installer manually: sudo $DRIVER_FILE" + log_info "" + log_info "For latest driver, visit: https://www.nvidia.com/Download/index.aspx" + NVIDIA_INSTALLED=false + NVIDIA_REBOOT_NEEDED=true # Still need reboot for nouveau blacklist + fi + + # Cleanup installer + rm -f "$DRIVER_FILE" + else + log_error "Failed to download NVIDIA driver" + log_info "Download log saved to: /tmp/nvidia-download.log" + log_info "You can download and install manually from:" + log_info " https://www.nvidia.com/Download/index.aspx" + NVIDIA_INSTALLED=false + NVIDIA_REBOOT_NEEDED=false + fi + + # Install NVIDIA Container Toolkit if drivers installed successfully if [ "$NVIDIA_INSTALLED" = true ]; then if command -v nvidia-container-runtime &> /dev/null; then log_warning "NVIDIA Container Toolkit is already installed" @@ -541,60 +719,70 @@ EOF fi fi fi +} + +#========================================== +# MAIN EXECUTION +#========================================== + +# Add trap for cleanup on error +cleanup_on_error() { + local exit_code=$? + if [ $exit_code -ne 0 ]; then + log_error "Script failed with exit code: $exit_code" + echo "" + log_info "Partial setup may have occurred. To resume:" + log_info " 1. Review error messages above" + log_info " 2. Fix the issue if possible" + log_info " 3. Re-run: sudo ./setup-homelab.sh" + echo "" + log_info "The script is designed to be idempotent (safe to re-run)" + fi +} + +trap cleanup_on_error EXIT + +# Validate script is running from correct location +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_DIR="$(dirname "$SCRIPT_DIR")" +if [ ! -f "$REPO_DIR/.env.example" ] || [ ! -d "$REPO_DIR/docker-compose" ]; then + log_error "This script must be run from the AI-Homelab repository" + log_info "Expected location: AI-Homelab/scripts/setup-homelab.sh" + log_info "Current location: $SCRIPT_DIR" echo "" -else - log_info "No NVIDIA GPU detected, skipping driver installation" - NVIDIA_REBOOT_NEEDED=false - echo "" + log_info "Please:" + log_info " 1. Clone the repository: git clone https://github.com/kelinfoxy/AI-Homelab.git" + log_info " 2. Enter the directory: cd AI-Homelab" + log_info " 3. Run: sudo ./scripts/setup-homelab.sh" + exit 1 fi - -# Final Summary -echo "" -echo "==========================================" -log_success "AI-Homelab setup completed successfully!" -echo "==========================================" -echo "" - -if [ "${NVIDIA_REBOOT_NEEDED:-false}" = true ]; then - log_warning "⚠️ REBOOT REQUIRED FOR NVIDIA DRIVERS ⚠️" - echo "" - log_info "NVIDIA drivers were installed and require a system reboot." - log_info "Please reboot before running the deployment script." - echo "" - echo " After reboot, verify drivers with: nvidia-smi" - echo "" - echo "==========================================" - echo "" +# If running as root (not via sudo), handle sudo grant and exit +if [ "$ACTUAL_USER" = "root" ]; then + handle_root_user fi -log_info "Next steps:" -echo "" -echo " 1. Log out and log back in for group changes to take effect" -echo " (or run: newgrp docker)" -echo "" -if [ "${NVIDIA_REBOOT_NEEDED:-false}" = true ]; then -echo " 2. REBOOT YOUR SYSTEM for NVIDIA drivers to load" -echo " Run: sudo reboot" -echo "" -echo " 3. After reboot, run the deployment script to deploy your homelab" -else -echo " 2. Run the deployment script to deploy your homelab" +log_info "Setting up AI-Homelab for user: $ACTUAL_USER" +if [ "$AUTO_YES" = true ]; then + log_info "Running in automated mode (--yes flag enabled)" fi echo "" -echo "==========================================" -echo "" -log_info "Setup complete!" -echo "" -# Instructions for deployment -if [ "${NVIDIA_REBOOT_NEEDED:-false}" = true ]; then - log_info "Please reboot your system for NVIDIA drivers, then run:" -else - log_info "Next step - deploy your homelab services:" -fi -echo "" -echo " cd ~/AI-Homelab" -echo " sudo ./scripts/deploy-homelab.sh" -echo "" +# Progress tracking +STEPS_TOTAL=9 +STEPS_COMPLETED=0 + +# Execute setup steps in order +step_0_preflight_checks +step_1_update_system +step_2_install_packages +step_3_install_docker +step_4_configure_user_groups +step_5_configure_firewall +step_6_configure_ssh +step_7_generate_authelia_secrets +step_8_create_directories +step_9_create_networks +step_10_nvidia_drivers +show_final_summary