Refactor scripts for improved maintainability

- setup-homelab.sh: Fixed syntax errors, placeholder detection, and hardcoded paths
- deploy-homelab.sh: Refactored from inline code to function-based structure
- Both scripts now use consistent function organization for better readability
- Enhanced credential handling and error checking
- All scripts validated for syntax correctness
This commit is contained in:
2026-01-14 18:10:23 -05:00
parent 650700ed0a
commit 258e8eec94
2 changed files with 1158 additions and 924 deletions

View File

@@ -30,49 +30,33 @@ log_error() {
echo -e "${RED}[ERROR]${NC} $1" echo -e "${RED}[ERROR]${NC} $1"
} }
# Check if running as root #==========================================
if [ "$EUID" -ne 0 ]; then # VALIDATION FUNCTIONS
log_error "Please run as root (use: sudo ./deploy-homelab.sh)" #==========================================
exit 1
fi
# Get the actual user who invoked sudo validate_prerequisites() {
ACTUAL_USER="${SUDO_USER:-$USER}" # Check if .env file exists
if [ "$ACTUAL_USER" = "root" ]; then if [ ! -f "$REPO_DIR/.env" ]; 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_error ".env file not found!"
log_info "Please create and configure your .env file first:" log_info "Please create and configure your .env file first:"
echo " cd $REPO_DIR" echo " cd $REPO_DIR"
echo " cp .env.example .env" echo " cp .env.example .env"
echo " nano .env" echo " nano .env"
exit 1 exit 1
fi fi
# Check if Docker is installed and running # Check if Docker is installed and running
log_info "Validating Docker installation..." log_info "Validating Docker installation..."
if ! command -v docker &> /dev/null; then if ! command -v docker &> /dev/null; then
log_error "Docker is not installed" log_error "Docker is not installed"
log_info "Please run the setup script first:" log_info "Please run the setup script first:"
log_info " cd ~/AI-Homelab/scripts" log_info " cd $REPO_DIR"
log_info " sudo ./setup-homelab.sh" log_info " sudo ./scripts/setup-homelab.sh"
exit 1 exit 1
fi fi
if ! docker info &> /dev/null 2>&1; then if ! docker info &> /dev/null 2>&1; then
log_error "Docker daemon is not running or not accessible" log_error "Docker daemon is not running or not accessible"
echo "" echo ""
log_info "Troubleshooting steps:" log_info "Troubleshooting steps:"
@@ -85,104 +69,53 @@ if ! docker info &> /dev/null 2>&1; then
log_info "Current user: $ACTUAL_USER" log_info "Current user: $ACTUAL_USER"
log_info "Docker group membership: $(groups $ACTUAL_USER | grep -o docker || echo 'NOT IN DOCKER GROUP')" log_info "Docker group membership: $(groups $ACTUAL_USER | grep -o docker || echo 'NOT IN DOCKER GROUP')"
exit 1 exit 1
fi fi
log_success "Docker is available and running" log_success "Docker is available and running"
log_info "Docker version: $(docker --version | cut -d' ' -f3 | tr -d ',')" log_info "Docker version: $(docker --version | cut -d' ' -f3 | tr -d ',')"
echo "" echo ""
# Load environment variables for domain check # Load environment variables for domain check
source "$REPO_DIR/.env" source "$REPO_DIR/.env"
if [ -z "$DOMAIN" ]; then if [ -z "$DOMAIN" ]; then
log_error "DOMAIN is not set in .env file" log_error "DOMAIN is not set in .env file"
log_info "Please edit .env and set your DuckDNS domain" log_info "Please edit .env and set your DuckDNS domain"
exit 1 exit 1
fi fi
log_info "Using domain: $DOMAIN" log_info "Using domain: $DOMAIN"
echo "" echo ""
}
# Step 1: Create required directories #==========================================
log_info "Step 1/5: Creating required directories..." # DEPLOYMENT STEP FUNCTIONS
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) step_1_create_directories() {
log_info "Step 2/5: Creating Docker networks..." log_info "Step 1/7: Creating required directories..."
docker network create homelab-network 2>/dev/null && log_success "Created homelab-network" || log_info "homelab-network already exists" mkdir -p /opt/stacks/core
docker network create traefik-network 2>/dev/null && log_success "Created traefik-network" || log_info "traefik-network already exists" mkdir -p /opt/stacks/infrastructure
docker network create dockerproxy-network 2>/dev/null && log_success "Created dockerproxy-network" || log_info "dockerproxy-network already exists" mkdir -p /opt/dockge/data
docker network create media-network 2>/dev/null && log_success "Created media-network" || log_info "media-network already exists" log_success "Directories created"
echo "" echo ""
}
# Step 3: Deploy core infrastructure (DuckDNS, Traefik, Authelia, Gluetun) step_2_create_networks() {
log_info "Step 3/5: Deploying core infrastructure stack..." log_info "Step 2/7: Creating Docker networks..."
log_info " - DuckDNS (Dynamic DNS)" docker network create homelab-network 2>/dev/null && log_success "Created homelab-network" || log_info "homelab-network already exists"
log_info " - Traefik (Reverse Proxy with SSL)" docker network create traefik-network 2>/dev/null && log_success "Created traefik-network" || log_info "traefik-network already exists"
log_info " - Authelia (Single Sign-On)" docker network create dockerproxy-network 2>/dev/null && log_success "Created dockerproxy-network" || log_info "dockerproxy-network already exists"
log_info " - Gluetun (VPN Client)" docker network create media-network 2>/dev/null && log_success "Created media-network" || log_info "media-network already exists"
echo "" echo ""
}
# Copy core stack files configure_authelia() {
log_info "Preparing core stack configuration files..." # 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
# 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..." log_info "Loading Authelia admin credentials from setup temp files..."
source /opt/stacks/.setup-temp/authelia_admin_credentials.tmp source /opt/stacks/.setup-temp/authelia_admin_credentials.tmp
elif [ -n "${AUTHELIA_ADMIN_USER}" ] && [ -n "${AUTHELIA_ADMIN_EMAIL}" ] && [ -n "${AUTHELIA_ADMIN_PASSWORD}" ]; then elif [ -n "${AUTHELIA_ADMIN_USER}" ] && [ -n "${AUTHELIA_ADMIN_EMAIL}" ] && [ -n "${AUTHELIA_ADMIN_PASSWORD}" ]; then
log_info "Loading Authelia admin credentials from .env file..." log_info "Loading Authelia admin credentials from .env file..."
ADMIN_USER="${AUTHELIA_ADMIN_USER}" ADMIN_USER="${AUTHELIA_ADMIN_USER}"
ADMIN_EMAIL="${AUTHELIA_ADMIN_EMAIL}" ADMIN_EMAIL="${AUTHELIA_ADMIN_EMAIL}"
@@ -211,9 +144,9 @@ elif [ -n "${AUTHELIA_ADMIN_USER}" ] && [ -n "${AUTHELIA_ADMIN_EMAIL}" ] && [ -n
ADMIN_USER="" ADMIN_USER=""
ADMIN_EMAIL="" ADMIN_EMAIL=""
fi fi
fi fi
if [ -f /opt/stacks/.setup-temp/authelia_admin_credentials.tmp ] && [ -f /opt/stacks/.setup-temp/authelia_password_hash.tmp ]; then 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 source /opt/stacks/.setup-temp/authelia_admin_credentials.tmp
if [ -n "$ADMIN_USER" ] && [ -n "$ADMIN_EMAIL" ]; then if [ -n "$ADMIN_USER" ] && [ -n "$ADMIN_EMAIL" ]; then
@@ -283,6 +216,15 @@ PYTHON_EOF
chmod 600 /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 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" 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 "" echo ""
# Clean up credentials files from setup script # Clean up credentials files from setup script
@@ -294,76 +236,140 @@ PYTHON_EOF
log_warning "Incomplete credentials from setup script" log_warning "Incomplete credentials from setup script"
log_info "Using template users_database.yml - please configure manually" log_info "Using template users_database.yml - please configure manually"
fi fi
else else
log_warning "No credentials file found from setup script" log_warning "No credentials file found from setup script"
log_info "Using template users_database.yml from config-templates" log_info "Using template users_database.yml from config-templates"
log_info "Please run setup-homelab.sh first or configure manually" log_info "Please run setup-homelab.sh first or configure manually"
fi fi
}
# Clean up old Authelia database if encryption key changed step_3_deploy_core() {
# This prevents "encryption key does not appear to be valid" errors log_info "Step 3/7: Deploying core infrastructure stack..."
if [ -d "/var/lib/docker/volumes/core_authelia-data/_data" ]; then 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
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..." log_info "Checking for existing Authelia database..."
# Check if database exists and might have encryption key mismatch # Check if database exists and might have encryption key mismatch
if [ -f "/var/lib/docker/volumes/core_authelia-data/_data/db.sqlite3" ]; then if [ -f "/var/lib/docker/volumes/core_authelia-data/_data/db.sqlite3" ]; then
log_warning "Existing Authelia database found from previous deployment" 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" log_info "If deployment fails with encryption key errors, run: sudo ./scripts/reset-test-environment.sh"
fi fi
fi fi
# Deploy core stack # Deploy core stack
cd /opt/stacks/core cd /opt/stacks/core
docker compose up -d docker compose up -d
log_success "Core infrastructure deployed" log_success "Core infrastructure deployed"
echo "" echo ""
# Wait for Traefik to be ready # Wait for Traefik to be ready
log_info "Waiting for Traefik to initialize..." log_info "Waiting for Traefik to initialize..."
sleep 10 sleep 10
# Check if Traefik is healthy # Check if Traefik is healthy
if docker ps | grep -q "traefik.*Up"; then if docker ps | grep -q "traefik.*Up"; then
log_success "Traefik is running" log_success "Traefik is running"
else else
log_warning "Traefik container check inconclusive, continuing..." log_warning "Traefik container check inconclusive, continuing..."
fi fi
echo "" echo ""
}
# Step 4: Deploy infrastructure stack (Dockge and monitoring tools) step_4_deploy_infrastructure() {
log_info "Step 4/6: Deploying infrastructure stack..." log_info "Step 4/7: Deploying infrastructure stack..."
log_info " - Dockge (Docker Compose Manager)" log_info " - Dockge (Docker Compose Manager)"
log_info " - Pi-hole (DNS Ad Blocker)" log_info " - Pi-hole (DNS Ad Blocker)"
log_info " - Dozzle (Log Viewer)" log_info " - Dozzle (Log Viewer)"
log_info " - Glances (System Monitor)" log_info " - Glances (System Monitor)"
log_info " - Docker Proxy (Security)" log_info " - Docker Proxy (Security)"
log_info " - Watchtower (Automatic Updates)" log_info " - Watchtower (Automatic Updates)"
echo "" echo ""
# Copy infrastructure stack # Copy infrastructure stack
cp "$REPO_DIR/docker-compose/infrastructure.yml" /opt/stacks/infrastructure/docker-compose.yml cp "$REPO_DIR/docker-compose/infrastructure.yml" /opt/stacks/infrastructure/docker-compose.yml
cp "$REPO_DIR/.env" /opt/stacks/infrastructure/.env cp "$REPO_DIR/.env" /opt/stacks/infrastructure/.env
# Deploy infrastructure stack # Deploy infrastructure stack
cd /opt/stacks/infrastructure cd /opt/stacks/infrastructure
docker compose up -d docker compose up -d
log_success "Infrastructure stack deployed" log_success "Infrastructure stack deployed"
echo "" echo ""
}
# Step 5: Deploy dashboards stack (Homepage and Homarr) step_5_deploy_dashboards() {
log_info "Step 5/6: Deploying dashboards stack..." log_info "Step 5/7: Deploying dashboards stack..."
log_info " - Homepage (AI-configurable Dashboard)" log_info " - Homepage (AI-configurable Dashboard)"
log_info " - Homarr (Modern Dashboard)" log_info " - Homarr (Modern Dashboard)"
echo "" echo ""
# Copy dashboards stack # Copy dashboards stack
mkdir -p /opt/stacks/dashboards mkdir -p /opt/stacks/dashboards
cp "$REPO_DIR/docker-compose/dashboards.yml" /opt/stacks/dashboards/docker-compose.yml cp "$REPO_DIR/docker-compose/dashboards.yml" /opt/stacks/dashboards/docker-compose.yml
cp "$REPO_DIR/.env" /opt/stacks/dashboards/.env cp "$REPO_DIR/.env" /opt/stacks/dashboards/.env
# Copy and configure homepage templates # Copy and configure homepage templates
if [ -d "$REPO_DIR/config-templates/homepage" ]; then if [ -d "$REPO_DIR/config-templates/homepage" ]; then
cp -r "$REPO_DIR/config-templates/homepage" /opt/stacks/dashboards/ cp -r "$REPO_DIR/config-templates/homepage" /opt/stacks/dashboards/
# Replace HOMEPAGE_VAR_DOMAIN with actual domain in all homepage config files # Replace HOMEPAGE_VAR_DOMAIN with actual domain in all homepage config files
@@ -371,36 +377,37 @@ if [ -d "$REPO_DIR/config-templates/homepage" ]; then
find /opt/stacks/dashboards/homepage -type f \( -name "*.yaml" -o -name "*.yml" \) -exec sed -i "s/{{HOMEPAGE_VAR_DOMAIN}}/${DOMAIN}/g" {} \; 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" log_info "Homepage configuration templates copied and configured"
fi fi
# Deploy dashboards stack # Deploy dashboards stack
cd /opt/stacks/dashboards cd /opt/stacks/dashboards
docker compose up -d docker compose up -d
log_success "Dashboards stack deployed" log_success "Dashboards stack deployed"
echo "" echo ""
}
# Step 6: Deploy additional stacks to Dockge (not started) step_6_prepare_additional_stacks() {
log_info "Step 6/7: Preparing additional stacks for Dockge..." log_info "Step 6/7: Preparing additional stacks for Dockge..."
echo "" echo ""
log_info "The following stacks can be deployed through Dockge's web UI:" log_info "The following stacks can be deployed through Dockge's web UI:"
log_info " - media.yml (Jellyfin, Calibre-web, qBittorrent)" log_info " - media.yml (Jellyfin, Calibre-web, qBittorrent)"
log_info " - media-management.yml (Sonarr, Radarr, *arr apps)" log_info " - media-management.yml (Sonarr, Radarr, *arr apps)"
log_info " - homeassistant.yml (Home Assistant and accessories)" log_info " - homeassistant.yml (Home Assistant and accessories)"
log_info " - productivity.yml (Nextcloud, Gitea, wikis)" log_info " - productivity.yml (Nextcloud, Gitea, wikis)"
log_info " - monitoring.yml (Grafana, Prometheus, etc.)" log_info " - monitoring.yml (Grafana, Prometheus, etc.)"
log_info " - utilities.yml (Backups, code editors, etc.)" log_info " - utilities.yml (Backups, code editors, etc.)"
log_info " - alternatives.yml (Portainer, Authentik)" log_info " - alternatives.yml (Portainer, Authentik)"
echo "" echo ""
# Ask user if they want to pre-pull images for additional stacks # 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 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} PULL_IMAGES=${PULL_IMAGES:-n}
# Copy additional stacks to Dockge directory # Copy additional stacks to Dockge directory
ADDITIONAL_STACKS=("media" "media-management" "homeassistant" "productivity" "monitoring" "utilities" "alternatives") ADDITIONAL_STACKS=("media" "media-management" "homeassistant" "productivity" "monitoring" "utilities" "alternatives")
for stack in "${ADDITIONAL_STACKS[@]}"; do for stack in "${ADDITIONAL_STACKS[@]}"; do
mkdir -p "/opt/stacks/$stack" mkdir -p "/opt/stacks/$stack"
cp "$REPO_DIR/docker-compose/${stack}.yml" "/opt/stacks/$stack/docker-compose.yml" cp "$REPO_DIR/docker-compose/${stack}.yml" "/opt/stacks/$stack/docker-compose.yml"
cp "$REPO_DIR/.env" "/opt/stacks/$stack/.env" cp "$REPO_DIR/.env" "/opt/stacks/$stack/.env"
@@ -411,27 +418,28 @@ for stack in "${ADDITIONAL_STACKS[@]}"; do
cd "/opt/stacks/$stack" cd "/opt/stacks/$stack"
docker compose pull 2>&1 | grep -E '(Pulling|Downloaded|Already exists|up to date)' || true docker compose pull 2>&1 | grep -E '(Pulling|Downloaded|Already exists|up to date)' || true
fi fi
done done
log_success "Additional stacks prepared in Dockge" log_success "Additional stacks prepared in Dockge"
log_info "These stacks are NOT started - deploy them via Dockge web UI as needed" log_info "These stacks are NOT started - deploy them via Dockge web UI as needed"
echo "" 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 step_7_wait_for_dockge() {
while [ $WAITED -lt $MAX_WAIT ]; do 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) HTTP_CODE=$(check_dockge)
if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "302" ] || [ "$HTTP_CODE" = "401" ]; then if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "302" ] || [ "$HTTP_CODE" = "401" ]; then
log_success "Dockge web UI is ready!" log_success "Dockge web UI is ready!"
@@ -440,15 +448,15 @@ while [ $WAITED -lt $MAX_WAIT ]; do
echo -n "." echo -n "."
sleep 2 sleep 2
WAITED=$((WAITED + 2)) WAITED=$((WAITED + 2))
done done
echo "" echo ""
echo "" echo ""
if [ $WAITED -ge $MAX_WAIT ]; then if [ $WAITED -ge $MAX_WAIT ]; then
log_warning "Dockge did not respond within ${MAX_WAIT} seconds" log_warning "Dockge did not respond within ${MAX_WAIT} seconds"
log_info "It may still be starting up. Check manually at: $DOCKGE_URL" log_info "It may still be starting up. Check manually at: $DOCKGE_URL"
else else
# Try to open browser # Try to open browser
log_info "Opening Dockge in your browser..." log_info "Opening Dockge in your browser..."
@@ -468,48 +476,86 @@ else
else else
log_warning "No browser detected. Please manually open: $DOCKGE_URL" log_warning "No browser detected. Please manually open: $DOCKGE_URL"
fi 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 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 ""
echo "=========================================="
log_success "Deployment completed successfully!" # Execute deployment steps
echo "==========================================" validate_prerequisites
echo "" step_1_create_directories
log_info "Access your services:" step_2_create_networks
echo "" step_3_deploy_core
echo " 🚀 Dockge: $DOCKGE_URL" step_4_deploy_infrastructure
echo " 🔒 Authelia: https://auth.${DOMAIN}" step_5_deploy_dashboards
echo " 🔀 Traefik: https://traefik.${DOMAIN}" step_6_prepare_additional_stacks
echo "" step_7_wait_for_dockge
log_info "SSL Certificates:" show_final_summary
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 ""

