feat: Add Option 3 - Deploy Additional Server with multi-server support

Major features:
- Automated SSH key setup between remote and core servers
- Docker TLS configuration with shared CA certificates
- Automatic deployment of Dockge, Traefik, Sablier, and Infrastructure stacks
- Copy all stacks (except core) to remote server for on-demand deployment
- New standalone Traefik stack for remote server container discovery
- Locale-aware SSH/SCP commands to handle Raspberry Pi warnings
- Variable expansion support in .env files (${VAR} references)
- Comprehensive error handling and verbose deployment logging

Technical improvements:
- setup_ssh_key_to_core() - Automated RSA 4096-bit key generation and installation
- setup_multi_server_tls() - Fetch shared CA from core server via SSH
- copy_all_stacks_for_remote() - Deploy all stacks except core
- deploy_traefik_stack() - Local Traefik for container discovery
- Enhanced localization with envsubst support
- Docker network creation (traefik-network, homelab-network)
- Password authentication with special character handling

Fixes:
- Fixed SSH key path handling for non-root users
- Fixed SCP exit code checking (was checking grep instead of scp)
- Fixed CA file detection with proper test commands
- Removed unnecessary prepare_deployment() function call
- Added ACTUAL_USER variable initialization for remote deployments
This commit is contained in:
kelin
2026-02-06 22:00:25 -05:00
parent 5b3c4a2c5b
commit 44b529a7cb
3 changed files with 460 additions and 151 deletions

View File

@@ -12,13 +12,6 @@ TZ=America/New_York
SERVER_IP=192.168.1.100 # This server SERVER_IP=192.168.1.100 # This server
SERVER_HOSTNAME=debian SERVER_HOSTNAME=debian
# If deploying with option 3: Remote Core Server
# the REMOTE_SERVER is where the Core Stack (Traefik) is running
REMOTE_SERVER_IP=your.remote.ip.address
REMOTE_SERVER_HOSTNAME=your-remote-server
REMOTE_SERVER_USER=${DEFAULT_USER}
REMOTE_SERVER_PASSWORD=${DEFAULT_PASSWORD}
# Domain Configuration # Domain Configuration
DUCKDNS_SUBDOMAINS=yourdomain # Without .duckdns.org DUCKDNS_SUBDOMAINS=yourdomain # Without .duckdns.org
DUCKDNS_TOKEN=your-duckdns-token DUCKDNS_TOKEN=your-duckdns-token
@@ -35,6 +28,13 @@ MEDIADIR=/mnt/media # Large media files on separate drive
DOWNLOADDIR=/mnt/downloads # Downloads on separate drive DOWNLOADDIR=/mnt/downloads # Downloads on separate drive
PROJECTDIR=~/projects # User's projects folder PROJECTDIR=~/projects # User's projects folder
# If selecting option 3: Deploy Additional Server
# the CORE_SERVER is where the Core Traefik is running
CORE_SERVER_IP=192.168.1.101
CORE_SERVER_HOSTNAME=debian2
CORE_SERVER_USER=${DEFAULT_USER}
CORE_SERVER_PASSWORD=${DEFAULT_PASSWORD}
# ########################################## # ##########################################
# #### NOTEABLE OPTIONAL CONFIGURATIONS #### # #### NOTEABLE OPTIONAL CONFIGURATIONS ####

View File

@@ -0,0 +1,40 @@
# Traefik Service for Remote Servers
# This standalone Traefik instance runs on remote servers to discover local containers
# and communicate with the core Traefik on the core server via Docker TLS
services:
traefik:
# Local Traefik instance for container discovery on this remote server
image: traefik:v3
container_name: traefik
restart: unless-stopped
command:
- '--api.dashboard=true'
- '--api.insecure=false'
- '--providers.docker=true'
- '--providers.docker.exposedbydefault=false'
- '--providers.docker.network=traefik-network'
- '--providers.file.directory=/dynamic'
- '--providers.file.watch=true'
- '--log.level=INFO'
- '--accesslog=true'
- '--entrypoints.web.address=:80'
- '--entrypoints.websecure.address=:443'
environment:
- TZ=America/New_York
ports:
- '8080:8080' # Dashboard (optional, for debugging)
volumes:
- ./config:/config
- ./dynamic:/dynamic
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- traefik-network
labels:
- 'homelab.category=infrastructure'
- 'homelab.description=Local reverse proxy for container discovery'
- 'traefik.enable=false' # This Traefik doesn't route itself
networks:
traefik-network:
external: true

View File

