v0.1.2: Multi-server architecture + security cleanup

- Implement multi-server Traefik + Sablier architecture
- Add label-based automatic service discovery
- Create separate Sablier stack deployment
- Add remote server deployment workflow (Option 3)
- Add 9 new functions for multi-server management
- Remove deprecated config-templates folder
- Replace hardcoded private data with placeholders
- Update backup timestamp format to YY_MM_DD_hh_mm
- Add markup.yml to .gitignore

Breaking changes:
- Removed Sablier from core docker-compose.yml (now separate stack)
- Config templates moved from config-templates/ to docker-compose/core/
- REQUIRED_VARS now dynamic based on deployment type
This commit is contained in:
Kelin
2026-02-04 19:36:18 -05:00
parent 75e66586d1
commit 73cb274160
94 changed files with 2343 additions and 12845 deletions

View File

@@ -215,3 +215,130 @@ run_cmd() {
fi
fi
}
# =============================================
# MULTI-SERVER FUNCTIONS
# =============================================
# Detect server role based on deployed stacks
detect_server_role() {
debug_log "Detecting server role"
if [ -d "/opt/stacks/core" ] && [ -f "/opt/stacks/core/docker-compose.yml" ]; then
echo "core"
debug_log "Detected role: core"
else
echo "remote"
debug_log "Detected role: remote"
fi
}
# Generate Traefik provider configuration for a remote Docker host
generate_traefik_provider_config() {
local server_ip="$1"
local server_hostname="$2"
local output_file="$3"
debug_log "Generating Traefik provider config for $server_hostname ($server_ip)"
if [ -z "$server_ip" ] || [ -z "$server_hostname" ] || [ -z "$output_file" ]; then
log_error "generate_traefik_provider_config requires server_ip, server_hostname, and output_file"
return 1
fi
cat > "$output_file" <<EOF
# Traefik Docker Provider for Remote Server: $server_hostname
# Auto-generated by EZ-Homelab
# Last updated: $(date '+%Y-%m-%d %H:%M:%S')
providers:
docker:
endpoint: "tcp://${server_ip}:2376"
exposedByDefault: false
network: traefik-network
watch: true
tls:
ca: /shared-ca/ca.pem
cert: /shared-ca/cert.pem
key: /shared-ca/key.pem
insecureSkipVerify: false
# Server-specific constraints
defaultRule: "Host(\`{{ normalize .Name }}.${DUCKDNS_DOMAIN}\`)"
EOF
log_success "Generated Traefik provider config: $output_file"
debug_log "Provider config written to $output_file"
}
# Generate Sablier middleware configuration for remote server
generate_sablier_middleware_config() {
local server_hostname="$1"
local server_ip="$2"
local output_file="$3"
debug_log "Generating Sablier middleware config for $server_hostname ($server_ip)"
if [ -z "$server_hostname" ] || [ -z "$server_ip" ] || [ -z "$output_file" ]; then
log_error "generate_sablier_middleware_config requires server_hostname, server_ip, and output_file"
return 1
fi
cat > "$output_file" <<EOF
# Sablier Middleware for Remote Server: $server_hostname
# Auto-generated by EZ-Homelab
# Last updated: $(date '+%Y-%m-%d %H:%M:%S')
#
# This middleware enables lazy loading for services on $server_hostname
# Each server has its own Sablier instance managing local containers
http:
middlewares:
sablier-${server_hostname}:
plugin:
sablier:
sablierUrl: "http://${server_ip}:10000"
sessionDuration: "5m"
dynamic:
theme: "hacker-terminal"
EOF
log_success "Generated Sablier middleware config: $output_file"
debug_log "Sablier middleware config written to $output_file"
}
# Register remote server with core Traefik
add_remote_server_to_traefik() {
local server_ip="$1"
local server_hostname="$2"
debug_log "Registering remote server $server_hostname with core Traefik"
if [ -z "$server_ip" ] || [ -z "$server_hostname" ]; then
log_error "add_remote_server_to_traefik requires server_ip and server_hostname"
return 1
fi
# Check if core stack exists
if [ ! -d "/opt/stacks/core" ]; then
log_error "Core stack not found at /opt/stacks/core - cannot register remote server"
return 1
fi
local traefik_dynamic_dir="/opt/stacks/core/traefik/dynamic"
# Create dynamic directory if it doesn't exist
if [ ! -d "$traefik_dynamic_dir" ]; then
log_info "Creating Traefik dynamic config directory"
mkdir -p "$traefik_dynamic_dir"
fi
# Generate provider configuration
local provider_file="${traefik_dynamic_dir}/docker-provider-${server_hostname}.yml"
generate_traefik_provider_config "$server_ip" "$server_hostname" "$provider_file"
# Generate Sablier middleware configuration
local sablier_file="${traefik_dynamic_dir}/sablier-middleware-${server_hostname}.yml"
generate_sablier_middleware_config "$server_hostname" "$server_ip" "$sablier_file"
log_success "Registered remote server $server_hostname with core Traefik"
log_info "Traefik will auto-reload configurations within 2 seconds"
}

