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,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
# Check if Docker is installed and running
log_info "Validating Docker installation..."
if [ -n "$ADMIN_USER" ] && [ -n "$ADMIN_EMAIL" ]; then
log_success "Using credentials: $ADMIN_USER ($ADMIN_EMAIL)"
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
# 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'
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}"
# 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()
@@ -267,249 +200,362 @@ 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!"
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 ""
# 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"
# 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"
# 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
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 ""
# 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
echo -n "."
sleep 2
WAITED=$((WAITED + 2))
done
echo ""
echo ""
# 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
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..."
# Copy compose file
cp "$REPO_DIR/docker-compose/core.yml" /opt/stacks/core/docker-compose.yml
# 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"
# 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 "No browser detected. Please manually open: $DOCKGE_URL"
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

File diff suppressed because it is too large Load Diff