View File

@@ -1,15 +1,44 @@
#!/bin/bash #!/bin/bash
# AI-Homelab First-Run Setup Script # AI-Homelab First-Run Setup Script
# This script prepares a fresh Debian installation for homelab deployment # 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 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 # Colors for output
RED='\033[0;31m' RED='\033[0;31m'
GREEN='\033[0;32m' GREEN='\033[0;32m'
YELLOW='\033[1;33m' YELLOW='\033[1;33m'
BLUE='\033[0;34m' BLUE='\033[0;34m'
CYAN='\033[0;36m'
MAGENTA='\033[0;35m'
BOLD='\033[1m'
NC='\033[0m' # No Color NC='\033[0m' # No Color
# Log functions # Log functions
@@ -29,76 +58,162 @@ log_error() {
echo -e "${RED}[ERROR]${NC} $1" 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 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 exit 1
fi fi
# Get the actual user who invoked sudo # Get the actual user who invoked sudo
ACTUAL_USER="${SUDO_USER:-$USER}" 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() { # STEP FUNCTIONS
local exit_code=$? #==========================================
if [ $exit_code -ne 0 ]; then
log_error "Script failed with exit code: $exit_code" # Handle root user scenario - grant sudo and exit
handle_root_user() {
log_info "Running as root - checking for non-root users..."
echo "" echo ""
log_info "Partial setup may have occurred. To resume:"
log_info " 1. Review error messages above" # Find non-root users with home directories and UID >= 1000
log_info " 2. Fix the issue if possible" AVAILABLE_USERS=$(awk -F: '$3 >= 1000 && $3 < 65534 && $1 != "nobody" {print $1}' /etc/passwd)
log_info " 3. Re-run: sudo ./setup-homelab.sh"
echo "" if [ -z "$AVAILABLE_USERS" ]; then
log_info "The script is designed to be idempotent (safe to re-run)" log_error "No non-root user found on this system"
log_info "Please create a user first:"
log_info " adduser <username>"
exit 1
fi 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 ""
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
} }
trap cleanup_on_error EXIT step_0_preflight_checks() {
log_info "Step 0/$STEPS_TOTAL: Running pre-flight checks..."
log_progress "Starting setup process"
log_info "Setting up AI-Homelab for user: $ACTUAL_USER" # Check internet connectivity
echo "" if ! ping -c 1 -W 2 8.8.8.8 &> /dev/null && ! ping -c 1 -W 2 1.1.1.1 &> /dev/null; then
# 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_error "No internet connectivity detected"
log_info "Internet access is required for:" log_info "Internet access is required for:"
log_info " - Installing packages" log_info " - Installing packages"
log_info " - Downloading Docker images" log_info " - Downloading Docker images"
log_info " - Accessing Docker Hub" log_info " - Accessing Docker Hub"
exit 1 exit 1
fi fi
# Check disk space (require at least 5GB free) # Check disk space (require at least 5GB free)
AVAILABLE_SPACE=$(df / | tail -1 | awk '{print $4}') AVAILABLE_SPACE=$(df / | tail -1 | awk '{print $4}')
REQUIRED_SPACE=5000000 # 5GB in KB REQUIRED_SPACE=5000000 # 5GB in KB
if [ "$AVAILABLE_SPACE" -lt "$REQUIRED_SPACE" ]; then if [ "$AVAILABLE_SPACE" -lt "$REQUIRED_SPACE" ]; then
log_error "Insufficient disk space on root partition" log_error "Insufficient disk space on root partition"
log_info "Available: $(($AVAILABLE_SPACE / 1024 / 1024))GB" log_info "Available: $(($AVAILABLE_SPACE / 1024 / 1024))GB"
log_info "Required: $(($REQUIRED_SPACE / 1024 / 1024))GB" log_info "Required: $(($REQUIRED_SPACE / 1024 / 1024))GB"
exit 1 exit 1
fi fi
log_success "Pre-flight checks passed" log_success "Pre-flight checks passed"
log_info "Internet: Connected" log_info "Internet: Connected"
log_info "Disk space: $(($AVAILABLE_SPACE / 1024 / 1024))GB available" log_info "Disk space: $(($AVAILABLE_SPACE / 1024 / 1024))GB available"
echo "" STEPS_COMPLETED=$((STEPS_COMPLETED + 1))
log_progress "Completed: $STEPS_COMPLETED/$STEPS_TOTAL steps"
echo ""
}
# Step 1: System Update step_1_update_system() {
log_info "Step 1/10: Updating system packages..." log_info "Step 1/$STEPS_TOTAL: Updating system packages..."
apt-get update && apt-get upgrade -y apt-get update && apt-get upgrade -y
log_success "System updated successfully" log_success "System updated successfully"
echo "" STEPS_COMPLETED=$((STEPS_COMPLETED + 1))
log_progress "Completed: $STEPS_COMPLETED/$STEPS_TOTAL steps"
echo ""
}
# Step 2: Install Required Packages step_2_install_packages() {
log_info "Step 2/10: Installing required packages..." log_info "Step 2/$STEPS_TOTAL: Installing required packages..."
apt-get install -y \ apt-get install -y \
apt-transport-https \ apt-transport-https \
ca-certificates \ ca-certificates \
curl \ curl \
@@ -112,14 +227,17 @@ apt-get install -y \
net-tools \ net-tools \
ufw ufw
log_success "Required packages installed" log_success "Required packages installed"
echo "" STEPS_COMPLETED=$((STEPS_COMPLETED + 1))
log_progress "Completed: $STEPS_COMPLETED/$STEPS_TOTAL steps"
echo ""
}
# Step 3: Install Docker step_3_install_docker() {
log_info "Step 3/10: Installing Docker..." log_info "Step 3/$STEPS_TOTAL: Installing Docker..."
if command -v docker &> /dev/null && docker --version &> /dev/null; then if command -v docker &> /dev/null && docker --version &> /dev/null; then
log_warning "Docker is already installed ($(docker --version))" log_warning "Docker is already installed ($(docker --version))"
else else
# Add Docker's official GPG key # Add Docker's official GPG key
install -m 0755 -d /etc/apt/keyrings install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
@@ -136,73 +254,86 @@ else
apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
log_success "Docker installed successfully ($(docker --version))" log_success "Docker installed successfully ($(docker --version))"
fi fi
echo "" STEPS_COMPLETED=$((STEPS_COMPLETED + 1))
log_progress "Completed: $STEPS_COMPLETED/$STEPS_TOTAL steps"
echo ""
}
# Step 4: Configure User Groups step_4_configure_user_groups() {
log_info "Step 4/10: Configuring user groups..." log_info "Step 4/$STEPS_TOTAL: Configuring user groups..."
# Add user to sudo group if not already # Add user to sudo group if not already
if groups "$ACTUAL_USER" | grep -q '\bsudo\b'; then if groups "$ACTUAL_USER" | grep -q '\bsudo\b'; then
log_warning "User $ACTUAL_USER is already in sudo group" log_warning "User $ACTUAL_USER is already in sudo group"
else else
usermod -aG sudo "$ACTUAL_USER" usermod -aG sudo "$ACTUAL_USER"
log_success "User $ACTUAL_USER added to sudo group" log_success "User $ACTUAL_USER added to sudo group"
fi fi
# Add user to docker group # Add user to docker group
if groups "$ACTUAL_USER" | grep -q '\bdocker\b'; then if groups "$ACTUAL_USER" | grep -q '\bdocker\b'; then
log_warning "User $ACTUAL_USER is already in docker group" log_warning "User $ACTUAL_USER is already in docker group"
else else
usermod -aG docker "$ACTUAL_USER" usermod -aG docker "$ACTUAL_USER"
log_success "User $ACTUAL_USER added to docker group" log_success "User $ACTUAL_USER added to docker group"
fi fi
echo "" STEPS_COMPLETED=$((STEPS_COMPLETED + 1))
log_progress "Completed: $STEPS_COMPLETED/$STEPS_TOTAL steps"
echo ""
}
# Step 5: Configure Firewall step_5_configure_firewall() {
log_info "Step 5/10: Configuring firewall..." log_info "Step 5/$STEPS_TOTAL: Configuring firewall..."
# Enable UFW if not already enabled # Enable UFW if not already enabled
if ufw status | grep -q "Status: active"; then if ufw status | grep -q "Status: active"; then
log_warning "Firewall is already active" log_warning "Firewall is already active"
else else
ufw --force enable ufw --force enable
log_success "Firewall enabled" log_success "Firewall enabled"
fi fi
# Allow SSH if not already allowed # Allow SSH if not already allowed
if ufw status | grep -q "22/tcp"; then if ufw status | grep -q "22/tcp"; then
log_warning "SSH port 22 is already allowed" log_warning "SSH port 22 is already allowed"
else else
ufw allow ssh ufw allow ssh
log_success "SSH port allowed in firewall" log_success "SSH port allowed in firewall"
fi fi
# Allow HTTP/HTTPS for web services # Allow HTTP/HTTPS for web services
ufw allow 80/tcp ufw allow 80/tcp
ufw allow 443/tcp ufw allow 443/tcp
log_success "HTTP/HTTPS ports allowed in firewall" log_success "HTTP/HTTPS ports allowed in firewall"
echo "" STEPS_COMPLETED=$((STEPS_COMPLETED + 1))
log_progress "Completed: $STEPS_COMPLETED/$STEPS_TOTAL steps"
echo ""
}
# Step 6: Configure SSH step_6_configure_ssh() {
log_info "Step 6/10: Configuring SSH server..." log_info "Step 6/$STEPS_TOTAL: Configuring SSH server..."
systemctl enable ssh systemctl enable ssh
systemctl start ssh systemctl start ssh
# Check if SSH is running # Check if SSH is running
if systemctl is-active --quiet ssh; then if systemctl is-active --quiet ssh; then
SSH_PORT=$(grep "^Port" /etc/ssh/sshd_config | awk '{print $2}') SSH_PORT=$(grep "^Port" /etc/ssh/sshd_config | awk '{print $2}')
SSH_PORT=${SSH_PORT:-22} SSH_PORT=${SSH_PORT:-22}
log_success "SSH server is running on port $SSH_PORT" log_success "SSH server is running on port $SSH_PORT"
else else
log_warning "SSH server failed to start, check configuration" log_warning "SSH server failed to start, check configuration"
fi fi
echo "" STEPS_COMPLETED=$((STEPS_COMPLETED + 1))
# Step 7: Generate Authelia Secrets log_progress "Completed: $STEPS_COMPLETED/$STEPS_TOTAL steps"
log_info "Step 7/10: Generating Authelia authentication secrets..." echo ""
echo "" }
# Validate Docker is available for password hash generation step_7_generate_authelia_secrets() {
if ! docker info &> /dev/null 2>&1; then 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_error "Docker is not available for password hash generation"
log_info "Docker must be running to generate Authelia password hashes." log_info "Docker must be running to generate Authelia password hashes."
log_info "Please ensure:" log_info "Please ensure:"
@@ -212,74 +343,43 @@ if ! docker info &> /dev/null 2>&1; then
echo "" echo ""
log_info "After fixing, re-run: sudo ./setup-homelab.sh" log_info "After fixing, re-run: sudo ./setup-homelab.sh"
exit 1 exit 1
fi fi
log_success "Docker is available for password operations" log_success "Docker is available for password operations"
echo "" echo ""
# Function to generate a secure random secret # Check if .env file exists in the repo
generate_secret() { REPO_ENV_FILE="$REPO_DIR/.env"
openssl rand -hex 64 if [ ! -f "$REPO_ENV_FILE" ]; then
}
# 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_error ".env file not found at $REPO_ENV_FILE"
log_info "Please create .env file from .env.example first" log_info "Please create .env file from .env.example first"
exit 1 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 fi
else
# Generate secrets for first time
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 # Check if secrets are already set (not placeholder values)
sed -i "s|^AUTHELIA_JWT_SECRET=.*|AUTHELIA_JWT_SECRET=${JWT_SECRET}|" "$REPO_ENV_FILE" CURRENT_JWT=$(grep "^AUTHELIA_JWT_SECRET=" "$REPO_ENV_FILE" | cut -d'=' -f2)
sed -i "s|^AUTHELIA_SESSION_SECRET=.*|AUTHELIA_SESSION_SECRET=${SESSION_SECRET}|" "$REPO_ENV_FILE" if [ -n "$CURRENT_JWT" ] && [ "$CURRENT_JWT" != "your-jwt-secret-here" ] && [ "$CURRENT_JWT" != "generate-with-openssl-rand-hex-64" ] && [ ${#CURRENT_JWT} -ge 64 ]; then
sed -i "s|^AUTHELIA_STORAGE_ENCRYPTION_KEY=.*|AUTHELIA_STORAGE_ENCRYPTION_KEY=${ENCRYPTION_KEY}|" "$REPO_ENV_FILE" 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
log_success "Secrets generated and saved to .env" # Prompt for admin password
fi echo ""
log_info "Setting up Authelia admin user..."
echo ""
prompt_user "Enter admin username" "admin"
read -p "> " ADMIN_USER
ADMIN_USER=${ADMIN_USER:-admin}
# Prompt for admin password while true; do
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 read -sp "Enter password for $ADMIN_USER: " ADMIN_PASSWORD
echo "" echo ""
read -sp "Confirm password: " ADMIN_PASSWORD_CONFIRM read -sp "Confirm password: " ADMIN_PASSWORD_CONFIRM
@@ -294,128 +394,125 @@ while true; do
else else
log_warning "Passwords do not match, please try again" log_warning "Passwords do not match, please try again"
fi fi
done done
# Generate password hash using Docker # Generate password hash using Docker
log_info "Generating password hash (this may take 30-60 seconds)..." log_info "Generating password hash (this may take 30-60 seconds)..."
log_info "Pulling Authelia image if not already present..." log_info "Pulling Authelia image if not already present..."
# Pull image first to show progress # Pull image first to show progress
if ! docker pull authelia/authelia:4.37 2>&1 | grep -E '(Pulling|Downloaded|Already exists|Status)'; then 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_error "Failed to pull Authelia Docker image"
log_info "Please check:" log_info "Please check:"
log_info " 1. Internet connectivity: ping docker.io" log_info " 1. Internet connectivity: ping docker.io"
log_info " 2. Docker Hub access: docker search authelia" log_info " 2. Docker Hub access: docker search authelia"
log_info " 3. Disk space: df -h" log_info " 3. Disk space: df -h"
exit 1 exit 1
fi fi
echo "" echo ""
log_info "Generating password hash..." log_info "Generating password hash..."
# Generate hash and write DIRECTLY to file to avoid bash variable expansion of $ characters # Generate hash and write DIRECTLY to file
# 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 | \
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 grep -oP 'Digest: \K\$argon2.*' > /tmp/authelia_password_hash.tmp
HASH_EXIT_CODE=$? HASH_EXIT_CODE=$?
if [ $HASH_EXIT_CODE -eq 124 ]; then if [ $HASH_EXIT_CODE -eq 124 ]; then
log_error "Password hash generation timed out after 60 seconds" 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 exit 1
elif [ $HASH_EXIT_CODE -ne 0 ] || [ ! -s /tmp/authelia_password_hash.tmp ]; then elif [ $HASH_EXIT_CODE -ne 0 ] || [ ! -s /tmp/authelia_password_hash.tmp ]; then
log_error "Failed to generate password hash" 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 exit 1
fi fi
chmod 600 /tmp/authelia_password_hash.tmp chmod 600 /tmp/authelia_password_hash.tmp
log_success "Password hash generated successfully" log_success "Password hash generated successfully"
# Read admin email from .env or prompt # Read admin email from .env or prompt
ADMIN_EMAIL=$(grep "^ADMIN_EMAIL=" "$REPO_ENV_FILE" | cut -d'=' -f2) ADMIN_EMAIL=$(grep "^ADMIN_EMAIL=" "$REPO_ENV_FILE" | cut -d'=' -f2)
if [ -z "$ADMIN_EMAIL" ] || [ "$ADMIN_EMAIL" = "admin@example.com" ]; then if [ -z "$ADMIN_EMAIL" ] || [ "$ADMIN_EMAIL" = "admin@example.com" ] || [ "$ADMIN_EMAIL" = "your-email@example.com" ]; then
read -p "Enter admin email address: " ADMIN_EMAIL prompt_user "Enter admin email address"
read -p "> " ADMIN_EMAIL
sed -i "s|^ADMIN_EMAIL=.*|ADMIN_EMAIL=${ADMIN_EMAIL}|" "$REPO_ENV_FILE" sed -i "s|^ADMIN_EMAIL=.*|ADMIN_EMAIL=${ADMIN_EMAIL}|" "$REPO_ENV_FILE"
fi fi
log_success "Admin user configured: $ADMIN_USER" log_success "Admin user configured: $ADMIN_USER"
log_success "Password hash generated and will be applied during deployment" log_success "Password hash generated and will be applied during deployment"
# Store the admin credentials for the deployment script # Store credentials
# Password hash is already in /tmp/authelia_password_hash.tmp (written directly from Docker) mkdir -p /opt/stacks/.setup-temp
# 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_USER=$ADMIN_USER"
echo "ADMIN_EMAIL=$ADMIN_EMAIL" echo "ADMIN_EMAIL=$ADMIN_EMAIL"
echo "ADMIN_PASSWORD=$ADMIN_PASSWORD" echo "ADMIN_PASSWORD=$ADMIN_PASSWORD"
} > /opt/stacks/.setup-temp/authelia_admin_credentials.tmp } > /opt/stacks/.setup-temp/authelia_admin_credentials.tmp
chmod 600 /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
cp /tmp/authelia_password_hash.tmp /opt/stacks/.setup-temp/authelia_password_hash.tmp chmod 600 /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 # Save to .env file for persistence
log_info "Saving credentials 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_USER=/d" "$REPO_ENV_FILE"
sed -i "/^AUTHELIA_ADMIN_EMAIL=/d" "$REPO_ENV_FILE" sed -i "/^AUTHELIA_ADMIN_EMAIL=/d" "$REPO_ENV_FILE"
sed -i "/^AUTHELIA_ADMIN_PASSWORD=/d" "$REPO_ENV_FILE" sed -i "/^AUTHELIA_ADMIN_PASSWORD=/d" "$REPO_ENV_FILE"
echo "" >> "$REPO_ENV_FILE" echo "" >> "$REPO_ENV_FILE"
echo "# Authelia Admin Credentials (generated by setup script)" >> "$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_USER=$ADMIN_USER" >> "$REPO_ENV_FILE"
echo "AUTHELIA_ADMIN_EMAIL=$ADMIN_EMAIL" >> "$REPO_ENV_FILE" echo "AUTHELIA_ADMIN_EMAIL=$ADMIN_EMAIL" >> "$REPO_ENV_FILE"
echo "AUTHELIA_ADMIN_PASSWORD=$ADMIN_PASSWORD" >> "$REPO_ENV_FILE" echo "AUTHELIA_ADMIN_PASSWORD=$ADMIN_PASSWORD" >> "$REPO_ENV_FILE"
log_success "Credentials saved to .env file" log_success "Credentials saved to .env file"
log_info "Credentials saved for deployment script" log_info "Credentials saved for deployment script"
echo "" STEPS_COMPLETED=$((STEPS_COMPLETED + 1))
log_progress "Completed: $STEPS_COMPLETED/$STEPS_TOTAL steps"
echo ""
}
# Step 8: Create Directory Structure step_8_create_directories() {
log_info "Step 8/10: Creating directory structure..." log_info "Step 8/$STEPS_TOTAL: Creating directory structure..."
mkdir -p /opt/stacks mkdir -p /opt/stacks
mkdir -p /opt/dockge/data mkdir -p /opt/dockge/data
mkdir -p /mnt/media/{movies,tv,music,books,photos} mkdir -p /mnt/media/{movies,tv,music,books,photos}
mkdir -p /mnt/downloads/{complete,incomplete} mkdir -p /mnt/downloads/{complete,incomplete}
mkdir -p /mnt/backups mkdir -p /mnt/backups
mkdir -p /mnt/surveillance mkdir -p /mnt/surveillance
mkdir -p /mnt/git mkdir -p /mnt/git
# Set ownership # Set ownership
chown -R "$ACTUAL_USER:$ACTUAL_USER" /opt/stacks chown -R "$ACTUAL_USER:$ACTUAL_USER" /opt/stacks
chown -R "$ACTUAL_USER:$ACTUAL_USER" /opt/dockge chown -R "$ACTUAL_USER:$ACTUAL_USER" /opt/dockge
chown -R "$ACTUAL_USER:$ACTUAL_USER" /mnt/media chown -R "$ACTUAL_USER:$ACTUAL_USER" /mnt/media
chown -R "$ACTUAL_USER:$ACTUAL_USER" /mnt/downloads chown -R "$ACTUAL_USER:$ACTUAL_USER" /mnt/downloads
chown -R "$ACTUAL_USER:$ACTUAL_USER" /mnt/backups chown -R "$ACTUAL_USER:$ACTUAL_USER" /mnt/backups
chown -R "$ACTUAL_USER:$ACTUAL_USER" /mnt/surveillance chown -R "$ACTUAL_USER:$ACTUAL_USER" /mnt/surveillance
chown -R "$ACTUAL_USER:$ACTUAL_USER" /mnt/git 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 ""
}
log_success "Directory structure created" step_9_create_networks() {
echo "" 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 9: Create Docker Networks step_10_nvidia_drivers() {
log_info "Step 9/10: Creating Docker networks..." log_info "Step 10/$STEPS_TOTAL (Optional): Checking for NVIDIA GPU..."
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 # Detect NVIDIA GPU
log_info "Step 10/10 (Optional): Checking for NVIDIA GPU..." if lspci | grep -i nvidia > /dev/null; then
# Detect NVIDIA GPU
if lspci | grep -i nvidia > /dev/null; then
log_info "NVIDIA GPU detected:" log_info "NVIDIA GPU detected:"
GPU_INFO=$(lspci | grep -i nvidia) GPU_INFO=$(lspci | grep -i nvidia)
echo "$GPU_INFO" echo "$GPU_INFO"
@@ -430,10 +527,98 @@ if lspci | grep -i nvidia > /dev/null; then
else else
log_warning "NVIDIA GPU detected but drivers not installed" log_warning "NVIDIA GPU detected but drivers not installed"
echo "" echo ""
read -p "Do you want to install NVIDIA drivers now? (y/N): " -n 1 -r
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 ""
fi
}
show_final_summary() {
echo ""
echo "=========================================="
log_success "AI-Homelab setup completed successfully!"
log_progress "All $STEPS_TOTAL core steps completed!"
echo "=========================================="
echo "" echo ""
if [[ $REPLY =~ ^[Yy]$ ]]; then 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
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
}
# 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..."
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"
}
# NVIDIA Driver Installation Function
install_nvidia_drivers() {
local GPU_INFO="$1"
log_info "Installing NVIDIA drivers using official installer..." log_info "Installing NVIDIA drivers using official installer..."
echo "" echo ""
@@ -503,15 +688,8 @@ EOF
NVIDIA_INSTALLED=false NVIDIA_INSTALLED=false
NVIDIA_REBOOT_NEEDED=false NVIDIA_REBOOT_NEEDED=false
fi 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
fi
# Check if NVIDIA Container Toolkit is installed # Install NVIDIA Container Toolkit if drivers installed successfully
if [ "$NVIDIA_INSTALLED" = true ]; then if [ "$NVIDIA_INSTALLED" = true ]; then
if command -v nvidia-container-runtime &> /dev/null; then if command -v nvidia-container-runtime &> /dev/null; then
log_warning "NVIDIA Container Toolkit is already installed" log_warning "NVIDIA Container Toolkit is already installed"
@@ -541,60 +719,70 @@ EOF
fi fi
fi 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 "" echo ""
else log_info "Partial setup may have occurred. To resume:"
log_info "No NVIDIA GPU detected, skipping driver installation" log_info " 1. Review error messages above"
NVIDIA_REBOOT_NEEDED=false log_info " 2. Fix the issue if possible"
log_info " 3. Re-run: sudo ./setup-homelab.sh"
echo "" 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 ""
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 fi
# If running as root (not via sudo), handle sudo grant and exit
# Final Summary if [ "$ACTUAL_USER" = "root" ]; then
echo "" handle_root_user
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 ""
fi fi
log_info "Next steps:" log_info "Setting up AI-Homelab for user: $ACTUAL_USER"
echo "" if [ "$AUTO_YES" = true ]; then
echo " 1. Log out and log back in for group changes to take effect" log_info "Running in automated mode (--yes flag enabled)"
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"
fi fi
echo "" echo ""
echo "=========================================="
echo ""
log_info "Setup complete!"
echo ""
# Instructions for deployment # Progress tracking
if [ "${NVIDIA_REBOOT_NEEDED:-false}" = true ]; then STEPS_TOTAL=9
log_info "Please reboot your system for NVIDIA drivers, then run:" STEPS_COMPLETED=0
else
log_info "Next step - deploy your homelab services:" # Execute setup steps in order
fi step_0_preflight_checks
echo "" step_1_update_system
echo " cd ~/AI-Homelab" step_2_install_packages
echo " sudo ./scripts/deploy-homelab.sh" step_3_install_docker
echo "" 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