View File

@@ -206,17 +206,6 @@ localize_deployment() {
done < <(find "$REPO_DIR/docker-compose" -name "*.yml" -o -name "*.yaml" -print0 2>/dev/null)
fi
# Process config-templates files
if [ -d "$REPO_DIR/config-templates" ]; then
while IFS= read -r -d '' file_path; do
if [ -f "$file_path" ]; then
debug_log "Processing config template file: $file_path"
localize_yml_file "$file_path" false
processed_files=$((processed_files + 1))
fi
done < <(find "$REPO_DIR/config-templates" -name "*.yml" -o -name "*.yaml" -print0 2>/dev/null)
fi
log_success "Deployment localization completed - processed $processed_files files"
debug_log "Localization completed for $processed_files files"
@@ -909,9 +898,9 @@ deploy_core() {
# Copy and configure Traefik config
debug_log "Setting up Traefik configuration"
if [ -d "/opt/stacks/core/traefik" ]; then
mv /opt/stacks/core/traefik /opt/stacks/core/traefik.backup.$(date +%Y%m%d_%H%M%S)
mv /opt/stacks/core/traefik /opt/stacks/core/traefik.backup.$(date +%y_%m_%d_%H_%M)
fi
cp -r "$REPO_DIR/config-templates/traefik" /opt/stacks/core/
cp -r "$REPO_DIR/docker-compose/core/traefik" /opt/stacks/core/
sudo chown -R "$ACTUAL_USER:$ACTUAL_USER" /opt/stacks/core/traefik
# Move Traefik config file to the correct location for Docker mount
@@ -954,9 +943,9 @@ deploy_core() {
# Copy and configure Authelia config
debug_log "Setting up Authelia configuration"
if [ -d "/opt/stacks/core/authelia" ]; then
mv /opt/stacks/core/authelia /opt/stacks/core/authelia.backup.$(date +%Y%m%d_%H%M%S)
mv /opt/stacks/core/authelia /opt/stacks/core/authelia.backup.$(date +%y_%m_%d_%H_%M)
fi
cp -r "$REPO_DIR/config-templates/authelia" /opt/stacks/core/
cp -r "$REPO_DIR/docker-compose/core/authelia" /opt/stacks/core/
sudo chown -R "$ACTUAL_USER:$ACTUAL_USER" /opt/stacks/core/authelia
# Replace all placeholders in Authelia config files
@@ -991,6 +980,11 @@ deploy_core() {
run_cmd docker compose up -d || true
log_success "Core infrastructure deployed"
echo ""
# Deploy Sablier stack for lazy loading
log_info "Deploying Sablier stack for lazy loading..."
deploy_sablier_stack
echo ""
}
# Deploy infrastructure stack function
@@ -1363,6 +1357,154 @@ show_main_menu() {
echo ""
}
# =============================================
# MULTI-SERVER DEPLOYMENT FUNCTIONS
# =============================================
# Check if Docker is installed and accessible
check_docker_installed() {
debug_log "Checking if Docker is installed"
if ! command -v docker &> /dev/null; then
log_error "Docker is not installed on this system"
log_info "Please run Option 1 (Install Prerequisites) first"
return 1
fi
if ! docker ps &> /dev/null; then
log_error "Docker is installed but not accessible"
log_info "Current user may not be in docker group. Try logging out and back in."
return 1
fi
debug_log "Docker is installed and accessible"
return 0
}
# Set required variables based on deployment type
set_required_vars_for_deployment() {
local deployment_type="$1"
debug_log "Setting required vars for deployment type: $deployment_type"
case "$deployment_type" in
"core")
REQUIRED_VARS=("SERVER_IP" "SERVER_HOSTNAME" "DUCKDNS_SUBDOMAINS" "DUCKDNS_TOKEN" "DOMAIN" "DEFAULT_USER" "DEFAULT_PASSWORD" "DEFAULT_EMAIL")
debug_log "Set REQUIRED_VARS for core deployment"
;;
"remote")
REQUIRED_VARS=("SERVER_IP" "SERVER_HOSTNAME" "DUCKDNS_DOMAIN" "DEFAULT_USER" "REMOTE_SERVER_IP" "REMOTE_SERVER_HOSTNAME" "REMOTE_SERVER_USER")
debug_log "Set REQUIRED_VARS for remote deployment"
;;
*)
log_error "Unknown deployment type: $deployment_type"
return 1
;;
esac
}
# Deploy remote server
deploy_remote_server() {
log_info "Deploying Remote Server Configuration"
echo ""
# Check Docker is installed
if ! check_docker_installed; then
log_error "Docker must be installed before deploying remote server"
return 1
fi
# Ensure we have core server information
if [ -z "$REMOTE_SERVER_IP" ] || [ -z "$REMOTE_SERVER_HOSTNAME" ]; then
log_error "Remote server IP and hostname are required"
return 1
fi
log_info "Configuring Docker TLS for remote API access..."
setup_docker_tls
log_info "Fetching shared CA from core server..."
setup_multi_server_tls "$REMOTE_SERVER_IP" "$REMOTE_SERVER_USER"
log_info "Deploying Sablier stack for local lazy loading..."
deploy_sablier_stack
log_info "Registering this remote server with core Traefik..."
register_remote_server_with_core
log_success "Remote server deployment complete!"
echo ""
echo "This server is now configured to:"
echo " - Accept Docker API connections via TLS (port 2376)"
echo " - Run Sablier for local container lazy loading"
echo " - Have its containers discovered by core Traefik"
echo ""
echo "Services deployed on this server will automatically:"
echo " - Be discovered by Traefik on the core server"
echo " - Get SSL certificates via core Traefik"
echo " - Be accessible at: https://servicename.${DUCKDNS_DOMAIN}"
echo ""
}
# Register remote server with core Traefik
register_remote_server_with_core() {
debug_log "Registering remote server with core Traefik via SSH"
if [ -z "$REMOTE_SERVER_IP" ] || [ -z "$REMOTE_SERVER_USER" ]; then
log_error "REMOTE_SERVER_IP and REMOTE_SERVER_USER are required"
return 1
fi
log_info "Connecting to core server to register this remote server..."
# SSH to core server and run registration function
ssh -o ConnectTimeout=10 "${REMOTE_SERVER_USER}@${REMOTE_SERVER_IP}" bash <<EOF
# Source common.sh to get registration function
source ~/EZ-Homelab/scripts/common.sh
# Register this remote server
add_remote_server_to_traefik "${SERVER_IP}" "${SERVER_HOSTNAME}"
# Restart Traefik to reload configs
cd /opt/stacks/core
docker compose restart traefik
EOF
if [ $? -eq 0 ]; then
log_success "Successfully registered with core server"
else
log_error "Failed to register with core server via SSH"
return 1
fi
}
# Deploy Sablier stack
deploy_sablier_stack() {
debug_log "Deploying Sablier stack"
local sablier_dir="/opt/stacks/sablier"
# Create sablier stack directory
if [ ! -d "$sablier_dir" ]; then
mkdir -p "$sablier_dir"
sudo chown -R "$ACTUAL_USER:$ACTUAL_USER" "$sablier_dir"
fi
# Copy stack files
cp "$REPO_DIR/docker-compose/sablier/docker-compose.yml" "$sablier_dir/"
cp "$REPO_DIR/.env" "$sablier_dir/"
sudo chown "$ACTUAL_USER:$ACTUAL_USER" "$sablier_dir/docker-compose.yml"
sudo chown "$ACTUAL_USER:$ACTUAL_USER" "$sablier_dir/.env"
# Localize the docker-compose file
localize_compose_labels "$sablier_dir/docker-compose.yml"
# Deploy
cd "$sablier_dir"
run_cmd docker compose up -d
log_success "Sablier stack deployed at $sablier_dir"
}
# Show help function
show_help() {
echo "EZ-Homelab Setup & Deployment Script"
@@ -1611,15 +1753,42 @@ main() {
;;
2)
log_info "Selected: Deploy Core Server"
# Check Docker first
if ! check_docker_installed; then
echo ""
log_error "Docker must be installed before deploying core server"
log_info "Please run Option 1 (Install Prerequisites) first"
echo ""
read -p "Press Enter to return to main menu..."
continue
fi
DEPLOY_CORE=true
DEPLOY_INFRASTRUCTURE=true
DEPLOY_DASHBOARDS=true
SETUP_STACKS=true
DEPLOY_REMOTE_SERVER=false
# Set required variables for core deployment
set_required_vars_for_deployment "core"
break
;;
3)
log_info "Selected: Deploy Additional Server"
echo ""
# Check Docker first
if ! check_docker_installed; then
echo ""
log_error "Docker must be installed before deploying remote server"
log_info "Please run Option 1 (Install Prerequisites) first"
echo ""
read -p "Press Enter to return to main menu..."
continue
fi
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 ""
@@ -1632,9 +1801,14 @@ main() {
continue
fi
DEPLOY_CORE=false
DEPLOY_INFRASTRUCTURE=true
DEPLOY_INFRASTRUCTURE=false
DEPLOY_DASHBOARDS=false
SETUP_STACKS=true
SETUP_STACKS=false
DEPLOY_REMOTE_SERVER=true
# Set required variables for remote deployment
set_required_vars_for_deployment "remote"
break
;;
4)
@@ -1664,6 +1838,22 @@ main() {
# Prepare deployment environment
prepare_deployment
# Handle remote server deployment separately
if [ "$DEPLOY_REMOTE_SERVER" = true ]; then
# Prompt for configuration values
validate_and_prompt_variables
# Save configuration
save_env_file
# Deploy remote server
deploy_remote_server
echo ""
log_success "Remote server deployment complete!"
exit 0
fi
# Prompt for configuration values
validate_and_prompt_variables