@@ -94,6 +94,30 @@ load_env_file_safely() {
fi fi
done < "$env_file" done < "$env_file"
# Second pass: expand any ${VAR} references in loaded variables
while IFS= read -r line || [ -n "$line" ]; do
# Skip comments and empty lines
[[ $line =~ ^[[:space:]]*# ]] && continue
[[ -z "$line" ]] && continue
# Parse KEY=VALUE
if [[ $line =~ ^([^=]+)=(.*)$ ]]; then
local key="${BASH_REMATCH[1]}"
key=$(echo "$key" | xargs)
# Get current value
local current_value="${!key}"
# Check if value contains ${...} and expand it
if [[ "$current_value" =~ \$\{[^}]+\} ]]; then
# Use eval to expand the variable reference safely within quotes
local expanded_value=$(eval echo "\"$current_value\"")
export "$key"="$expanded_value"
debug_log "Expanded $key variable reference"
fi
fi
done < "$env_file"
debug_log "Env file loaded successfully" debug_log "Env file loaded successfully"
} }
load_env_file() { load_env_file() {
@@ -226,134 +250,197 @@ generate_shared_ca() {
log_success "Shared CA generated" log_success "Shared CA generated"
} }
# Setup SSH key authentication to core server
setup_ssh_key_to_core() {
local key_name="id_rsa_${SERVER_HOSTNAME}_to_core"
local key_path="/home/$ACTUAL_USER/.ssh/$key_name"
log_info "Setting up SSH key authentication to core server..."
# Ensure .ssh directory exists
mkdir -p "/home/$ACTUAL_USER/.ssh"
chmod 700 "/home/$ACTUAL_USER/.ssh"
# Check if key already exists
if [ -f "$key_path" ]; then
log_info "SSH key already exists: $key_path"
# Test if key works with aggressive timeout
log_info "Testing existing SSH key..."
if timeout 3 bash -c "LC_ALL=C ssh -i '$key_path' -o BatchMode=yes -o ConnectTimeout=2 -o StrictHostKeyChecking=no \
-o ServerAliveInterval=1 -o ServerAliveCountMax=1 -o LogLevel=ERROR \
'${CORE_SERVER_USER}@${CORE_SERVER_IP}' 'echo test' 2>&1 | grep -v 'locale\|LC_ALL\|setlocale' | grep -q 'test'"; then
log_success "Existing SSH key works, skipping key setup"
export SSH_KEY_PATH="$key_path"
return 0
else
log_warning "Existing key doesn't work or connection failed, will regenerate and install"
rm -f "$key_path" "$key_path.pub"
fi
fi
# Generate new SSH key
log_info "Generating SSH key: $key_path"
ssh-keygen -t rsa -b 4096 -f "$key_path" -N "" \
-C "${SERVER_HOSTNAME}-to-core-${CORE_SERVER_HOSTNAME}" 2>&1 | grep -v "^Generating\|^Your identification\|^Your public key"
if [ ${PIPESTATUS[0]} -ne 0 ]; then
log_error "Failed to generate SSH key"
return 1
fi
log_success "SSH key generated"
# Install key on core server using password
log_info "Installing SSH key on core server ${CORE_SERVER_IP}..."
if ! command -v sshpass &> /dev/null; then
log_info "sshpass is not installed. Installing now..."
sudo apt-get update -qq && sudo apt-get install -y sshpass >/dev/null 2>&1
fi
# Debug: Check if password is set
if [ -z "$CORE_SERVER_PASSWORD" ]; then
log_error "CORE_SERVER_PASSWORD is empty!"
log_error "Check your .env file - ensure CORE_SERVER_PASSWORD is set correctly"
return 1
fi
# Show password length for debugging (not actual password)
log_info "Password length: ${#CORE_SERVER_PASSWORD} characters"
# Test SSH connection with password first
log_info "Testing SSH connection with password..."
if ! LC_ALL=C sshpass -p "$CORE_SERVER_PASSWORD" ssh \
-o StrictHostKeyChecking=no \
-o ConnectTimeout=10 \
-o LogLevel=ERROR \
"${CORE_SERVER_USER}@${CORE_SERVER_IP}" "echo 'SSH connection successful'" 2>&1 | grep -v "locale\|LC_ALL\|setlocale" | grep -q "successful"; then
log_error "SSH password authentication failed"
log_error "Please verify the password in your .env file is correct"
log_error "You can test manually: sshpass -p 'YOUR_PASSWORD' ssh ${CORE_SERVER_USER}@${CORE_SERVER_IP}"
return 1
fi
log_success "SSH password authentication works"
# Read the public key
local pub_key=$(cat "${key_path}.pub")
# Copy key to core server using direct SSH command (more reliable than ssh-copy-id)
log_info "Adding public key to authorized_keys on core server..."
LC_ALL=C sshpass -p "$CORE_SERVER_PASSWORD" ssh \
-o StrictHostKeyChecking=no \
-o ConnectTimeout=10 \
-o LogLevel=ERROR \
"${CORE_SERVER_USER}@${CORE_SERVER_IP}" \
"mkdir -p ~/.ssh && chmod 700 ~/.ssh && echo '$pub_key' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys" 2>&1 | grep -v "locale\|LC_ALL\|setlocale"
if [ $? -ne 0 ]; then
log_error "Failed to copy SSH key to core server"
log_error "Please verify:"
echo " 1. Core server IP is correct: ${CORE_SERVER_IP}"
echo " 2. Username is correct: ${CORE_SERVER_USER}"
echo " 3. Password is correct"
echo " 4. SSH server is running on core server"
return 1
fi
# Verify key works
log_info "Verifying SSH key authentication..."
if LC_ALL=C ssh -i "$key_path" -o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o LogLevel=ERROR \
"${CORE_SERVER_USER}@${CORE_SERVER_IP}" "echo 'SSH key authentication successful'" 2>&1 | grep -v "locale\|LC_ALL\|setlocale" | grep -q "successful"; then
log_success "SSH key authentication verified"
# Export key path for use by other functions
export SSH_KEY_PATH="$key_path"
return 0
else
log_error "SSH key verification failed"
return 1
fi
}
# Function to setup multi-server TLS for remote servers # Function to setup multi-server TLS for remote servers
setup_multi_server_tls() { setup_multi_server_tls() {
local ca_dir="/opt/stacks/core/shared-ca" local ca_dir="/opt/stacks/core/shared-ca"
# Use the SSH key path that was exported by setup_ssh_key_to_core
if [ -z "$SSH_KEY_PATH" ]; then
log_error "SSH_KEY_PATH not set. Please run setup_ssh_key_to_core first."
return 1
fi
local key_path="$SSH_KEY_PATH"
log_info "Using SSH key: $key_path"
sudo mkdir -p "$ca_dir" sudo mkdir -p "$ca_dir"
sudo chown "$ACTUAL_USER:$ACTUAL_USER" "$ca_dir" sudo chown "$ACTUAL_USER:$ACTUAL_USER" "$ca_dir"
if [ -n "$CORE_SERVER_IP" ]; then log_info "Fetching shared CA from core server ${CORE_SERVER_IP}..."
log_info "Setting up multi-server TLS using shared CA from core server $CORE_SERVER_IP..."
else # Check if shared CA exists on core server
# Prompt for core server IP if not set log_info "Checking for shared CA on core server..."
read -p "Enter the IP address of your core server: " CORE_SERVER_IP
while [ -z "$CORE_SERVER_IP" ]; do SHARED_CA_PATH=""
log_warning "Core server IP is required for shared TLS"
read -p "Enter the IP address of your core server: " CORE_SERVER_IP # Test for shared-ca directory (preferred location)
done if LC_ALL=C ssh -i "$key_path" -o StrictHostKeyChecking=no -o LogLevel=ERROR "${CORE_SERVER_USER}@${CORE_SERVER_IP}" \
log_info "Setting up multi-server TLS using shared CA from core server $CORE_SERVER_IP..." "test -f /opt/stacks/core/shared-ca/ca.pem && test -f /opt/stacks/core/shared-ca/ca-key.pem && echo 'EXISTS'" 2>/dev/null | grep -q "EXISTS"; then
SHARED_CA_PATH="/opt/stacks/core/shared-ca"
log_success "Found shared CA in: $SHARED_CA_PATH"
# Test for docker-tls directory (alternative location)
elif LC_ALL=C ssh -i "$key_path" -o StrictHostKeyChecking=no -o LogLevel=ERROR "${CORE_SERVER_USER}@${CORE_SERVER_IP}" \
"test -f /opt/stacks/core/docker-tls/ca.pem && test -f /opt/stacks/core/docker-tls/ca-key.pem && echo 'EXISTS'" 2>/dev/null | grep -q "EXISTS"; then
SHARED_CA_PATH="/opt/stacks/core/docker-tls"
log_success "Found shared CA in: $SHARED_CA_PATH"
fi fi
# Prompt for SSH username if not set if [ -z "$SHARED_CA_PATH" ]; then
if [ -z "$SSH_USER" ]; then log_error "Shared CA not found on core server"
DEFAULT_SSH_USER="${DEFAULT_USER:-$USER}" log_error "Please ensure core server is fully deployed with Option 2 first"
read -p "SSH username for core server [$DEFAULT_SSH_USER]: " SSH_USER log_info "Checking what exists on core server..."
SSH_USER="${SSH_USER:-$DEFAULT_SSH_USER}" LC_ALL=C ssh -i "$key_path" -o StrictHostKeyChecking=no "${CORE_SERVER_USER}@${CORE_SERVER_IP}" \
fi "ls -la /opt/stacks/core/shared-ca/ /opt/stacks/core/docker-tls/ 2>&1" | grep -v "locale\|LC_ALL\|setlocale"
TLS_ISSUES_SUMMARY="⚠️ TLS Configuration Issue: Shared CA not found on core server ${CORE_SERVER_IP}
# Test SSH connection - try key authentication first
log_info "Testing SSH connection to core server ($SSH_USER@$CORE_SERVER_IP)..."
if ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=no -o BatchMode=yes "$SSH_USER@$CORE_SERVER_IP" "echo 'SSH connection successful'" 2>/dev/null; then
log_success "SSH connection established using key authentication"
USE_SSHPASS=false
else
# Key authentication failed, try password authentication
log_info "Key authentication failed, trying password authentication..."
read -s -p "Enter SSH password for $SSH_USER@$CORE_SERVER_IP: " SSH_PASSWORD
echo ""
if sshpass -p "$SSH_PASSWORD" ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=no "$SSH_USER@$CORE_SERVER_IP" "echo 'SSH connection successful'" 2>/dev/null; then
log_success "SSH connection established using password authentication"
USE_SSHPASS=true
else
log_error "Cannot connect to core server via SSH. Please check:"
echo " 1. SSH is running on the core server"
echo " 2. SSH keys are properly configured, or username/password are correct"
echo " 3. The core server IP is correct"
echo ""
TLS_ISSUES_SUMMARY="⚠️ TLS Configuration Issue: Cannot connect to core server $CORE_SERVER_IP via SSH
This will prevent Sablier from connecting to remote Docker daemons.
To fix this: To fix this:
1. Ensure SSH is running on the core server 1. Deploy core server first using Option 2
2. Configure SSH keys or provide correct password 2. Verify CA exists: ssh ${CORE_SERVER_USER}@${CORE_SERVER_IP} 'ls -la /opt/stacks/core/shared-ca/'
3. Verify the core server IP is correct 3. Re-run Option 3 deployment"
4. Test SSH connection: ssh $SSH_USER@$CORE_SERVER_IP return 1
Without SSH access, shared CA cannot be fetched for secure multi-server TLS."
return
fi
fi fi
# Fetch shared CA certificates from core server # Copy shared CA from core server using SCP
log_info "Fetching shared CA certificates from core server..." log_info "Copying shared CA certificates..."
SHARED_CA_EXISTS=false
# Check if shared CA exists on core server (check both old and new locations) # Copy ca.pem
if [ "$USE_SSHPASS" = true ] && [ -n "$SSH_PASSWORD" ]; then if ! LC_ALL=C scp -i "$key_path" -o StrictHostKeyChecking=no -o LogLevel=ERROR \
if sshpass -p "$SSH_PASSWORD" ssh -o StrictHostKeyChecking=no "$SSH_USER@$CORE_SERVER_IP" "[ -f /opt/stacks/core/shared-ca/ca.pem ] && [ -f /opt/stacks/core/shared-ca/ca-key.pem ] && [ -r /opt/stacks/core/shared-ca/ca.pem ] && [ -r /opt/stacks/core/shared-ca/ca-key.pem ]" 2>/dev/null; then "${CORE_SERVER_USER}@${CORE_SERVER_IP}:${SHARED_CA_PATH}/ca.pem" \
SHARED_CA_EXISTS=true "$ca_dir/" 2>/dev/null; then
SHARED_CA_PATH="/opt/stacks/core/shared-ca" log_error "Failed to copy ca.pem from core server"
log_info "Detected CA certificate and key in shared-ca location" TLS_ISSUES_SUMMARY="⚠️ TLS Configuration Issue: Could not copy shared CA from ${CORE_SERVER_IP}
elif sshpass -p "$SSH_PASSWORD" ssh -o StrictHostKeyChecking=no "$SSH_USER@$CORE_SERVER_IP" "[ -f /opt/stacks/core/docker-tls/ca.pem ] && [ -f /opt/stacks/core/docker-tls/ca-key.pem ] && [ -r /opt/stacks/core/docker-tls/ca.pem ] && [ -r /opt/stacks/core/docker-tls/ca-key.pem ]" 2>/dev/null; then
SHARED_CA_EXISTS=true To fix this:
SHARED_CA_PATH="/opt/stacks/core/docker-tls" 1. Verify SSH key works: ssh -i $key_path ${CORE_SERVER_USER}@${CORE_SERVER_IP}
log_info "Detected CA certificate and key in docker-tls location" 2. Check file permissions: ssh ${CORE_SERVER_USER}@${CORE_SERVER_IP} 'ls -la ${SHARED_CA_PATH}/'
fi 3. Manually copy if needed: scp -i $key_path ${CORE_SERVER_USER}@${CORE_SERVER_IP}:${SHARED_CA_PATH}/ca* $ca_dir/"
else return 1
if ssh -o StrictHostKeyChecking=no "$SSH_USER@$CORE_SERVER_IP" "[ -f /opt/stacks/core/shared-ca/ca.pem ] && [ -f /opt/stacks/core/shared-ca/ca-key.pem ] && [ -r /opt/stacks/core/shared-ca/ca.pem ] && [ -r /opt/stacks/core/shared-ca/ca-key.pem ]" 2>/dev/null; then
SHARED_CA_EXISTS=true
SHARED_CA_PATH="/opt/stacks/core/shared-ca"
log_info "Detected CA certificate and key in shared-ca location"
elif ssh -o StrictHostKeyChecking=no "$SSH_USER@$CORE_SERVER_IP" "[ -f /opt/stacks/core/docker-tls/ca.pem ] && [ -f /opt/stacks/core/docker-tls/ca-key.pem ] && [ -r /opt/stacks/core/docker-tls/ca.pem ] && [ -r /opt/stacks/core/docker-tls/ca-key.pem ]" 2>/dev/null; then
SHARED_CA_EXISTS=true
SHARED_CA_PATH="/opt/stacks/core/docker-tls"
log_info "Detected CA certificate and key in docker-tls location"
fi
fi fi
if [ "$SHARED_CA_EXISTS" = true ]; then # Copy ca-key.pem
# Copy existing shared CA from core server if ! LC_ALL=C scp -i "$key_path" -o StrictHostKeyChecking=no -o LogLevel=ERROR \
set +e "${CORE_SERVER_USER}@${CORE_SERVER_IP}:${SHARED_CA_PATH}/ca-key.pem" \
scp_output=$(scp -o StrictHostKeyChecking=no "$SSH_USER@$CORE_SERVER_IP:$SHARED_CA_PATH/ca.pem" "$SSH_USER@$CORE_SERVER_IP:$SHARED_CA_PATH/ca-key.pem" "$ca_dir/" 2>&1) "$ca_dir/" 2>/dev/null; then
scp_exit_code=$? log_error "Failed to copy ca-key.pem from core server"
set -e return 1
if [ $scp_exit_code -eq 0 ]; then fi
log_success "Shared CA certificate and key fetched from core server"
log_success "Shared CA copied successfully"
# Now setup Docker TLS using the shared CA
setup_docker_tls setup_docker_tls
else
log_error "Failed to fetch shared CA certificate and key from core server"
TLS_ISSUES_SUMMARY="⚠️ TLS Configuration Issue: Could not copy shared CA from core server $CORE_SERVER_IP
SCP Error: $scp_output
To fix this:
1. Ensure SSH key authentication works: ssh $ACTUAL_USER@$CORE_SERVER_IP
2. Verify core server has: $SHARED_CA_PATH/ca.pem and ca-key.pem
3. Check file permissions on core server: ls -la $SHARED_CA_PATH/
4. Manually copy CA: scp $ACTUAL_USER@$CORE_SERVER_IP:$SHARED_CA_PATH/ca.pem $ca_dir/
scp $ACTUAL_USER@$CORE_SERVER_IP:$SHARED_CA_PATH/ca-key.pem $ca_dir/
5. Regenerate server certificates: run setup_docker_tls after copying
6. Restart Docker: sudo systemctl restart docker
Then restart Sablier on the core server to reconnect."
return
fi
else
log_warning "Shared CA certificates not found on core server."
log_info "Please ensure the core server has been set up first and has generated the shared CA certificates."
TLS_ISSUES_SUMMARY="⚠️ TLS Configuration Issue: Shared CA certificates not found on core server $CORE_SERVER_IP
This will prevent Sablier from connecting to remote Docker daemons.
To fix this:
1. Ensure the core server is set up and has generated shared CA certificates
2. Verify SSH access: ssh $ACTUAL_USER@$CORE_SERVER_IP
3. Check core server locations: /opt/stacks/core/shared-ca/ or /opt/stacks/core/docker-tls/
4. Manually copy CA certificates if needed
5. Re-run the infrastructure deployment
Without shared CA, remote Docker access will not work securely."
return
fi
} }
# Get script directory and repo directory # Get script directory and repo directory
@@ -373,7 +460,6 @@ fi
# Default values # Default values
DOMAIN="" DOMAIN=""
SERVER_IP="" SERVER_IP=""
CORE_SERVER_IP=""
ADMIN_USER="" ADMIN_USER=""
ADMIN_EMAIL="" ADMIN_EMAIL=""
AUTHELIA_ADMIN_PASSWORD="" AUTHELIA_ADMIN_PASSWORD=""
@@ -381,7 +467,13 @@ DEPLOY_CORE=false
DEPLOY_INFRASTRUCTURE=false DEPLOY_INFRASTRUCTURE=false
DEPLOY_DASHBOARDS=false DEPLOY_DASHBOARDS=false
SETUP_STACKS=false SETUP_STACKS=false
DEPLOY_REMOTE_SERVER=false
TLS_ISSUES_SUMMARY="" TLS_ISSUES_SUMMARY=""
CORE_SERVER_IP=""
CORE_SERVER_HOSTNAME=""
CORE_SERVER_USER=""
CORE_SERVER_PASSWORD=""
SSH_KEY_PATH=""
# Required variables for configuration # Required variables for configuration
REQUIRED_VARS=("SERVER_IP" "SERVER_HOSTNAME" "DUCKDNS_SUBDOMAINS" "DUCKDNS_TOKEN" "DOMAIN" "DEFAULT_USER" "DEFAULT_PASSWORD" "DEFAULT_EMAIL") REQUIRED_VARS=("SERVER_IP" "SERVER_HOSTNAME" "DUCKDNS_SUBDOMAINS" "DUCKDNS_TOKEN" "DOMAIN" "DEFAULT_USER" "DEFAULT_PASSWORD" "DEFAULT_EMAIL")
@@ -1408,7 +1500,7 @@ set_required_vars_for_deployment() {
debug_log "Set REQUIRED_VARS for core deployment" debug_log "Set REQUIRED_VARS for core deployment"
;; ;;
"remote") "remote")
REQUIRED_VARS=("SERVER_IP" "SERVER_HOSTNAME" "DUCKDNS_DOMAIN" "DEFAULT_USER" "REMOTE_SERVER_IP" "REMOTE_SERVER_HOSTNAME" "REMOTE_SERVER_USER") REQUIRED_VARS=("SERVER_IP" "SERVER_HOSTNAME" "DOMAIN" "DEFAULT_USER" "CORE_SERVER_IP" "CORE_SERVER_HOSTNAME" "CORE_SERVER_USER" "CORE_SERVER_PASSWORD")
debug_log "Set REQUIRED_VARS for remote deployment" debug_log "Set REQUIRED_VARS for remote deployment"
;; ;;
*) *)
@@ -1420,9 +1512,23 @@ set_required_vars_for_deployment() {
# Deploy remote server # Deploy remote server
deploy_remote_server() { deploy_remote_server() {
# Enable verbose mode for remote deployment
VERBOSE=true
log_info "Deploying Remote Server Configuration" log_info "Deploying Remote Server Configuration"
echo "" echo ""
# Set ACTUAL_USER if not already set (needed for SSH key paths)
if [ -z "$ACTUAL_USER" ]; then
if [ "$EUID" -eq 0 ]; then
ACTUAL_USER=${SUDO_USER:-$USER}
else
ACTUAL_USER=$USER
fi
export ACTUAL_USER
debug_log "Set ACTUAL_USER=$ACTUAL_USER"
fi
# Check Docker is installed # Check Docker is installed
if ! check_docker_installed; then if ! check_docker_installed; then
log_error "Docker must be installed before deploying remote server" log_error "Docker must be installed before deploying remote server"
@@ -1430,34 +1536,91 @@ deploy_remote_server() {
fi fi
# Ensure we have core server information # Ensure we have core server information
if [ -z "$REMOTE_SERVER_IP" ] || [ -z "$REMOTE_SERVER_HOSTNAME" ]; then if [ -z "$CORE_SERVER_IP" ] || [ -z "$CORE_SERVER_HOSTNAME" ]; then
log_error "Remote server IP and hostname are required" log_error "Core server IP and hostname are required"
return 1 return 1
fi fi
log_info "Configuring Docker TLS for remote API access..." # Step 1: Setup SSH key authentication to core server
setup_docker_tls log_info "Step 1: Setting up SSH key authentication to core server..."
log_info "Using ACTUAL_USER=$ACTUAL_USER for SSH key path"
log_info "Target: ${CORE_SERVER_USER}@${CORE_SERVER_IP}"
if ! setup_ssh_key_to_core; then
log_error "Failed to setup SSH key authentication"
return 1
fi
log_success "SSH key authentication setup complete"
echo ""
log_info "Fetching shared CA from core server..." # Step 2: Fetch shared CA and setup Docker TLS
setup_multi_server_tls "$REMOTE_SERVER_IP" "$REMOTE_SERVER_USER" log_info "Step 2: Fetching shared CA from core server..."
log_info "Using SSH key: $SSH_KEY_PATH"
if ! setup_multi_server_tls; then
log_error "Failed to setup multi-server TLS"
return 1
fi
echo ""
log_info "Deploying Sablier stack for local lazy loading..." # Step 3: Create required Docker networks
log_info "Step 3: Creating required Docker networks..."
docker network create traefik-network 2>/dev/null && log_success "Created traefik-network" || log_info "traefik-network already exists"
docker network create homelab-network 2>/dev/null && log_success "Created homelab-network" || log_info "homelab-network already exists"
echo ""
# Step 4: Install envsubst if not present
if ! command -v envsubst &> /dev/null; then
log_info "Installing envsubst (gettext-base)..."
sudo apt-get update -qq && sudo apt-get install -y gettext-base >/dev/null 2>&1
log_success "envsubst installed"
fi
# Step 5: Copy all stacks to remote server
log_info "Step 5: Copying all stacks to remote server..."
copy_all_stacks_for_remote
echo ""
# Step 6: Deploy Dockge
log_info "Step 6: Deploying Dockge..."
deploy_dockge
echo ""
# Step 7: Deploy Traefik (local instance for container discovery)
log_info "Step 7: Deploying local Traefik..."
deploy_traefik_stack
echo ""
# Step 8: Deploy Sablier stack for local lazy loading
log_info "Step 8: Deploying Sablier stack..."
deploy_sablier_stack deploy_sablier_stack
echo ""
log_info "Registering this remote server with core Traefik..." # Step 9: Deploy Infrastructure stack
log_info "Step 9: Deploying Infrastructure stack..."
deploy_infrastructure
echo ""
# Step 10: Register this remote server with core Traefik
log_info "Step 10: Registering with core Traefik..."
register_remote_server_with_core register_remote_server_with_core
echo ""
log_success "Remote server deployment complete!" log_success "Remote server deployment complete!"
echo "" echo ""
echo "This server is now configured to:" echo "This server is now configured to:"
echo " - Accept Docker API connections via TLS (port 2376)" echo " - Accept Docker API connections via TLS (port 2376)"
echo " - Run local Traefik for container discovery"
echo " - Run Sablier for local container lazy loading" echo " - Run Sablier for local container lazy loading"
echo " - Run infrastructure services"
echo " - Have its containers discovered by core Traefik" echo " - Have its containers discovered by core Traefik"
echo "" echo ""
echo "Services deployed on this server will automatically:" echo "Services deployed on this server will automatically:"
echo " - Be discovered by Traefik on the core server" echo " - Be discovered by Traefik on the core server"
echo " - Get SSL certificates via core Traefik" echo " - Get SSL certificates via core Traefik"
echo " - Be accessible at: https://servicename.${DUCKDNS_DOMAIN}" echo " - Be accessible at: https://servicename.${DOMAIN}"
echo ""
echo "Additional stacks available in /opt/stacks/ (not started):"
echo " - dashboards, media, media-management, monitoring, productivity"
echo " - transcoders, utilities, vpn, wikis, homeassistant, alternatives"
echo "" echo ""
} }
@@ -1465,15 +1628,19 @@ deploy_remote_server() {
register_remote_server_with_core() { register_remote_server_with_core() {
debug_log "Registering remote server with core Traefik via SSH" debug_log "Registering remote server with core Traefik via SSH"
if [ -z "$REMOTE_SERVER_IP" ] || [ -z "$REMOTE_SERVER_USER" ]; then local key_name="id_rsa_${SERVER_HOSTNAME}_to_core"
log_error "REMOTE_SERVER_IP and REMOTE_SERVER_USER are required" local key_path="/home/$ACTUAL_USER/.ssh/$key_name"
if [ -z "$CORE_SERVER_IP" ] || [ -z "$CORE_SERVER_USER" ]; then
log_error "CORE_SERVER_IP and CORE_SERVER_USER are required"
return 1 return 1
fi fi
log_info "Connecting to core server to register this remote server..." log_info "Connecting to core server to register this remote server..."
# SSH to core server and run registration function # SSH to core server and run registration function
ssh -o ConnectTimeout=10 "${REMOTE_SERVER_USER}@${REMOTE_SERVER_IP}" bash <<EOF LC_ALL=C ssh -i "$key_path" -o ConnectTimeout=10 -o StrictHostKeyChecking=no -o LogLevel=ERROR \
"${CORE_SERVER_USER}@${CORE_SERVER_IP}" bash <<EOF 2>&1 | grep -v "locale\|LC_ALL\|setlocale"
# Source common.sh to get registration function # Source common.sh to get registration function
source ~/EZ-Homelab/scripts/common.sh source ~/EZ-Homelab/scripts/common.sh
@@ -1499,28 +1666,137 @@ deploy_sablier_stack() {
local sablier_dir="/opt/stacks/sablier" local sablier_dir="/opt/stacks/sablier"
# Create sablier stack directory # Create sablier stack directory with sudo
if [ ! -d "$sablier_dir" ]; then if [ ! -d "$sablier_dir" ]; then
mkdir -p "$sablier_dir" sudo mkdir -p "$sablier_dir" || { log_error "Failed to create $sablier_dir"; return 1; }
sudo chown -R "$ACTUAL_USER:$ACTUAL_USER" "$sablier_dir" sudo chown -R "$ACTUAL_USER:$ACTUAL_USER" "$sablier_dir"
log_success "Created $sablier_dir"
fi
# Check if source files exist
if [ ! -f "$REPO_DIR/docker-compose/sablier/docker-compose.yml" ]; then
log_error "Sablier docker-compose.yml not found in repo at $REPO_DIR/docker-compose/sablier/"
return 1
fi fi
# Copy stack files # Copy stack files
cp "$REPO_DIR/docker-compose/sablier/docker-compose.yml" "$sablier_dir/" log_info "Copying Sablier stack files from $REPO_DIR/docker-compose/sablier/..."
cp "$REPO_DIR/.env" "$sablier_dir/" cp "$REPO_DIR/docker-compose/sablier/docker-compose.yml" "$sablier_dir/" || { log_error "Failed to copy docker-compose.yml"; return 1; }
cp "$REPO_DIR/.env" "$sablier_dir/" || { log_error "Failed to copy .env"; return 1; }
sudo chown "$ACTUAL_USER:$ACTUAL_USER" "$sablier_dir/docker-compose.yml" sudo chown "$ACTUAL_USER:$ACTUAL_USER" "$sablier_dir/docker-compose.yml"
sudo chown "$ACTUAL_USER:$ACTUAL_USER" "$sablier_dir/.env" sudo chown "$ACTUAL_USER:$ACTUAL_USER" "$sablier_dir/.env"
log_success "Stack files copied"
# Remove Authelia and other unnecessary variables from sablier .env
sed -i '/^AUTHELIA_/d' "$sablier_dir/.env"
sed -i '/^DEFAULT_PASSWORD=/d' "$sablier_dir/.env"
sed -i '/^CORE_SERVER_PASSWORD=/d' "$sablier_dir/.env"
# Localize the docker-compose file # Localize the docker-compose file
localize_compose_labels "$sablier_dir/docker-compose.yml" localize_compose_labels "$sablier_dir/docker-compose.yml"
# Deploy # Deploy
log_info "Starting Sablier container..."
cd "$sablier_dir" cd "$sablier_dir"
run_cmd docker compose up -d if ! docker compose up -d; then
log_error "Failed to start Sablier stack"
return 1
fi
log_success "Sablier stack deployed at $sablier_dir" log_success "Sablier stack deployed at $sablier_dir"
} }
# Copy all stacks for remote server (except core)
copy_all_stacks_for_remote() {
debug_log "Copying all stacks for remote server"
# Create base stacks directory
sudo mkdir -p /opt/stacks
sudo mkdir -p /opt/dockge
sudo chown -R "$ACTUAL_USER:$ACTUAL_USER" /opt/stacks
sudo chown -R "$ACTUAL_USER:$ACTUAL_USER" /opt/dockge
# List of stacks to copy (all except core and dockge - dockge is handled separately)
local stacks=(
"alternatives"
"dashboards"
"homeassistant"
"infrastructure"
"media"
"media-management"
"monitoring"
"productivity"
"sablier"
"traefik"
"transcoders"
"utilities"
"vpn"
"wikis"
)
local copied_count=0
for stack in "${stacks[@]}"; do
local src_dir="$REPO_DIR/docker-compose/$stack"
local dest_dir="/opt/stacks/$stack"
# Skip if source doesn't exist
if [ ! -d "$src_dir" ]; then
debug_log "Skipping $stack - source not found"
continue
fi
# Create destination directory
mkdir -p "$dest_dir"
# Copy docker-compose.yml and any config directories
if [ -f "$src_dir/docker-compose.yml" ]; then
cp "$src_dir/docker-compose.yml" "$dest_dir/"
cp "$REPO_DIR/.env" "$dest_dir/"
# Copy any subdirectories (config, etc.)
for item in "$src_dir"/*; do
if [ -d "$item" ]; then
cp -r "$item" "$dest_dir/"
fi
done
# Clean up sensitive data from .env
sed -i '/^AUTHELIA_/d' "$dest_dir/.env"
sed -i '/^DEFAULT_PASSWORD=/d' "$dest_dir/.env"
sed -i '/^CORE_SERVER_PASSWORD=/d' "$dest_dir/.env"
# Localize compose file
localize_compose_labels "$dest_dir/docker-compose.yml" || true
copied_count=$((copied_count + 1))
debug_log "Copied $stack to $dest_dir"
fi
done
log_success "Copied $copied_count stacks to /opt/stacks/"
}
# Deploy Traefik stack (standalone for remote servers)
deploy_traefik_stack() {
debug_log "Deploying Traefik stack"
local traefik_dir="/opt/stacks/traefik"
# Create required directories
mkdir -p "$traefik_dir/config"
mkdir -p "$traefik_dir/dynamic"
# Deploy
log_info "Starting Traefik container..."
cd "$traefik_dir"
if ! docker compose up -d; then
log_error "Failed to start Traefik stack"
return 1
fi
log_success "Traefik stack deployed at $traefik_dir"
}
# Show help function # Show help function
show_help() { show_help() {
echo "EZ-Homelab Setup & Deployment Script" echo "EZ-Homelab Setup & Deployment Script"
@@ -1808,14 +2084,7 @@ main() {
echo "⚠️ IMPORTANT: Deploying an additional server requires an existing core server to be already deployed." echo "⚠️ IMPORTANT: Deploying an additional server requires an existing core server to be already deployed."
echo "The core server provides essential services like Traefik, Authelia, and shared TLS certificates." echo "The core server provides essential services like Traefik, Authelia, and shared TLS certificates."
echo "" echo ""
read -p "Do you have an existing core server deployed? (y/N): " -n 1 -r
echo ""
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
log_info "Returning to main menu. Please deploy a core server first using Option 2."
echo ""
sleep 2
continue
fi
DEPLOY_CORE=false DEPLOY_CORE=false
DEPLOY_INFRASTRUCTURE=false DEPLOY_INFRASTRUCTURE=false
DEPLOY_DASHBOARDS=false DEPLOY_DASHBOARDS=false
@@ -1851,9 +2120,6 @@ main() {
echo "" echo ""
# Prepare deployment environment
prepare_deployment
# Handle remote server deployment separately # Handle remote server deployment separately
if [ "$DEPLOY_REMOTE_SERVER" = true ]; then if [ "$DEPLOY_REMOTE_SERVER" = true ]; then
# Prompt for configuration values # Prompt for configuration values
@@ -1862,6 +2128,9 @@ main() {
# Save configuration # Save configuration
save_env_file save_env_file
# Reload .env file to get all variables including expanded ones
load_env_file
# Deploy remote server # Deploy remote server
deploy_remote_server deploy_remote_server