rvs-prxy/deploy-rvs-prxy.sh

4052 lines
163 KiB
Bash
Executable File

#!/bin/bash
# deploy-rvs-prxy.sh - Automated deployment script for Reverse Proxy setup
# This script supports four modes:
# 1) Setup Reverse Proxy (Traefik or FRP Tunnel with Traefik)
# 2) Setup New Domain into existing reverse proxy (routes to a local Docker container)
# 3) Setup Port Exposure (exposes a host port to a container port)
# 4) Setup External Service Domain (routes a domain to an external web service URL)
# This script runs on the same computer as the reverse proxy
set -e # Exit on any error
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Global variables
THIS_INSTANCE=""
SELECTED_NETWORK=""
INTERNAL_SERVICE_PORT=""
SERVER_ADDR=""
SERVER_PORT="16999"
RAD_REMOTE_PORT=""
SSH_REMOTE_PORT=""
WEB_REMOTE_PORT=""
DOMAIN_NAME=""
# FRP Mode variables
FRP=true
# Instance configuration variables
INSTANCE_NAME="traefik" # Default container name, can be overridden in .env.rvs-prxy
DOCKER_NETWORK="${DOCKER_NETWORK}" # Default Docker network name, can be overridden in .env.rvs-prxy
DASHBOARD_PORT="18080" # Default dashboard port, can be overridden in .env.rvs-prxy
# Extra entrypoints beyond default web (:80/tcp) and websecure (:443/tcp)
# Format: comma-separated list of name=address
# Example: "quic=:443/udp, custom-tcp=:8888"
# Address format: :port or :port/protocol (default protocol: tcp)
EXTRA_ENTRYPOINTS=""
# Target server variables (for Traefik mode - now running locally)
LOCAL_SERVICE_NAME=""
LOCAL_SERVICE_PORT=""
LOCAL_DOCKER_NETWORK=""
# Deployment mode: "proxy" (setup reverse proxy), "domain" (setup new domain),
# "port" (expose host port), or "external" (route a domain to an external service)
DEPLOY_MODE=""
# Domain configuration file pattern: .env-your-domain-com
DOMAIN_ENV_FILE=""
# Port configuration file pattern: .env-port-<port_number>
PORT_ENV_FILE=""
# External service configuration file pattern: .env-ext-your-domain-com
EXT_ENV_FILE=""
# External service target (Traefik mode only) - upstream URL of the remote web service
EXTERNAL_URL=""
# Array to store all domain configuration files for batch processing
declare -a DOMAIN_ENV_FILES=()
# Array to store all port configuration files for batch processing
declare -a PORT_ENV_FILES=()
# Array to store all external service configuration files for batch processing
declare -a EXT_ENV_FILES=()
# Function to load environment variables from .env.rvs-prxy files
load_env() {
local proxy_env_loaded=false
local domain_env_loaded=false
# Step 1: Load reverse proxy instance configuration from .env.rvs-prxy
if [ -f ".env.rvs-prxy" ]; then
echo -e "${BLUE}[INFO]${NC} Loading reverse proxy config from .env.rvs-prxy" >&2
source .env.rvs-prxy
proxy_env_loaded=true
elif [ -f "$(dirname "$0")/.env.rvs-prxy" ]; then
echo -e "${BLUE}[INFO]${NC} Loading reverse proxy config from $(dirname "$0")/.env.rvs-prxy" >&2
source "$(dirname "$0")/.env.rvs-prxy"
proxy_env_loaded=true
fi
# Step 2: Collect all domain-specific configuration files (.env-* pattern)
# These files contain domain-specific settings like DOMAIN_NAME, LOCAL_SERVICE_NAME, LOCAL_SERVICE_PORT
# Pattern matches any domain format: .env-example-com, .env-sub-domain-org-hk, etc.
# All found files will be processed in batch mode
for env_file in .env-*; do
# Skip the main reverse proxy config file .env.rvs-prxy
if [ "$env_file" = ".env.rvs-prxy" ]; then
continue
fi
# Skip port configuration files (.env-port-*)
if [[ "$env_file" == .env-port-* ]]; then
continue
fi
# Skip external service configuration files (.env-ext-*)
if [[ "$env_file" == .env-ext-* ]]; then
continue
fi
if [ -f "$env_file" ]; then
DOMAIN_ENV_FILES+=("$env_file")
fi
done
# Collect port configuration files (.env-port-* pattern)
# These files contain port-based routing settings: HOST_PORT, CONTAINER_NAME, CONTAINER_PORT
for port_file in .env-port-*; do
if [ -f "$port_file" ]; then
PORT_ENV_FILES+=("$port_file")
fi
done
# Collect external service configuration files (.env-ext-* pattern)
# These files contain external URL routing settings: DOMAIN_NAME, EXTERNAL_URL
for ext_file in .env-ext-*; do
if [ -f "$ext_file" ]; then
EXT_ENV_FILES+=("$ext_file")
fi
done
# Check for legacy .env.port-* files and auto-rename to .env-port-*
for legacy_file in .env.port-*; do
if [ -f "$legacy_file" ]; then
# Extract port number from filename
local port_num="${legacy_file#.env.port-}"
local new_file=".env-port-${port_num}"
log_warn "Legacy port config file detected: $legacy_file"
log_warn "Auto-renaming to new format: $new_file"
mv "$legacy_file" "$new_file"
log_info "File renamed successfully"
# Add to port files list
PORT_ENV_FILES+=("$new_file")
fi
done
# Load the first domain file for initial variable setup (backward compatibility)
if [ ${#DOMAIN_ENV_FILES[@]} -gt 0 ]; then
echo -e "${BLUE}[INFO]${NC} Loading domain config from ${DOMAIN_ENV_FILES[0]}" >&2
source "${DOMAIN_ENV_FILES[0]}"
domain_env_loaded=true
DOMAIN_ENV_FILE="${DOMAIN_ENV_FILES[0]}"
if [ ${#DOMAIN_ENV_FILES[@]} -gt 1 ]; then
log_info "Found ${#DOMAIN_ENV_FILES[@]} domain config files, will process all in batch mode"
fi
fi
# Load the first port config file for initial variable setup
if [ ${#PORT_ENV_FILES[@]} -gt 0 ]; then
echo -e "${BLUE}[INFO]${NC} Loading port config from ${PORT_ENV_FILES[0]}" >&2
source "${PORT_ENV_FILES[0]}"
PORT_ENV_FILE="${PORT_ENV_FILES[0]}"
if [ ${#PORT_ENV_FILES[@]} -gt 1 ]; then
log_info "Found ${#PORT_ENV_FILES[@]} port config files, will process all in batch mode"
fi
fi
# Load the first external service config file for initial variable setup
if [ ${#EXT_ENV_FILES[@]} -gt 0 ]; then
echo -e "${BLUE}[INFO]${NC} Loading external service config from ${EXT_ENV_FILES[0]}" >&2
source "${EXT_ENV_FILES[0]}"
EXT_ENV_FILE="${EXT_ENV_FILES[0]}"
if [ ${#EXT_ENV_FILES[@]} -gt 1 ]; then
log_info "Found ${#EXT_ENV_FILES[@]} external service config files, will process all in batch mode"
fi
fi
# Step 3: Detect proxy mode from running containers
# This allows domain setup even without .env.rvs-prxy file, by detecting running proxy
# IMPORTANT: Running container detection takes precedence over .env settings
local proxy_running=false
local detected_frp_mode=""
# Check if Traefik is running (takes precedence)
if docker ps --filter name=^${INSTANCE_NAME}$ --format '{{.Names}}' | grep -q "^traefik$"; then
proxy_running=true
detected_frp_mode="false"
log_info "Detected running Traefik reverse proxy"
fi
# Check if FRP client is running (look for any *-frpc container)
local frpc_containers=$(docker ps --filter name="-frpc$" --format '{{.Names}}' 2>/dev/null || true)
if [ -n "$frpc_containers" ]; then
# FRP container found, extract instance name from first container
local first_frpc=$(echo "$frpc_containers" | head -1)
INSTANCE_NAME="${first_frpc%-frpc}"
proxy_running=true
# Only set FRP mode if Traefik is not detected (Traefik takes precedence)
if [ -z "$detected_frp_mode" ]; then
detected_frp_mode="true"
log_info "Detected running FRP client container: ${first_frpc} (instance: ${INSTANCE_NAME})"
# Load FRP configuration from volume if .env not loaded
if [ "$proxy_env_loaded" = false ]; then
load_frp_config_from_volume "$INSTANCE_NAME"
fi
fi
fi
# ALWAYS use detected FRP mode when proxy is running (override .env settings)
# This ensures the script uses the actual running proxy mode, not stale config
if [ "$proxy_running" = true ] && [ -n "$detected_frp_mode" ]; then
FRP="$detected_frp_mode"
log_info "Auto-detected FRP mode: $FRP (from running container)"
fi
# Determine deployment mode based on what configuration files exist and running proxy
if [ ${#PORT_ENV_FILES[@]} -gt 0 ]; then
# Port config exists - setup port-based routing
DEPLOY_MODE="port"
log_info "Deployment mode: Setup Port Exposure (found port config: ${PORT_ENV_FILE})"
elif [ ${#EXT_ENV_FILES[@]} -gt 0 ]; then
# External service config exists - route domain to external URL
DEPLOY_MODE="external"
log_info "Deployment mode: Setup External Service Domain (found external service config: ${EXT_ENV_FILE})"
elif [ "$domain_env_loaded" = true ]; then
# Domain config exists - setup new domain
DEPLOY_MODE="domain"
log_info "Deployment mode: Setup New Domain (found domain config: ${DOMAIN_ENV_FILE})"
elif [ "$proxy_running" = true ]; then
# Proxy is running (detected from containers) - setup new domain via wizard if no domain config
DEPLOY_MODE="domain_wizard"
log_info "Deployment mode: Setup New Domain (proxy running, no domain config)"
elif [ "$proxy_env_loaded" = true ]; then
# Proxy config exists but proxy not running - setup proxy
DEPLOY_MODE="proxy"
log_info "Deployment mode: Setup Reverse Proxy (config exists, proxy not running)"
else
# No configuration files found and no proxy running - run interactive wizard
echo -e "${YELLOW}[INFO]${NC} No configuration file found." >&2
interactive_setup_wizard
# Source the newly created files
if [ "$DEPLOY_MODE" = "domain" ] && [ -n "$DOMAIN_ENV_FILE" ]; then
source "$DOMAIN_ENV_FILE"
elif [ "$DEPLOY_MODE" = "port" ] && [ -n "$PORT_ENV_FILE" ]; then
source "$PORT_ENV_FILE"
elif [ "$DEPLOY_MODE" = "external" ] && [ -n "$EXT_ENV_FILE" ]; then
source "$EXT_ENV_FILE"
else
source .env.rvs-prxy
fi
fi
# Validate required parameters based on mode (skip validation for domain_wizard - will be validated after wizard)
if [ "$DEPLOY_MODE" = "port" ]; then
# Port config file exists - validate it
if [ -z "$HOST_PORT" ]; then
log_error "HOST_PORT is required for port exposure mode"
log_error "Please set HOST_PORT in your .env-port-<port> file"
exit 1
fi
if [ -z "$CONTAINER_NAME" ] || [ -z "$CONTAINER_PORT" ]; then
log_error "CONTAINER_NAME and CONTAINER_PORT are required for port exposure"
log_error "Please set these in your .env-port-<port> file"
exit 1
fi
elif [ "$DEPLOY_MODE" = "domain" ]; then
# Domain config file exists - validate it
if [ -z "$DOMAIN_NAME" ]; then
log_error "DOMAIN_NAME is required for domain setup mode"
log_error "Please set DOMAIN_NAME in your .env-<domain> file"
exit 1
fi
if [ -z "$LOCAL_SERVICE_NAME" ] || [ -z "$LOCAL_SERVICE_PORT" ]; then
log_error "LOCAL_SERVICE_NAME and LOCAL_SERVICE_PORT are required for domain setup"
log_error "Please set these in your .env-<domain> file"
exit 1
fi
elif [ "$DEPLOY_MODE" = "external" ]; then
# External service config file exists - validate it
if [ -z "$DOMAIN_NAME" ]; then
log_error "DOMAIN_NAME is required for external service setup mode"
log_error "Please set DOMAIN_NAME in your .env-ext-<domain> file"
exit 1
fi
if [ -z "$EXTERNAL_URL" ]; then
log_error "EXTERNAL_URL is required for external service setup"
log_error "Please set it in your .env-ext-<domain> file (e.g. https://app.example.com/)"
exit 1
fi
elif [ "$DEPLOY_MODE" = "proxy" ]; then
# For proxy setup mode, validate proxy-specific settings
if [ "$FRP" = false ]; then
# Traefik mode - check for Traefik-specific settings
if [ -z "$TRAEFIK_VER" ]; then
log_error "TRAEFIK_VER is required for Traefik proxy setup"
log_error "Please set TRAEFIK_VER in your .env.rvs-prxy file"
exit 1
fi
if [ -z "$LETSENCRYPT_EMAIL" ]; then
log_error "LETSENCRYPT_EMAIL is required for Traefik proxy setup"
log_error "Please set LETSENCRYPT_EMAIL in your .env.rvs-prxy file"
exit 1
fi
else
# FRP mode - check for FRP-specific settings
if [ -z "$FRP_SERVER_ADDR" ]; then
log_error "FRP_SERVER_ADDR is required for FRP proxy setup"
log_error "Please set FRP_SERVER_ADDR in your .env.rvs-prxy file"
exit 1
fi
fi
fi
# Note: domain_wizard mode skips validation here - wizard will collect the required values
}
# Logging functions
log_info() {
echo -e "${GREEN}[INFO]${NC} $1" >&2
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1" >&2
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1" >&2
}
log_debug() {
if [ "$DEBUG" = "true" ]; then
echo -e "${BLUE}[DEBUG]${NC} $1" >&2
fi
}
# Function to save answered values to .env.rvs-prxy file
save_to_env() {
local var_name="$1"
local var_value="$2"
local target_file="${3:-.env.rvs-prxy}"
# Create or update .env.rvs-prxy file with the new value
if [ -f "$target_file" ]; then
# Check if variable already exists
if grep -q "^${var_name}=" "$target_file"; then
# Update existing variable
sed -i "s/^${var_name}=.*/${var_name}=${var_value}/" "$target_file"
else
# Add new variable
echo "${var_name}=${var_value}" >> "$target_file"
fi
else
# Create new .env.rvs-prxy file
echo "${var_name}=${var_value}" >> "$target_file"
fi
}
# Function to save domain-specific configuration
save_domain_env() {
local domain_name="$1"
local service_name="$2"
local service_port="$3"
# Convert domain name to filename format: your-domain-com -> .env-your-domain-com
local domain_file=".env-$(echo "$domain_name" | sed 's/\./-/g')"
echo -e "${BLUE}[INFO]${NC} Saving domain configuration to $domain_file" >&2
# Create domain-specific env file
cat > "$domain_file" << EOF
# Domain configuration for ${domain_name}
# Generated by deploy-rvs-prxy.sh on $(date)
DOMAIN_NAME="${domain_name}"
LOCAL_SERVICE_NAME="${service_name}"
LOCAL_SERVICE_PORT="${service_port}"
EOF
echo -e "${GREEN}[INFO]${NC} Domain configuration saved to $domain_file" >&2
DOMAIN_ENV_FILE="$domain_file"
}
# Function to save port exposure configuration to .env-port-<port> file
save_port_env() {
local host_port="$1"
local container_name="$2"
local container_port="$3"
# Create port-specific filename: .env-port-<port>
local port_file=".env-port-${host_port}"
echo -e "${BLUE}[INFO]${NC} Saving port configuration to $port_file" >&2
# Create port-specific env file
cat > "$port_file" << EOF
# Port-based routing configuration
# Generated by deploy-rvs-prxy.sh on $(date)
HOST_PORT="${host_port}"
CONTAINER_NAME="${container_name}"
CONTAINER_PORT="${container_port}"
EOF
echo -e "${GREEN}[INFO]${NC} Port configuration saved to $port_file" >&2
PORT_ENV_FILE="$port_file"
}
# Function to save external service configuration to .env-ext-<domain> file
save_ext_env() {
local domain_name="$1"
local external_url="$2"
# Convert domain name to filename format: your-domain-com -> .env-ext-your-domain-com
# Sub-path (/) is also converted to keep the filename flat
local domain_file=".env-ext-$(echo "$domain_name" | sed 's/\./-/g; s/\//-/g')"
echo -e "${BLUE}[INFO]${NC} Saving external service configuration to $domain_file" >&2
# Create external service-specific env file
cat > "$domain_file" << EOF
# External service configuration for ${domain_name}
# Generated by deploy-rvs-prxy.sh on $(date)
# Traefik terminates TLS for this domain and proxies to EXTERNAL_URL (origin Host header)
DOMAIN_NAME="${domain_name}"
EXTERNAL_URL="${external_url}"
EOF
echo -e "${GREEN}[INFO]${NC} External service configuration saved to $domain_file" >&2
EXT_ENV_FILE="$domain_file"
}
# Function to parse EXTERNAL_URL into scheme/host/port/base-path components
# Sets: EXT_SCHEME, EXT_HOST, EXT_PORT, EXT_BASE_PATH
parse_external_url() {
EXT_SCHEME=""
EXT_HOST=""
EXT_PORT=""
EXT_BASE_PATH=""
local url="${EXTERNAL_URL# }"
url="${url% }"
case "$url" in
https://*) EXT_SCHEME="https" ;;
http://*) EXT_SCHEME="http" ;;
*) return 1 ;;
esac
# Strip scheme
local remainder="${url#*://}"
# Split host(+port) from path
local host_part="${remainder%%/*}"
local path_part=""
if [[ "$remainder" == *"/"* ]]; then
path_part="/${remainder#*/}"
fi
# Split host and port
if [[ "$host_part" == *:* ]]; then
EXT_HOST="${host_part%%:*}"
EXT_PORT="${host_part##*:}"
else
EXT_HOST="$host_part"
fi
# Validate host
if [ -z "$EXT_HOST" ]; then
return 1
fi
# Validate port (numeric, 1-65535) or default by scheme
if [ -z "$EXT_PORT" ]; then
if [ "$EXT_SCHEME" = "https" ]; then
EXT_PORT="443"
else
EXT_PORT="80"
fi
elif ! [[ "$EXT_PORT" =~ ^[0-9]+$ ]] || [ "$EXT_PORT" -lt 1 ] || [ "$EXT_PORT" -gt 65535 ]; then
return 1
fi
# Normalise base path (strip trailing slash, keep leading slash)
if [ "$path_part" != "/" ]; then
EXT_BASE_PATH="${path_part%/}"
fi
return 0
}
# Function to warn when the external host cannot be resolved (non-fatal)
check_external_service_reachable() {
log_info "Checking external service ${EXT_SCHEME}://${EXT_HOST}:${EXT_PORT} ..."
local resolved=""
if command -v getent >/dev/null 2>&1; then
resolved=$(getent hosts "$EXT_HOST" 2>/dev/null || true)
fi
if [ -n "$resolved" ]; then
log_info "External host ${EXT_HOST} resolves to: $(echo "$resolved" | awk '{print $1}' | head -3 | tr '\n' ' ')"
return 0
fi
log_warn "Could not resolve external host '${EXT_HOST}' from this server."
log_warn "Traefik resolves and connects to it from inside the container - DNS there may still work."
log_warn "If the domain does not load, verify ${EXTERNAL_URL} is reachable and publicly resolvable."
return 0
}
# Function to read from terminal instead of pipe
read_terminal() {
local prompt="$1"
local default_value="$2"
# Read from terminal (/dev/tty) instead of stdin
read -p "$prompt" input_name </dev/tty
echo "${input_name:-$default_value}"
}
# Interactive setup wizard for missing .env.rvs-prxy file
interactive_setup_wizard() {
echo -e "${BLUE}=============================================${NC}" >&2
echo -e "${GREEN}Welcome to Deploy Reverse Proxy Setup Wizard${NC}" >&2
echo -e "${BLUE}=============================================${NC}" >&2
echo "" >&2
# Check if reverse proxy already exists
local proxy_exists=false
if docker ps -a --filter name=^${INSTANCE_NAME}$ --format '{{.Names}}' | grep -q "^traefik$"; then
proxy_exists=true
fi
if [ "$proxy_exists" = true ]; then
# Reverse proxy exists, ask whether to setup new domain or port exposure
echo -e "${YELLOW}Existing reverse proxy (Traefik) detected.${NC}" >&2
echo "" >&2
# Ask what to setup
echo -e "${BLUE}--- What would you like to setup? ---${NC}" >&2
echo -e " 1) ${GREEN}Setup New Domain${NC} - Route traffic based on domain name" >&2
echo -e " 2) ${GREEN}Setup Port Exposure${NC} - Expose host port to container port" >&2
echo -e " 3) ${GREEN}Setup External Service Domain${NC} - Route a domain to an external web service (URL)" >&2
echo "" >&2
while true; do
setup_choice=$(read_terminal "Select option [1-3] (default: 1 for Domain): " "1")
case "$setup_choice" in
1|"")
# Setup New Domain
echo -e "${GREEN}Selected: Setup New Domain${NC}" >&2
echo "" >&2
echo -e "${BLUE}--- Setup New Domain ---${NC}" >&2
echo -e "Let's configure a new domain for your existing reverse proxy." >&2
echo "" >&2
# Get domain name
echo -e "Enter the domain name for this service:" >&2
while [ -z "$DOMAIN_NAME" ]; do
DOMAIN_NAME=$(read_terminal "Domain (e.g., app.example.com): " "")
if [ -z "$DOMAIN_NAME" ]; then
echo -e "${RED}Domain name cannot be empty.${NC}" >&2
fi
done
# Get service name
echo -e "Enter your Docker application container name:" >&2
while [ -z "$LOCAL_SERVICE_NAME" ]; do
LOCAL_SERVICE_NAME=$(read_terminal "Service name (e.g., my-app): " "")
if [ -z "$LOCAL_SERVICE_NAME" ]; then
echo -e "${RED}Service name cannot be empty.${NC}" >&2
fi
done
# Get service port(s)
echo -e "Enter your application's internal port(s):" >&2
echo -e " Single port: 80" >&2
echo -e " Multiple: 80,4443/udp (comma-separated, protocol defaults to tcp)" >&2
while [ -z "$LOCAL_SERVICE_PORT" ]; do
LOCAL_SERVICE_PORT=$(read_terminal "Service port (default: 3000): " "3000")
if [ -z "$LOCAL_SERVICE_PORT" ]; then
LOCAL_SERVICE_PORT="3000"
elif ! [[ "$LOCAL_SERVICE_PORT" =~ ^[0-9]+(/tcp|/udp)?(,[0-9]+(/tcp|/udp)?)*$ ]]; then
echo -e "${RED}Invalid format. Use comma-separated port/protocol, e.g. 80 or 80,4443/udp${NC}" >&2
LOCAL_SERVICE_PORT=""
fi
done
# Save to domain-specific .env.rvs-prxy file
save_domain_env "$DOMAIN_NAME" "$LOCAL_SERVICE_NAME" "$LOCAL_SERVICE_PORT"
# Load FRP setting from existing .env if it exists
if [ -f ".env.rvs-prxy" ]; then
source .env.rvs-prxy
else
# Set default FRP mode if .env doesn't exist
FRP=false
fi
DEPLOY_MODE="domain"
log_info "Deployment mode: Setup New Domain (interactive setup)"
break
;;
2)
# Setup Port Exposure
echo -e "${GREEN}Selected: Setup Port Exposure${NC}" >&2
echo "" >&2
echo -e "${BLUE}--- Setup Port Exposure ---${NC}" >&2
echo -e "Let's expose a host port to connect to a container port." >&2
echo "" >&2
# Get host port to expose
echo -e "Enter the host port number to expose:" >&2
while [ -z "$HOST_PORT" ]; do
HOST_PORT=$(read_terminal "Host port (e.g., 18081): " "")
if [ -z "$HOST_PORT" ]; then
echo -e "${RED}Port number cannot be empty.${NC}" >&2
elif ! [[ "$HOST_PORT" =~ ^[0-9]+$ ]] || [ "$HOST_PORT" -lt 1 ] || [ "$HOST_PORT" -gt 65535 ]; then
echo -e "${RED}Invalid port. Please enter a number between 1 and 65535.${NC}" >&2
HOST_PORT=""
fi
done
# Get container name
echo -e "Enter your Docker container name:" >&2
while [ -z "$CONTAINER_NAME" ]; do
CONTAINER_NAME=$(read_terminal "Container name (e.g., my-nginx): " "")
if [ -z "$CONTAINER_NAME" ]; then
echo -e "${RED}Container name cannot be empty.${NC}" >&2
fi
done
# Get container port
echo -e "Enter your container's internal port:" >&2
while [ -z "$CONTAINER_PORT" ]; do
CONTAINER_PORT=$(read_terminal "Container port (e.g., 80): " "")
if [ -z "$CONTAINER_PORT" ]; then
echo -e "${RED}Port number cannot be empty.${NC}" >&2
elif ! [[ "$CONTAINER_PORT" =~ ^[0-9]+$ ]] || [ "$CONTAINER_PORT" -lt 1 ] || [ "$CONTAINER_PORT" -gt 65535 ]; then
echo -e "${RED}Invalid port. Please enter a number between 1 and 65535.${NC}" >&2
CONTAINER_PORT=""
fi
done
# Save to port-specific .env.rvs-prxy file
save_port_env "$HOST_PORT" "$CONTAINER_NAME" "$CONTAINER_PORT"
# Load FRP setting from existing .env if it exists
if [ -f ".env.rvs-prxy" ]; then
source .env.rvs-prxy
else
# Set default FRP mode if .env doesn't exist
FRP=false
fi
DEPLOY_MODE="port"
log_info "Deployment mode: Setup Port Exposure (interactive setup)"
break
;;
3)
# Setup External Service Domain
echo -e "${GREEN}Selected: Setup External Service Domain${NC}" >&2
echo "" >&2
echo -e "${BLUE}--- Setup External Service Domain ---${NC}" >&2
echo -e "Let's route a new domain to an external web service." >&2
echo -e "No local Docker container is required - Traefik proxies to the URL below." >&2
echo "" >&2
# Get domain name (optional sub-path supported, e.g. app.example.com/api)
echo -e "Enter the domain name for this external service:" >&2
while [ -z "$DOMAIN_NAME" ]; do
DOMAIN_NAME=$(read_terminal "Domain (e.g., app.example.com or app.example.com/api): " "")
if [ -z "$DOMAIN_NAME" ]; then
echo -e "${RED}Domain name cannot be empty.${NC}" >&2
fi
done
# Get external service URL
echo -e "Enter the external web service URL:" >&2
while [ -z "$EXTERNAL_URL" ]; do
EXTERNAL_URL=$(read_terminal "External URL (e.g., https://cornersendbox-impact-vms.lovable.app/): " "")
if [ -z "$EXTERNAL_URL" ]; then
echo -e "${RED}External URL cannot be empty.${NC}" >&2
elif ! [[ "$EXTERNAL_URL" =~ ^https?://[^[:space:]]+$ ]]; then
echo -e "${RED}Invalid URL. It must start with http:// or https://${NC}" >&2
echo -e "${RED}Example: https://cornersendbox-impact-vms.lovable.app/${NC}" >&2
EXTERNAL_URL=""
fi
done
# Save to external service .env-ext-<domain> file
save_ext_env "$DOMAIN_NAME" "$EXTERNAL_URL"
# Load FRP setting from existing .env if it exists
if [ -f ".env.rvs-prxy" ]; then
source .env.rvs-prxy
else
# Set default FRP mode if .env doesn't exist
FRP=false
fi
DEPLOY_MODE="external"
log_info "Deployment mode: Setup External Service Domain (interactive setup)"
break
;;
*)
echo -e "${RED}Invalid choice. Please enter 1, 2 or 3.${NC}" >&2
;;
esac
done
else
# No reverse proxy exists, setup new one
echo -e "${YELLOW}No configuration file found. Let's set up your deployment.${NC}" >&2
echo "" >&2
# Step 1: Select deployment mode
echo -e "${BLUE}--- Step 1: Deployment Mode ---${NC}" >&2
echo -e "Choose your deployment mode:" >&2
echo -e " 1) ${GREEN}Traefik Mode${NC} - Local reverse proxy (runs on same server as your application)" >&2
echo -e " 2) ${GREEN}FRP Mode${NC} - Remote proxy via FRP tunnel" >&2
echo "" >&2
while true; do
mode_choice=$(read_terminal "Select mode [1-2] (default: 1 for Traefik): " "1")
case "$mode_choice" in
1|"")
FRP=false
echo -e "${GREEN}Selected: Traefik Mode${NC}" >&2
break
;;
2)
FRP=true
echo -e "${GREEN}Selected: FRP Mode${NC}" >&2
break
;;
*)
echo -e "${RED}Invalid choice. Please enter 1 or 2.${NC}" >&2
;;
esac
done
save_to_env "FRP" "$FRP" ".env.rvs-prxy"
echo "" >&2
# Step 2: Instance configuration
echo -e "${BLUE}--- Step 2: Instance Configuration ---${NC}" >&2
echo "" >&2
# Instance name
INSTANCE_NAME=$(read_terminal "Instance name / container name (default: traefik): " "traefik")
save_to_env "INSTANCE_NAME" "$INSTANCE_NAME" ".env.rvs-prxy"
echo "" >&2
# Docker network
DOCKER_NETWORK=$(read_terminal "Docker network name (default: traefik-public): " "traefik-public")
save_to_env "DOCKER_NETWORK" "$DOCKER_NETWORK" ".env.rvs-prxy"
echo "" >&2
# Dashboard port
DASHBOARD_PORT=$(read_terminal "Traefik dashboard port (default: 18080): " "18080")
save_to_env "DASHBOARD_PORT" "$DASHBOARD_PORT" ".env.rvs-prxy"
echo "" >&2
# Step 3: Mode-specific configuration
if [ "$FRP" = false ]; then
# Traefik Mode configuration - Reverse Proxy Instance settings only
echo -e "${BLUE}--- Step 3: Traefik Reverse Proxy Configuration ---${NC}" >&2
echo "" >&2
echo -e "${YELLOW}Note: This configures the Traefik reverse proxy instance itself.${NC}" >&2
echo -e "${YELLOW}Domain configuration will be done separately later.${NC}" >&2
echo "" >&2
# Traefik version
TRAEFIK_VER=$(read_terminal "Traefik version (default: v3.6.9): " "v3.6.9")
save_to_env "TRAEFIK_VER" "$TRAEFIK_VER" ".env.rvs-prxy"
echo "" >&2
# Let's Encrypt email
echo -e "Enter email for Let's Encrypt SSL certificates:" >&2
while [ -z "$LETSENCRYPT_EMAIL" ]; do
LETSENCRYPT_EMAIL=$(read_terminal "Email (e.g., admin@example.com): " "")
if [ -z "$LETSENCRYPT_EMAIL" ]; then
echo -e "${RED}Email cannot be empty.${NC}" >&2
fi
done
save_to_env "LETSENCRYPT_EMAIL" "$LETSENCRYPT_EMAIL" ".env.rvs-prxy"
echo "" >&2
# Extra entrypoints (e.g., QUIC)
echo -e "Extra Traefik entrypoints (optional):" >&2
echo -e " Format: name=:port/protocol (comma-separated)" >&2
echo -e " Example: quic=:443/udp enables HTTP/3 QUIC on port 443/udp" >&2
EXTRA_ENTRYPOINTS=$(read_terminal "Extra entrypoints (leave empty for none): " "")
if [ -n "$EXTRA_ENTRYPOINTS" ]; then
save_to_env "EXTRA_ENTRYPOINTS" "$EXTRA_ENTRYPOINTS" ".env.rvs-prxy"
fi
echo "" >&2
# Optional: Debug mode
DEBUG_MODE=$(read_terminal "Enable debug mode? (y/N): " "n")
if [[ "$DEBUG_MODE" =~ ^[Yy]$ ]]; then
save_to_env "DEBUG" "true" ".env.rvs-prxy"
else
save_to_env "DEBUG" "false" ".env.rvs-prxy"
fi
echo "" >&2
else
# FRP Mode configuration - Reverse Proxy Instance settings only
echo -e "${BLUE}--- Step 3: FRP Tunnel Configuration ---${NC}" >&2
echo "" >&2
echo -e "${YELLOW}Note: This configures the FRP tunnel instance itself.${NC}" >&2
echo -e "${YELLOW}Domain configuration will be done separately later.${NC}" >&2
echo "" >&2
# FRP server address
echo -e "Enter FRP server address:" >&2
while [ -z "$FRP_SERVER_ADDR" ]; do
FRP_SERVER_ADDR=$(read_terminal "Server IP/hostname: " "")
if [ -z "$FRP_SERVER_ADDR" ]; then
echo -e "${RED}Server address cannot be empty.${NC}" >&2
fi
done
save_to_env "FRP_SERVER_ADDR" "$FRP_SERVER_ADDR" ".env.rvs-prxy"
echo "" >&2
# FRP server port
FRP_SERVER_PORT=$(read_terminal "FRP server port (default: 16999): " "16999")
save_to_env "FRP_SERVER_PORT" "$FRP_SERVER_PORT" ".env.rvs-prxy"
echo "" >&2
# Instance name
INSTANCE_NAME=$(read_terminal "Instance name (default: auto-generated): " "")
if [ -n "$INSTANCE_NAME" ]; then
save_to_env "INSTANCE_NAME" "$INSTANCE_NAME" ".env.rvs-prxy"
fi
echo "" >&2
fi
# Step 3: Confirm setup completion
echo -e "${BLUE}--- Step 3: Setup Complete ---${NC}" >&2
echo "" >&2
echo -e "${GREEN}Reverse proxy instance configuration saved to .env${NC}" >&2
echo -e "${YELLOW}Next: Run the script again to setup your first domain.${NC}" >&2
echo "" >&2
DEPLOY_MODE="proxy"
fi
echo -e "${GREEN}=============================================${NC}" >&2
echo -e "${GREEN}Setup Complete!${NC}" >&2
if [ "$DEPLOY_MODE" = "domain" ]; then
echo -e "${GREEN}Domain configuration saved to ${DOMAIN_ENV_FILE}${NC}" >&2
elif [ "$DEPLOY_MODE" = "port" ]; then
echo -e "${GREEN}Port configuration saved to ${PORT_ENV_FILE}${NC}" >&2
else
echo -e "${GREEN}Configuration saved to .env.rvs-prxy${NC}" >&2
fi
echo -e "${GREEN}=============================================${NC}" >&2
echo "" >&2
echo -e "${YELLOW}You can edit these settings later in the configuration file.${NC}" >&2
echo "" >&2
}
# Prompt user for instance name
prompt_instance_name() {
local system_name="$1"
local default_name="${system_name#setup-}-$(date +%Y%m%d-%H%M%S)"
# Check if INSTANCE_NAME is set from environment
if [ -n "$INSTANCE_NAME" ]; then
THIS_INSTANCE="$INSTANCE_NAME"
log_info "Using environment variable for instance name: ${THIS_INSTANCE}"
else
# Scan Docker volumes for rad-git-sshd-.*-frpc to find existing instances
local instances=()
local volume_list=$(docker volume ls --format '{{.Name}}' | grep '^rad-git-sshd-.*-frpc' || true)
if [ -n "$volume_list" ]; then
for volume in $volume_list; do
# Extract instance name from volume name (everything after first dash)
local volume_instance=$(echo "$volume" | sed 's/^rad-git-sshd-\(.*\)-frpc$/\1/')
# Validate the instance name
if [[ "$volume_instance" =~ ^[a-zA-Z0-9_-]+$ ]]; then
instances+=("$volume_instance")
fi
done
fi
echo -e "${BLUE}=== Instance Setup ===${NC}" >&2
echo -e "System: ${GREEN}${system_name}${NC}" >&2
if [ ${#instances[@]} -eq 1 ]; then
THIS_INSTANCE="${instances[0]}"
log_info "Found single existing instance: ${THIS_INSTANCE}"
elif [ ${#instances[@]} -gt 1 ]; then
echo -e "${YELLOW}Existing instances found:${NC}" >&2
for i in "${!instances[@]}"; do
echo -e "${BLUE}$((i+1)).${NC} ${instances[i]}" >&2
done
echo -e "Enter number to select existing instance, or press Enter for new default: ${YELLOW}${default_name}${NC}" >&2
local selection
selection=$(read_terminal "Selection: " "")
if [ -n "$selection" ] && [[ "$selection" =~ ^[0-9]+$ ]] && [ "$selection" -ge 1 ] && [ "$selection" -le ${#instances[@]} ]; then
THIS_INSTANCE="${instances[$((selection-1))]}"
else
THIS_INSTANCE="$default_name"
fi
else
THIS_INSTANCE="$default_name"
fi
# Allow user to override
input_name=$(read_terminal "Enter instance name [${THIS_INSTANCE}]: " "${THIS_INSTANCE}")
THIS_INSTANCE="${input_name:-${THIS_INSTANCE}}"
# Save to env file if not already set
if [ -z "$INSTANCE_NAME" ]; then
save_to_env "INSTANCE_NAME" "$THIS_INSTANCE"
fi
fi
# Validate instance name (alphanumeric, hyphens, underscores)
if [[ "$THIS_INSTANCE" =~ ^[a-zA-Z0-9_-]+$ ]]; then
log_info "Using instance name: ${THIS_INSTANCE}"
else
log_error "Invalid instance name. Only letters, numbers, hyphens, and underscores are allowed." >&2
exit 1
fi
echo "" >&2
}
# Function to list and select Docker networks
select_docker_network() {
# Check if DOCKER_NETWORK is set from environment
if [ -n "$DOCKER_NETWORK" ]; then
SELECTED_NETWORK="$DOCKER_NETWORK"
log_info "Using environment variable for Docker network: ${SELECTED_NETWORK}"
return 0
fi
echo -e "${BLUE}=== Docker Network Selection ===${NC}" >&2
# Load existing network selection if available
local existing_network
existing_network=$(load_parameters)
local default_option="1"
# Get list of available Docker networks
local networks
networks=$(docker network ls --format '{{.Name}}' 2>/dev/null || true)
if [ -z "$networks" ]; then
log_warn "No Docker networks found" >&2
return 1
fi
# Convert to array
local network_array=()
while IFS= read -r line; do
network_array+=("$line")
done <<< "$networks"
# Display networks as a list
echo -e "${YELLOW}Available Docker Networks:${NC}" >&2
for i in "${!network_array[@]}"; do
echo -e "${BLUE}$((i+1)).${NC} ${network_array[i]}" >&2
done
# Ask user to select or enter custom network
echo -e "" >&2
echo -e "${YELLOW}Options:${NC}" >&2
echo -e "${BLUE}1-${#network_array[@]}.${NC} Select from list above" >&2
echo -e "${BLUE}C.${NC} Custom network name" >&2
echo -e "${BLUE}N.${NC} No network (default bridge)${NC}" >&2
echo -e "" >&2
# Set default option based on existing network
if [ -n "$existing_network" ]; then
# Check if existing network is in the list
for i in "${!network_array[@]}"; do
if [ "${network_array[i]}" = "$existing_network" ]; then
default_option=$((i+1))
break
fi
done
# If not in list, check if it's a custom network or none
if [ "$default_option" = "1" ] && [ -n "$existing_network" ]; then
if [ "$existing_network" = "none" ]; then
default_option="n"
else
default_option="c"
fi
fi
log_info "Using existing network selection as default: $existing_network"
fi
while true; do
local selection
selection=$(read_terminal "Select option [${default_option}]: " "$default_option")
case "${selection,,}" in
c)
# Custom network name
local custom_network
if [ -n "$existing_network" ] && [ "$existing_network" != "none" ]; then
custom_network=$(read_terminal "Enter custom network name [${existing_network}]: " "$existing_network")
else
custom_network=$(read_terminal "Enter custom network name: " "")
fi
if [ -n "$custom_network" ]; then
SELECTED_NETWORK="$custom_network"
save_to_env "DOCKER_NETWORK" "$SELECTED_NETWORK"
return 0
else
log_error "Network name cannot be empty" >&2
fi
;;
n|"")
# No network (default)
SELECTED_NETWORK=""
save_to_env "DOCKER_NETWORK" "none"
return 0
;;
*)
# Numeric selection
if [[ "$selection" =~ ^[0-9]+$ ]] && [ "$selection" -ge 1 ] && [ "$selection" -le ${#network_array[@]} ]; then
SELECTED_NETWORK="${network_array[$((selection-1))]}"
save_to_env "DOCKER_NETWORK" "$SELECTED_NETWORK"
return 0
else
log_error "Invalid selection. Please choose a valid option." >&2
fi
;;
esac
done
}
# Function to prompt for internal service port
prompt_internal_service_port() {
# Check if INTERNAL_SERVICE_PORT is set from environment
if [ -n "$INTERNAL_SERVICE_PORT" ]; then
log_info "Using environment variable for internal service port: ${INTERNAL_SERVICE_PORT}"
return
fi
echo -e "${BLUE}=== Internal Service Configuration ===${NC}" >&2
while [ -z "$INTERNAL_SERVICE_PORT" ]; do
INTERNAL_SERVICE_PORT=$(read_terminal "Enter internal service port (exposed by Docker container) [3000]: " "3000")
if [ -z "$INTERNAL_SERVICE_PORT" ]; then
log_error "Internal service port cannot be empty" >&2
elif ! [[ "$INTERNAL_SERVICE_PORT" =~ ^[0-9]+$ ]] || [ "$INTERNAL_SERVICE_PORT" -lt 1 ] || [ "$INTERNAL_SERVICE_PORT" -gt 65535 ]; then
log_error "Invalid port number. Please enter a number between 1 and 65535." >&2
INTERNAL_SERVICE_PORT=""
fi
done
save_to_env "INTERNAL_SERVICE_PORT" "$INTERNAL_SERVICE_PORT"
}
# Function to save parameters to .para file
save_parameters() {
local volume_name="${THIS_INSTANCE}-frpc"
local temp_container="temp-para-config-$(date +%s)"
local network_selection="$1"
# Create a temporary directory for the config file
local temp_dir="/tmp/para-config-${THIS_INSTANCE}"
mkdir -p "$temp_dir"
# Create JSON content for .para file
local para_content="{\"network\":\"$network_selection\"}"
# Write the parameters to a temporary file
echo "$para_content" > "${temp_dir}/.para"
# Copy the .para file to the volume
# Create a temporary container to copy the file
if docker run --rm -d --name "$temp_container" -v "$volume_name:/etc/frp" -v "${temp_dir}:/tmp/config" alpine:latest sleep 10 >/dev/null 2>&1; then
docker exec "$temp_container" cp /tmp/config/.para /etc/frp/.para 2>/dev/null || true
docker stop "$temp_container" >/dev/null 2>&1
fi
# Clean up temporary directory
rm -rf "$temp_dir"
}
# Function to load parameters from .para file
load_parameters() {
local volume_name="${THIS_INSTANCE}-frpc"
local temp_container="temp-para-config-$(date +%s)"
local existing_network=""
# Create a temporary container to check for existing config
if docker volume inspect "$volume_name" >/dev/null 2>&1; then
# Create temporary container to access volume
if docker run --rm -d --name "$temp_container" -v "$volume_name:/etc/frp" alpine:latest sleep 10 >/dev/null 2>&1; then
# Check if .para exists
if docker exec "$temp_container" test -f /etc/frp/.para >/dev/null 2>&1; then
# Try to extract existing network selection from JSON
existing_network=$(docker exec "$temp_container" grep '"network"' /etc/frp/.para | cut -d':' -f2 | tr -d ' "{}' 2>/dev/null || true)
if [ -n "$existing_network" ]; then
log_info "Found existing network selection: $existing_network"
fi
fi
# Stop the temporary container
docker stop "$temp_container" >/dev/null 2>&1
fi
fi
echo "$existing_network"
}
# Function to generate frpc.toml configuration (FRP Mode)
generate_frpc_config() {
if [ "$FRP" = false ]; then
log_info "Skipping FRP client configuration as FRP=false"
return 0
fi
echo -e "${BLUE}=== FRP Client Configuration ===${NC}" >&2
# Check if there's an existing frpc.toml in the volume
local volume_name="${THIS_INSTANCE}-frpc"
local temp_container="temp-frpc-config-$(date +%s)"
local existing_server_addr=""
local existing_server_port=""
local existing_proxy1_remote_port=""
local existing_proxy2_remote_port=""
local existing_proxy3_remote_port=""
# Create a temporary container to check for existing config
if docker volume inspect "$volume_name" >/dev/null 2>&1; then
log_info "Found existing volume: $volume_name"
# Create temporary container to access volume
if docker run --rm -d --name "$temp_container" -v "$volume_name:/etc/frp" alpine:latest sleep 10 >/dev/null 2>&1; then
# Check if frpc.toml exists
if docker exec "$temp_container" test -f /etc/frp/frpc.toml >/dev/null 2>&1; then
# Try to extract existing values
existing_server_addr=$(docker exec "$temp_container" grep "^serverAddr" /etc/frp/frpc.toml | cut -d'"' -f2 || true)
existing_server_port=$(docker exec "$temp_container" grep "^serverPort" /etc/frp/frpc.toml | cut -d'=' -f2 | tr -d ' ' || true)
existing_proxy1_remote_port=$(docker exec "$temp_container" grep -A 5 '\[\[proxies\]\]' /etc/frp/frpc.toml | grep "name = \"rad-${THIS_INSTANCE}\"" -A 5 | grep "remotePort" | cut -d'=' -f2 | tr -d ' ' | head -1 || true)
existing_proxy2_remote_port=$(docker exec "$temp_container" grep -A 5 '\[\[proxies\]\]' /etc/frp/frpc.toml | grep "name = \"ssh-${THIS_INSTANCE}\"" -A 5 | grep "remotePort" | cut -d'=' -f2 | tr -d ' ' | head -1 || true)
existing_proxy3_remote_port=$(docker exec "$temp_container" grep -A 5 '\[\[proxies\]\]' /etc/frp/frpc.toml | grep "name = \"web-${THIS_INSTANCE}\"" -A 5 | grep "remotePort" | cut -d'=' -f2 | tr -d ' ' | head -1 || true)
if [ -n "$existing_server_addr" ]; then
log_info "Found existing server address: $existing_server_addr"
fi
if [ -n "$existing_server_port" ]; then
log_info "Found existing server port: $existing_server_port"
fi
if [ -n "$existing_proxy1_remote_port" ]; then
log_info "Found existing rad proxy remote port: $existing_proxy1_remote_port"
fi
if [ -n "$existing_proxy2_remote_port" ]; then
log_info "Found existing ssh proxy remote port: $existing_proxy2_remote_port"
fi
if [ -n "$existing_proxy3_remote_port" ]; then
log_info "Found existing web proxy remote port: $existing_proxy3_remote_port"
fi
fi
# Stop the temporary container
docker stop "$temp_container" >/dev/null 2>&1
fi
fi
# Prompt for server address
if [ -n "$FRP_SERVER_ADDR" ]; then
SERVER_ADDR="$FRP_SERVER_ADDR"
log_info "Using environment variable for FRP server address: $SERVER_ADDR"
else
if [ -n "$existing_server_addr" ]; then
SERVER_ADDR=$(read_terminal "Enter FRP server address IP [${existing_server_addr}]: " "$existing_server_addr")
else
while [ -z "$SERVER_ADDR" ]; do
SERVER_ADDR=$(read_terminal "Enter FRP server address IP: " "")
if [ -z "$SERVER_ADDR" ]; then
log_error "Server address cannot be empty" >&2
fi
done
fi
save_to_env "FRP_SERVER_ADDR" "$SERVER_ADDR"
fi
# Use FRP server user from environment or default to 'teenfuture'
if [ -n "$FRP_SERVER_USER" ]; then
SERVER_USER="$FRP_SERVER_USER"
log_info "Using environment variable for FRP server user: $SERVER_USER"
else
# Use default 'teenfuture' user if not specified
SERVER_USER="teenfuture"
log_info "Using default FRP server user: $SERVER_USER"
fi
# Use default server port (16999) without prompting, but use existing if available
if [ -n "$FRP_SERVER_PORT" ]; then
SERVER_PORT="$FRP_SERVER_PORT"
log_info "Using environment variable for FRP server port: ${SERVER_PORT}"
elif [ -n "$existing_server_port" ]; then
SERVER_PORT="$existing_server_port"
else
# SERVER_PORT remains default 16999
save_to_env "FRP_SERVER_PORT" "$SERVER_PORT"
fi
log_info "Using FRP server port: ${SERVER_PORT}"
# Generate the frpc.toml content
local config_content=""
config_content+="serverAddr = \"${SERVER_ADDR}\"\n"
config_content+="serverPort = ${SERVER_PORT}\n\n"
# Dynamic proxy configuration based on environment variables
# Look for environment variables in the format PROXY_*_PORT
local proxy_configs=()
# Process PROXY_*_PORT variables if they exist in environment
local env_vars=($(compgen -v | grep -E "^PROXY_[A-Z0-9_]*_PORT$" 2>/dev/null))
for var_name in "${env_vars[@]}"; do
local var_value="${!var_name}"
if [ -n "$var_value" ] && [[ "$var_value" =~ ^[0-9]+$ ]]; then
# Extract proxy name from variable (e.g., PROXY_CUSTOM_PORT -> custom)
local proxy_name=$(echo "$var_name" | sed 's/^PROXY_\([A-Z0-9_]*\)_PORT$/\1/' | tr '[:upper:]' '[:lower:]' | tr '_' '-')
local local_port_var="PROXY_${proxy_name^^}_LOCAL_PORT" # Convert back to uppercase with underscores
# Get local port - either from PROXY_*_LOCAL_PORT or use defaults
local local_port="$INTERNAL_SERVICE_PORT" # Default to the main service port
if [ -n "${!local_port_var}" ]; then
local_port="${!local_port_var}"
fi
# Validate that local_port is not empty
if [ -z "$local_port" ] || ! [[ "$local_port" =~ ^[0-9]+$ ]]; then
log_error "Invalid or empty local port for ${proxy_name} proxy: '$local_port'"
log_error "Make sure INTERNAL_SERVICE_PORT or ${local_port_var} is set correctly in your .env.rvs-prxy file"
exit 1
fi
# Create the proxy configuration string
proxy_configs+=("${proxy_name}-${local_port}:${var_value}")
log_info "Adding ${proxy_name} proxy: local port ${local_port} to remote port ${var_value}"
fi
done
# If no proxy variables were found, add a default proxy if needed
if [ ${#proxy_configs[@]} -eq 0 ]; then
# Add a default proxy using WEB_REMOTE_PORT if available, otherwise use a default
local default_remote_port="${WEB_REMOTE_PORT:-8080}"
# Validate that INTERNAL_SERVICE_PORT is not empty
if [ -z "$INTERNAL_SERVICE_PORT" ] || ! [[ "$INTERNAL_SERVICE_PORT" =~ ^[0-9]+$ ]]; then
log_error "Invalid or empty internal service port: '$INTERNAL_SERVICE_PORT'"
log_error "Make sure INTERNAL_SERVICE_PORT is set correctly in your .env.rvs-prxy file"
exit 1
fi
proxy_configs+=("default-${INTERNAL_SERVICE_PORT}:${default_remote_port}")
log_info "Adding default proxy: local port $INTERNAL_SERVICE_PORT to remote port $default_remote_port"
fi
# Add all proxy configurations to the config content
for proxy_config in "${proxy_configs[@]}"; do
# Parse the proxy config format: proxy_name-local_port:remote_port
# Example: web-3000:12077 -> proxy_name="web", local_port=3000, remote_port=12077
local proxy_name=""
local local_remote_port=""
local local_port=""
local remote_port=""
# First, split on the LAST occurrence of '-' to get proxy name and local:remote part
# This handles cases where the proxy name might contain hyphens after processing
if [[ "$proxy_config" =~ ^(.*)-([0-9]+:[0-9]+)$ ]]; then
proxy_name="${BASH_REMATCH[1]}"
local_remote_port="${BASH_REMATCH[2]}"
else
log_error "Invalid proxy configuration format: $proxy_config"
log_error "Expected format: proxy_name-local_port:remote_port (e.g., web-3000:12077)"
exit 1
fi
# Then, split the local_remote_port on ':' to separate local and remote ports
IFS=':' read -r local_port remote_port <<< "$local_remote_port"
# Validate that both local and remote ports are set and are numeric
if [ -z "$local_port" ] || [ -z "$remote_port" ] || ! [[ "$local_port" =~ ^[0-9]+$ ]] || ! [[ "$remote_port" =~ ^[0-9]+$ ]]; then
log_error "Invalid proxy configuration format: $proxy_config"
log_error "Local port: '$local_port', Remote port: '$remote_port'"
exit 1
fi
config_content+="[[proxies]]\n"
config_content+="name = \"${proxy_name}-${THIS_INSTANCE}\"\n"
config_content+="type = \"tcp\"\n"
config_content+="localPort = ${local_port}\n"
# Use SERVICE_DOCKER_CONTAINER_NAME from .env* files if available, otherwise fall back to ${THIS_INSTANCE}-app
if [ -n "$SERVICE_DOCKER_CONTAINER_NAME" ]; then
config_content+="localIP = \"${SERVICE_DOCKER_CONTAINER_NAME}\"\n"
else
config_content+="localIP = \"${THIS_INSTANCE}-app\"\n"
fi
config_content+="remotePort = ${remote_port}\n\n"
done
# Create or update the frpc.toml file in the volume
log_info "Creating/updating frpc.toml configuration"
# Create a temporary directory for the config file
local temp_dir="/tmp/frpc-config-${THIS_INSTANCE}"
mkdir -p "$temp_dir"
# Write the config to a temporary file
echo -e "$config_content" > "${temp_dir}/frpc.toml"
# Create the volume if it doesn't exist
if ! docker volume inspect "$volume_name" >/dev/null 2>&1; then
log_info "Creating Docker volume: $volume_name"
docker volume create "$volume_name" >/dev/null
fi
# Copy the config file to the volume
# Create a temporary container to copy the file
if docker run --rm -d --name "$temp_container" -v "$volume_name:/etc/frp" -v "${temp_dir}:/tmp/config" alpine:latest sleep 10 >/dev/null 2>&1; then
docker exec "$temp_container" cp /tmp/config/frpc.toml /etc/frp/frpc.toml
docker stop "$temp_container" >/dev/null 2>&1
fi
# Clean up temporary directory
rm -rf "$temp_dir"
log_info "FRP client configuration created successfully"
echo "" >&2
}
# Function to check if any proxy configurations are web proxies
has_web_proxy() {
# Check if any proxy config has "web" as the proxy name
# Accept proxy configs as parameters
local proxy_configs=("$@")
for proxy_config in "${proxy_configs[@]}"; do
# Split the proxy config into proxy_name-local_port:remote_port
IFS='-' read -r proxy_name local_remote_port <<< "$proxy_config"
if [ "$proxy_name" = "web" ]; then
echo "true"
return 0
fi
done
echo "false"
return 0
}
# Function to start the Docker container (FRP Mode)
start_docker_container() {
if [ "$FRP" = false ]; then
log_info "Skipping Docker container start as FRP=false"
return 0
fi
echo -e "${BLUE}=== Starting Docker Container ===${NC}" >&2
local container_name="${THIS_INSTANCE}-frpc"
local volume_name="${THIS_INSTANCE}-frpc"
# Check if container already exists
if docker ps -a -f name=^${container_name}$ --format '{{.ID}}' | grep -q .; then
log_info "Container ${container_name} already exists"
if docker ps -f name=^${container_name}$ --format '{{.ID}}' | grep -q .; then
log_info "Container is already running"
return 0
else
log_info "Starting existing container ${container_name}"
if ! docker start "${container_name}"; then
log_error "Failed to start existing container"
return 1
fi
fi
else
log_info "Starting new Docker container: ${container_name}"
# Build docker run command
local docker_run_cmd="docker run --restart=always -d"
# Add network option if selected
if [ -n "$SELECTED_NETWORK" ]; then
docker_run_cmd+=" --network ${SELECTED_NETWORK}"
fi
# Add volume mount
docker_run_cmd+=" -v ${volume_name}:/etc/frp"
# Add container name
docker_run_cmd+=" --name ${container_name}"
# Add image
docker_run_cmd+=" snowdreamtech/frpc:0.62.0-alpine3.21"
# Execute the command
log_info "Executing: $docker_run_cmd"
if ! eval $docker_run_cmd; then
log_error "Failed to start Docker container"
return 1
fi
fi
log_info "Docker container started successfully"
echo "" >&2
}
# Function to display container operation commands (FRP Mode)
display_container_commands() {
if [ "$FRP" = false ]; then
log_info "Skipping container commands display as FRP=false"
return 0
fi
local container_name="${THIS_INSTANCE}-frpc"
local volume_name="${THIS_INSTANCE}-frpc"
# Build the docker run command string with network option
local display_docker_run_cmd="docker run --restart=always -d"
if [ -n "$SELECTED_NETWORK" ]; then
display_docker_run_cmd+=" --network ${SELECTED_NETWORK}"
fi
display_docker_run_cmd+=" -v ${volume_name}:/etc/frp --name ${container_name} snowdreamtech/frpc:0.62.0-alpine3.21"
# Display container operation commands in a rectangle box without side borders
echo -e "${BLUE}=============================================${NC}" >&2
echo -e "${GREEN}Container Operation Commands${NC}" >&2
echo -e "" >&2
echo -e "Run Container: ${GREEN}${display_docker_run_cmd}${NC}" >&2
echo -e "------------------------------------------------------------" >&2
echo -e "Container Accessing: ${GREEN}docker exec -it ${container_name} sh${NC}" >&2
echo -e "------------------------------------------------------------" >&2
echo -e "View the Logs: ${GREEN}docker logs --tail 50 ${container_name}${NC}" >&2
echo -e "------------------------------------------------------------" >&2
echo -e "Inspect: ${GREEN}docker inspect ${container_name}${NC}" >&2
echo -e "------------------------------------------------------------" >&2
echo -e "Remove Container: ${GREEN}docker rm -f ${container_name}${NC}" >&2
echo -e "${BLUE}=============================================${NC}" >&2
echo -e "${YELLOW}[REMARK]${NC} Please copy down these operation commands for future use." >&2
echo "" >&2
}
# Function to extract FRP server address from frpc.toml
extract_frp_server_address() {
if [ "$FRP" = false ]; then
log_info "Skipping FRP server address extraction as FRP=false"
return 0
fi
local volume_name="${THIS_INSTANCE}-frpc"
local temp_container="temp-extract-config-$(date +%s)"
local server_addr=""
# Create a temporary container to check for existing config
if docker volume inspect "$volume_name" >/dev/null 2>&1; then
# Create temporary container to access volume
if docker run --rm -d --name "$temp_container" -v "$volume_name:/etc/frp" alpine:latest sleep 10 >/dev/null 2>&1; then
# Check if frpc.toml exists
if docker exec "$temp_container" test -f /etc/frp/frpc.toml >/dev/null 2>&1; then
# Try to extract server address
server_addr=$(docker exec "$temp_container" grep "^serverAddr" /etc/frp/frpc.toml | cut -d'"' -f2 || true)
fi
# Stop the temporary container
docker stop "$temp_container" >/dev/null 2>&1
fi
fi
echo "$server_addr"
}
# Function to extract all remote ports from frpc.toml
extract_all_remote_ports() {
if [ "$FRP" = false ]; then
log_info "Skipping remote port extraction as FRP=false"
return 0
fi
local volume_name="${THIS_INSTANCE}-frpc"
local temp_container="temp-extract-ports-$(date +%s)"
local ports=()
# Create a temporary container to check for existing config
if docker volume inspect "$volume_name" >/dev/null 2>&1; then
# Create temporary container to access volume
if docker run --rm -d --name "$temp_container" -v "$volume_name:/etc/frp" alpine:latest sleep 10 >/dev/null 2>&1; then
# Check if frpc.toml exists
if docker exec "$temp_container" test -f /etc/frp/frpc.toml >/dev/null 2>&1; then
# Get all remotePort values from the configuration
local remote_ports=$(docker exec "$temp_container" grep "remotePort" /etc/frp/frpc.toml | cut -d'=' -f2 | tr -d ' ')
# Convert to array
while IFS= read -r port; do
if [ -n "$port" ]; then
ports+=("$port")
fi
done <<< "$remote_ports"
fi
# Stop the temporary container
docker stop "$temp_container" >/dev/null 2>&1
fi
fi
# Output all ports as space-separated values
echo "${ports[@]}"
}
# Function to load FRP configuration from volume (when .env is missing)
load_frp_config_from_volume() {
local instance_name="$1"
local volume_name="${instance_name}-frpc"
local temp_container="temp-load-config-$(date +%s)"
log_info "Loading FRP configuration from volume: $volume_name"
# Create a temporary container to check for existing config
if docker volume inspect "$volume_name" >/dev/null 2>&1; then
if docker run --rm -d --name "$temp_container" -v "$volume_name:/etc/frp" alpine:latest sleep 10 >/dev/null 2>&1; then
if docker exec "$temp_container" test -f /etc/frp/frpc.toml >/dev/null 2>&1; then
# Extract server address and port from frpc.toml
local server_addr=$(docker exec "$temp_container" grep "^serverAddr" /etc/frp/frpc.toml | cut -d'"' -f2 || true)
local server_port=$(docker exec "$temp_container" grep "^serverPort" /etc/frp/frpc.toml | cut -d'=' -f2 | tr -d ' ' || true)
if [ -n "$server_addr" ]; then
FRP_SERVER_ADDR="$server_addr"
log_info "Loaded FRP_SERVER_ADDR from volume: $FRP_SERVER_ADDR"
fi
if [ -n "$server_port" ]; then
FRP_SERVER_PORT="$server_port"
log_info "Loaded FRP_SERVER_PORT from volume: $FRP_SERVER_PORT"
fi
fi
docker stop "$temp_container" >/dev/null 2>&1
fi
fi
}
# Function to generate a new remote port for domain setup (FRP Mode)
generate_remote_port() {
if [ "$FRP" = false ]; then
log_info "Skipping remote port generation as FRP=false"
return 0
fi
# Get existing remote ports to avoid conflicts
local existing_ports
existing_ports=$(extract_all_remote_ports)
# Start from a base port (e.g., 12000) and find an available port
local base_port=12000
local new_port=$base_port
# Check if any existing ports conflict
while echo "$existing_ports" | grep -qw "$new_port"; do
new_port=$((new_port + 1))
done
echo "$new_port"
}
# Function to update frpc.toml with new domain proxy configuration (FRP Mode)
update_frpc_config_for_domain() {
local new_remote_port="$1"
if [ "$FRP" = false ]; then
log_info "Skipping frpc.toml update as FRP=false"
return 0
fi
log_info "Updating frpc.toml with new domain proxy configuration..."
local volume_name="${THIS_INSTANCE}-frpc"
local temp_container="temp-update-config-$(date +%s)"
local temp_dir="/tmp/frpc-update-${THIS_INSTANCE}"
# Create temporary directory
mkdir -p "$temp_dir"
# Get existing configuration
local existing_config=""
if docker volume inspect "$volume_name" >/dev/null 2>&1; then
if docker run --rm -d --name "$temp_container" -v "$volume_name:/etc/frp" alpine:latest sleep 10 >/dev/null 2>&1; then
if docker exec "$temp_container" test -f /etc/frp/frpc.toml >/dev/null 2>&1; then
existing_config=$(docker exec "$temp_container" cat /etc/frp/frpc.toml)
fi
docker stop "$temp_container" >/dev/null 2>&1
fi
fi
# Get server address and port from existing config or environment
local server_addr="${FRP_SERVER_ADDR}"
local server_port="${FRP_SERVER_PORT:-16999}"
if [ -z "$existing_config" ]; then
# No existing config, create new one
log_info "Creating new frpc.toml configuration"
existing_config="serverAddr = \"${server_addr}\"\nserverPort = ${server_port}\n\n"
fi
# Generate proxy name from domain
local proxy_name
proxy_name=$(echo "$DOMAIN_NAME" | sed 's/\./-/g' | tr '[:upper:]' '[:lower:]')
# Add new proxy configuration for this domain
local new_proxy_config
new_proxy_config="[[proxies]]\n"
new_proxy_config+="name = \"${proxy_name}-${THIS_INSTANCE}\"\n"
new_proxy_config+="type = \"tcp\"\n"
new_proxy_config+="localPort = ${LOCAL_SERVICE_PORT}\n"
if [ -n "$SERVICE_DOCKER_CONTAINER_NAME" ]; then
new_proxy_config+="localIP = \"${SERVICE_DOCKER_CONTAINER_NAME}\"\n"
else
new_proxy_config+="localIP = \"${THIS_INSTANCE}-app\"\n"
fi
new_proxy_config+="remotePort = ${new_remote_port}\n\n"
# Append new proxy config to existing config
local full_config="${existing_config}${new_proxy_config}"
# Write the config to a temporary file
echo -e "$full_config" > "${temp_dir}/frpc.toml"
# Create the volume if it doesn't exist
docker volume create "$volume_name" >/dev/null 2>&1 || true
# Copy the config file to the volume
docker run --rm -v "$volume_name:/etc/frp" -v "${temp_dir}:/tmp/source:ro" alpine:latest cp /tmp/source/frpc.toml /etc/frp/frpc.toml
# Clean up
rm -rf "$temp_dir"
# Restart the FRP client container to apply new configuration
log_info "Restarting FRP client container to apply new configuration..."
docker restart "${THIS_INSTANCE}-frpc" >/dev/null 2>&1
log_info "frpc.toml configuration updated successfully"
}
# Function to execute Docker commands locally (Traefik Mode)
local_docker_exec() {
local command="$1"
log_debug "Executing local Docker command: $command"
eval "$command"
}
# Function to copy files to Docker volume locally (Traefik Mode)
local_volume_copy() {
local source="$1"
local volume="$2"
local dest="$3"
log_debug "Copying $source to volume $volume:$dest"
docker run --rm -v "$volume:$dest" -v "$(dirname "$source"):/tmp/source:ro" alpine:latest cp -r "/tmp/source/$(basename "$source")" "$dest/"
}
# Function to parse EXTRA_ENTRYPOINTS variable and produce entrypoint args and port mappings
# Globals read: EXTRA_ENTRYPOINTS
# Globals set: EXTRA_ENTRYPOINT_ARGS (array of --entrypoints.NAME.address=ADDRESS)
# EXTRA_PORT_MAPPINGS (array of port mappings for docker-compose/docker-run)
# EXTRA_COMPOSE_ENTRYPOINTS (string of YAML command entries for compose)
# EXTRA_COMPOSE_PORTS (string of YAML port entries for compose)
parse_extra_entrypoints() {
EXTRA_ENTRYPOINT_ARGS=()
EXTRA_PORT_MAPPINGS=()
EXTRA_COMPOSE_ENTRYPOINTS=""
EXTRA_COMPOSE_PORTS=""
if [ -z "$EXTRA_ENTRYPOINTS" ]; then
return
fi
local saved_ifs="$IFS"
IFS=','
for entry in $EXTRA_ENTRYPOINTS; do
IFS="$saved_ifs"
entry=$(echo "$entry" | xargs)
local name="${entry%%=*}"
local address="${entry#*=}"
address=$(echo "$address" | xargs)
# Determine port and protocol from address
local port="${address#:}"
local protocol="tcp"
if [[ "$port" == *"/"* ]]; then
protocol="${port#*/}"
port="${port%%/*}"
fi
# Build entrypoint arg
EXTRA_ENTRYPOINT_ARGS+=("--entrypoints.${name}.address=${address}")
# Build port mapping
if [ "$protocol" = "udp" ]; then
EXTRA_PORT_MAPPINGS+=("${port}:${port}/udp")
else
EXTRA_PORT_MAPPINGS+=("${port}:${port}")
fi
# Build compose YAML strings (with real newlines for heredoc expansion)
EXTRA_COMPOSE_ENTRYPOINTS="${EXTRA_COMPOSE_ENTRYPOINTS} - '--entrypoints.${name}.address=${address}'
"
if [ "$protocol" = "udp" ]; then
EXTRA_COMPOSE_PORTS="${EXTRA_COMPOSE_PORTS} - '${port}:${port}/udp'
"
else
EXTRA_COMPOSE_PORTS="${EXTRA_COMPOSE_PORTS} - '${port}:${port}'
"
fi
done
IFS="$saved_ifs"
}
# Function to check if Traefik container exists locally (Traefik Mode)
check_traefik_container() {
log_info "Checking if Traefik container exists locally..."
local container_exists
container_exists=$(docker ps -a --filter name=traefik --format '{{.Names}}' 2>/dev/null || true)
if [ -n "$container_exists" ] && [ "$container_exists" = "traefik" ]; then
log_info "Traefik container already exists locally"
return 0
else
log_info "Traefik container does not exist locally"
return 1
fi
}
# Function to create Traefik Docker container locally if it doesn't exist (Traefik Mode)
create_traefik_container_local() {
log_info "Checking if Traefik container exists locally..."
local container_exists
container_exists=$(docker ps -a --filter name=traefik --format '{{.Names}}' 2>/dev/null || true)
# Use fixed container name "traefik" regardless of instance name
local TRAEFIK_CONTAINER_NAME="traefik"
# Use fixed volume name "traefik_data" regardless of instance name
local TRAEFIK_VOLUME_NAME="traefik_data"
if [ -n "$container_exists" ] && [ "$container_exists" = "$TRAEFIK_CONTAINER_NAME" ]; then
log_info "Traefik container '$TRAEFIK_CONTAINER_NAME' already exists locally"
# Parse extra entrypoints and check if container needs recreation
parse_extra_entrypoints
if [ ${#EXTRA_ENTRYPOINT_ARGS[@]} -gt 0 ]; then
# Check if entrypoints are already in the container
local current_cmd
current_cmd=$(docker inspect traefik --format='{{json .Config.Cmd}}' 2>/dev/null || echo "[]")
local needs_recreate=false
for ep_arg in "${EXTRA_ENTRYPOINT_ARGS[@]}"; do
if ! echo "$current_cmd" | grep -qF -- "${ep_arg}"; then
needs_recreate=true
break
fi
done
if [ "$needs_recreate" = true ]; then
log_info "Extra entrypoints changed, recreating Traefik container to apply..."
# Re-enter the creation path by removing the container reference
docker stop traefik 2>/dev/null || true
sleep 2
docker rm traefik 2>/dev/null || true
container_exists=""
fi
fi
fi
if [ -n "$container_exists" ] && [ "$container_exists" = "$TRAEFIK_CONTAINER_NAME" ]; then
log_info "Traefik container '$TRAEFIK_CONTAINER_NAME' exists, updating configuration..."
# Use specified Traefik version or default to v3.6.9
local TRAEFIK_VER="${TRAEFIK_VER:-v3.6.9}"
local TRAEFIK_IMAGE="traefik:$TRAEFIK_VER"
# Use specified Let's Encrypt email or default
local LETSENCRYPT_EMAIL="${LETSENCRYPT_EMAIL:-admin@yourdomain.com}"
# Create a minimal dynamic configuration file for the dashboard and basic routing, plus HTTP to HTTPS redirect
local temp_config="/tmp/traefik-$$.yml"
cat > "$temp_config" << 'EOF_HEREDOC'
http:
routers:
api:
rule: "PathPrefix(`/api`)"
service: api@internal
dashboard:
rule: "PathPrefix(`/dashboard`)"
service: api@internal
http-catchall:
rule: "PathRegexp(`{any:.*}`)"
entryPoints:
- "web"
service: "noop@internal"
middlewares:
- "redirect-to-https"
middlewares:
api-headers:
headers:
customFrameOptionsValue: SAMEORIGIN
contentTypeNosniff: true
browserXssFilter: true
stsIncludeSubdomains: true
stsPreload: true
stsSeconds: 15552000
redirect-to-https:
redirectScheme:
scheme: "https"
permanent: true
EOF_HEREDOC
# Copy the configuration file and create necessary directories in the volume with proper permissions
docker run --rm -v "$TRAEFIK_VOLUME_NAME:/etc/traefik" -v "/tmp:/tmp" alpine:latest sh -c 'mkdir -p /etc/traefik/config /etc/traefik/acme && cp /tmp/traefik.yml /etc/traefik/config/traefik.yml && touch /etc/traefik/acme/acme.json && chmod 600 /etc/traefik/acme/acme.json'
cp "$temp_config" "/tmp/traefik.yml"
docker run --rm -v "$TRAEFIK_VOLUME_NAME:/etc/traefik" -v "/tmp:/tmp" alpine:latest sh -c 'cp /tmp/traefik.yml /etc/traefik/config/traefik.yml'
rm -f "$temp_config"
log_info "Configuration updated in existing Traefik container"
# Exit early since container already exists and configuration was updated
log_info "Traefik container '$TRAEFIK_CONTAINER_NAME' is running locally using image: $TRAEFIK_IMAGE with volume: $TRAEFIK_VOLUME_NAME"
# Wait a bit for the container to start/update
sleep 3
# Commands will be shown at the end of setup_traefik_only()
return 0
else
log_info "Creating Traefik container '$TRAEFIK_CONTAINER_NAME' locally..."
# Create the Docker volume for persistent data using fixed name
docker volume create "$TRAEFIK_VOLUME_NAME"
fi
# Use specified Traefik version or default to v3.6.9
local TRAEFIK_VER="${TRAEFIK_VER:-v3.6.9}"
local TRAEFIK_IMAGE="traefik:$TRAEFIK_VER"
# Use specified Let's Encrypt email or default
local LETSENCRYPT_EMAIL="${LETSENCRYPT_EMAIL:-admin@yourdomain.com}"
# Create a minimal dynamic configuration file for the dashboard and basic routing, plus HTTP to HTTPS redirect
local temp_config="/tmp/traefik-$$.yml"
cat > "$temp_config" << 'EOF_HEREDOC'
http:
routers:
api:
rule: "PathPrefix(`/api`)"
service: api@internal
dashboard:
rule: "PathPrefix(`/dashboard`)"
service: api@internal
http-catchall:
rule: "PathRegexp(`{any:.*}`)"
entryPoints:
- "web"
service: "noop@internal"
middlewares:
- "redirect-to-https"
middlewares:
api-headers:
headers:
customFrameOptionsValue: SAMEORIGIN
contentTypeNosniff: true
browserXssFilter: true
stsIncludeSubdomains: true
stsPreload: true
stsSeconds: 15552000
redirect-to-https:
redirectScheme:
scheme: "https"
permanent: true
EOF_HEREDOC
# Copy the configuration file to volume
cp "$temp_config" "/tmp/traefik.yml"
docker run --rm -v "$TRAEFIK_VOLUME_NAME:/etc/traefik" -v "/tmp:/tmp" alpine:latest sh -c 'mkdir -p /etc/traefik/config /etc/traefik/acme && cp /tmp/traefik.yml /etc/traefik/config/traefik.yml && touch /etc/traefik/acme/acme.json && chmod 600 /etc/traefik/acme/acme.json'
rm -f "$temp_config"
# Parse extra entrypoints for dynamic entrypoint/port configuration
parse_extra_entrypoints
# Determine Docker version to decide if we need the version field
local compose_version_field
docker_version=$(docker --version | sed 's/Docker version //' | sed 's/,.*//')
major_version=$(echo "$docker_version" | cut -d. -f1)
minor_version=$(echo "$docker_version" | cut -d. -f2)
# Docker Compose v2.0+ (around Docker 20.10.0+) deprecates the version field
# But keep it for older versions for compatibility
if [ "$major_version" -lt 20 ] || ([ "$major_version" -eq 20 ] && [ "$minor_version" -lt 10 ]); then
compose_version_field="version: '3.8'"
else
compose_version_field=""
fi
# Create docker-compose.yml for Traefik using the specified version and volume
local temp_compose="/tmp/traefik-compose-$$.yml"
if [ -n "$compose_version_field" ]; then
# Include version field for older Docker versions
cat > "$temp_compose" << EOF
$compose_version_field
services:
traefik:
image: $TRAEFIK_IMAGE
container_name: $TRAEFIK_CONTAINER_NAME
restart: unless-stopped
command:
- '--api.insecure=true'
- '--providers.docker=true'
- '--providers.docker.exposedbydefault=false'
- '--entrypoints.web.address=:80'
- '--entrypoints.websecure.address=:443'
${EXTRA_COMPOSE_ENTRYPOINTS} - '--certificatesresolvers.letsencrypt.acme.httpchallenge=true'
- '--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web'
- '--certificatesresolvers.letsencrypt.acme.email=$LETSENCRYPT_EMAIL'
- '--certificatesresolvers.letsencrypt.acme.storage=/etc/traefik/acme/acme.json'
- '--providers.file.directory=/etc/traefik/config'
- '--providers.file.watch=true'
ports:
- '80:80'
- '443:443'
${EXTRA_COMPOSE_PORTS} - "127.0.0.1:${DASHBOARD_PORT}:8080" # Bind dashboard to localhost only for security
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- $TRAEFIK_VOLUME_NAME:/etc/traefik
labels:
- 'traefik.enable=true'
- 'traefik.http.routers.traefik.rule=Host(\`traefik.${DOMAIN_NAME:-traefik.localhost}\`)'
- 'traefik.http.routers.traefik.service=api@internal'
- 'traefik.http.routers.traefik.middlewares=auth'
- 'traefik.http.middlewares.auth.basicauth.users=admin:$$apr1$$H6usmud2$$YqJ2a7vLp9J9l5zK8Y4hD1'
volumes:
$TRAEFIK_VOLUME_NAME:
external: true
networks:
default:
name: ${DOCKER_NETWORK}
external: true
EOF
else
# No version field for newer Docker versions
cat > "$temp_compose" << EOF
services:
traefik:
image: $TRAEFIK_IMAGE
container_name: $TRAEFIK_CONTAINER_NAME
restart: unless-stopped
command:
- '--api.insecure=true'
- '--providers.docker=true'
- '--providers.docker.exposedbydefault=false'
- '--entrypoints.web.address=:80'
- '--entrypoints.websecure.address=:443'
${EXTRA_COMPOSE_ENTRYPOINTS} - '--certificatesresolvers.letsencrypt.acme.httpchallenge=true'
- '--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web'
- '--certificatesresolvers.letsencrypt.acme.email=$LETSENCRYPT_EMAIL'
- '--certificatesresolvers.letsencrypt.acme.storage=/etc/traefik/acme/acme.json'
- '--providers.file.directory=/etc/traefik/config'
- '--providers.file.watch=true'
ports:
- '80:80'
- '443:443'
${EXTRA_COMPOSE_PORTS} - "127.0.0.1:${DASHBOARD_PORT}:8080" # Bind dashboard to localhost only for security
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- $TRAEFIK_VOLUME_NAME:/etc/traefik
labels:
- 'traefik.enable=true'
- 'traefik.http.routers.traefik.rule=Host(\`traefik.${DOMAIN_NAME:-traefik.localhost}\`)'
- 'traefik.http.routers.traefik.service=api@internal'
- 'traefik.http.routers.traefik.middlewares=auth'
- 'traefik.http.middlewares.auth.basicauth.users=admin:$$apr1$$H6usmud2$$YqJ2a7vLp9J9l5zK8Y4hD1'
volumes:
$TRAEFIK_VOLUME_NAME:
external: true
networks:
default:
name: ${DOCKER_NETWORK}
external: true
EOF
fi
# Create the network if it doesn't exist
docker network create ${DOCKER_NETWORK} 2>/dev/null || true
# Determine which docker compose command to use based on Docker version
if [ "$major_version" -gt 20 ] || ([ "$major_version" -eq 20 ] && [ "$minor_version" -ge 10 ]); then
compose_cmd="docker compose"
else
compose_cmd="docker-compose"
fi
# Start Traefik container using appropriate command
$compose_cmd -f "$temp_compose" up -d
cp "$temp_compose" /tmp/docker-compose.yml
rm -f "$temp_compose"
log_info "Traefik container '$TRAEFIK_CONTAINER_NAME' is running locally using image: $TRAEFIK_IMAGE with volume: $TRAEFIK_VOLUME_NAME"
log_info "Docker compose file saved to /tmp/docker-compose.yml for reference"
log_info "Traefik dashboard: http://localhost:${DASHBOARD_PORT}/dashboard/"
# Wait a bit for the container to start
sleep 3
# Commands will be shown at the end of setup_traefik_only()
}
# Function to configure Traefik locally to connect to Application Service (Traefik Mode)
configure_traefik_local() {
log_info "Configuring Traefik locally to connect to Application Service..."
# Parse LOCAL_SERVICE_PORT for multi-port support
# Format: comma-separated port/protocol entries (protocol defaults to tcp)
# Example: "80,4443/udp" → two ports: 80/tcp and 4443/udp
local service_ports=()
local service_protocols=()
local first_tcp_port=""
local saved_ifs="$IFS"
local entry port protocol
IFS=','
for entry in $LOCAL_SERVICE_PORT; do
IFS="$saved_ifs"
entry=$(echo "$entry" | xargs)
port="$entry"
protocol="tcp"
if [[ "$port" == *"/"* ]]; then
protocol="${port#*/}"
port="${port%%/*}"
fi
service_ports+=("$port")
service_protocols+=("$protocol")
if [ -z "$first_tcp_port" ] && [ "$protocol" = "tcp" ]; then
first_tcp_port="$port"
fi
done
IFS="$saved_ifs"
if [ -z "$first_tcp_port" ]; then
log_error "No TCP port found in LOCAL_SERVICE_PORT ($LOCAL_SERVICE_PORT)"
log_error "At least one TCP port is required for HTTP routing"
exit 1
fi
# Check if the target service is running
local service_running
service_running=$(docker ps --filter name="$LOCAL_SERVICE_NAME" --format '{{.Names}}' 2>/dev/null || true)
if [ -z "$service_running" ]; then
log_error "Target application service '$LOCAL_SERVICE_NAME' is not running."
log_error "Please make sure your Application container is started."
exit 1
fi
# Check if the target service is connected to the ${DOCKER_NETWORK} network
local service_in_network
service_in_network=$(docker inspect "$LOCAL_SERVICE_NAME" 2>/dev/null | grep -o "\"${DOCKER_NETWORK}\"" || true)
if [ -z "$service_in_network" ]; then
log_error "Target application service '$LOCAL_SERVICE_NAME' is not connected to '${DOCKER_NETWORK}' network."
log_error "Please ensure the target application container is connected to the '${DOCKER_NETWORK}' network."
log_error "Application should connecting to ${DOCKER_NETWORK} network, here is the docker compose yml example:"
log_error " services:"
log_error " you-app:"
log_error " networks:"
log_error " - ${DOCKER_NETWORK}"
log_error " networks:"
log_error " ${DOCKER_NETWORK}:"
log_error " external: true"
log_error ""
log_error "You can connect it with: docker network connect ${DOCKER_NETWORK} $LOCAL_SERVICE_NAME"
log_error "Make sure your Application container is started."
exit 1
fi
# Check if DOMAIN_NAME contains a sub-path (like event.cgu.org.hk/api)
local domain_part
local path_part
if [[ "$DOMAIN_NAME" == *"/"* ]]; then
# Extract domain and path parts
domain_part=$(echo "$DOMAIN_NAME" | cut -d'/' -f1)
path_part="/$(echo "$DOMAIN_NAME" | cut -d'/' -f2-)"
log_info "Detected sub-path configuration: domain=$domain_part, path=$path_part"
else
domain_part="$DOMAIN_NAME"
path_part=""
fi
# Create dynamic configuration file for the service inside the volume
local temp_config="/tmp/app-service-$$.yml"
if [ -n "$path_part" ]; then
# Configure for sub-path routing
cat > "$temp_config" << EOF
http:
routers:
${domain_part}-https:
rule: 'Host(\`${domain_part}\`) && PathPrefix(\`${path_part}\`)'
service: ${domain_part}-service
tls:
certResolver: letsencrypt
entryPoints:
- websecure
middlewares:
- ${domain_part}-strip-prefix
${domain_part}-http:
rule: 'Host(\`${domain_part}\`) && PathPrefix(\`${path_part}\`)'
service: ${domain_part}-service
entryPoints:
- web
middlewares:
- redirect-to-https
- ${domain_part}-strip-prefix
middlewares:
${domain_part}-strip-prefix:
stripPrefix:
prefixes:
- ${path_part}
services:
${domain_part}-service:
loadBalancer:
servers:
- url: 'http://${LOCAL_SERVICE_NAME}:${first_tcp_port}'
passHostHeader: true
EOF
else
# Configure for root domain routing
cat > "$temp_config" << EOF
http:
routers:
${domain_part}-https:
rule: 'Host(\`${DOMAIN_NAME}\`)'
service: ${domain_part}-service
tls:
certResolver: letsencrypt
entryPoints:
- websecure
${domain_part}-http:
rule: 'Host(\`${DOMAIN_NAME}\`)'
service: ${domain_part}-service
entryPoints:
- web
middlewares:
- redirect-to-https
services:
${domain_part}-service:
loadBalancer:
servers:
- url: 'http://${LOCAL_SERVICE_NAME}:${first_tcp_port}'
passHostHeader: true
EOF
fi
# Generate UDP/TCP router config for non-HTTP service ports (e.g., UDP relay ports)
local udp_routers_added=false
if [ ${#service_ports[@]} -gt 1 ]; then
parse_extra_entrypoints
local udp_ep_ports=()
local udp_ep_names=()
for ep_arg in "${EXTRA_ENTRYPOINT_ARGS[@]}"; do
local ep_name="${ep_arg#--entrypoints.}"
ep_name="${ep_name%%.address=*}"
local ep_addr="${ep_arg#*=}"
if [[ "$ep_addr" == *"/udp" ]]; then
local ep_port="${ep_addr#:}"
ep_port="${ep_port%/udp}"
udp_ep_ports+=("$ep_port")
udp_ep_names+=("$ep_name")
fi
done
for i in "${!service_ports[@]}"; do
if [ "${service_protocols[$i]}" = "udp" ]; then
local container_port="${service_ports[$i]}"
local matching_ep=""
for j in "${!udp_ep_ports[@]}"; do
if [ "${udp_ep_ports[$j]}" = "$container_port" ]; then
matching_ep="${udp_ep_names[$j]}"
break
fi
done
if [ -z "$matching_ep" ] && [ ${#udp_ep_names[@]} -gt 0 ]; then
matching_ep="${udp_ep_names[0]}"
fi
if [ -n "$matching_ep" ]; then
cat >> "$temp_config" << EOF
udp:
routers:
${domain_part}-${matching_ep}:
entryPoints:
- ${matching_ep}
service: ${domain_part}-udp
services:
${domain_part}-udp:
loadBalancer:
servers:
- address: "${LOCAL_SERVICE_NAME}:${container_port}"
EOF
udp_routers_added=true
log_info "Added UDP routing: entrypoint '${matching_ep}' → ${LOCAL_SERVICE_NAME}:${container_port}"
else
log_warn "UDP port ${container_port} in LOCAL_SERVICE_PORT has no matching EXTRA_ENTRYPOINTS entry"
log_warn "Add e.g. EXTRA_ENTRYPOINTS=\"myudp=:${container_port}/udp\" to your .env file"
fi
fi
done
fi
# Generate a unique configuration file name based on the domain and service name to avoid conflicts
# Use the domain (without path) and service name to create a unique identifier
local domain_base=$(echo "$DOMAIN_NAME" | cut -d'/' -f1)
local config_file_name="app-service-${domain_base}-${LOCAL_SERVICE_NAME//[^a-zA-Z0-9]/-}.yml"
# Use fixed volume name for Traefik configuration
local TRAEFIK_VOLUME_NAME="traefik_data"
# Check if a config file already exists for this domain and service combination
local config_exists
config_exists=$(docker run --rm -v "${TRAEFIK_VOLUME_NAME}:/etc/traefik" alpine:latest ls -la /etc/traefik/config/ 2>/dev/null | grep "$config_file_name" || true)
if [ -n "$config_exists" ]; then
log_warn "Configuration file $config_file_name already exists, it will be overwritten"
log_info "This will update the existing domain configuration for ${DOMAIN_NAME}"
else
log_info "Creating new configuration for domain ${DOMAIN_NAME}"
fi
# Copy the configuration file into the volume using a temporary container
cp "$temp_config" "/tmp/app-service.yml"
docker run --rm -v "${TRAEFIK_VOLUME_NAME}:/etc/traefik" -v "/tmp:/tmp" alpine:latest sh -c "cp /tmp/app-service.yml /etc/traefik/config/$config_file_name"
rm -f "$temp_config"
log_info "Added configuration for service ${LOCAL_SERVICE_NAME} on domain $domain_base to Traefik (file: $config_file_name)"
# Connect the traefik container to the ${DOCKER_NETWORK} network to ensure communication
docker network connect ${DOCKER_NETWORK} traefik 2>/dev/null || true
# Check if restart is needed based on environment variable (default: false for dynamic loading)
if [ "${RESTART_TRAEFIK:-false}" = "true" ]; then
# Restart Traefik to apply the new configuration
docker restart traefik
log_info "Traefik restarted to apply new configuration"
else
log_info "Traefik dynamic configuration added (restart not performed, Traefik should auto-load the configuration)"
log_info "If needed, you can manually restart Traefik with: docker restart traefik"
fi
log_info "Traefik configured to route traffic to Application Service"
# Show success message based on configuration type
if [ -n "$path_part" ]; then
log_info "Service will be accessible at: https://$domain_part$path_part"
else
log_info "Service will be accessible at: https://$DOMAIN_NAME"
fi
}
# Function to configure Traefik to route a domain to an EXTERNAL web service (Traefik Mode)
# The backend is a remote URL (e.g. https://app.lovable.app/), not a local container.
configure_traefik_external() {
log_info "Configuring Traefik locally to route ${DOMAIN_NAME} to external service ${EXTERNAL_URL} ..."
# Validate and parse the external URL into scheme / host / port / base path
if ! parse_external_url; then
log_error "Invalid EXTERNAL_URL '${EXTERNAL_URL}'"
log_error "Expected format: https://host[:port][/path] (e.g. https://cornersendbox-impact-vms.lovable.app/)"
exit 1
fi
log_info "External target: ${EXT_SCHEME}://${EXT_HOST}:${EXT_PORT} (base path: ${EXT_BASE_PATH:-none})"
# Split DOMAIN_NAME into domain part and optional sub-path part
local domain_part
local path_part=""
if [[ "$DOMAIN_NAME" == *"/"* ]]; then
domain_part=$(echo "$DOMAIN_NAME" | cut -d'/' -f1)
path_part="/$(echo "$DOMAIN_NAME" | cut -d'/' -f2-)"
path_part="${path_part%/}"
if [ -z "$path_part" ]; then
path_part=""
fi
log_info "Detected sub-path configuration: domain=${domain_part}, path=${path_part}"
else
domain_part="$DOMAIN_NAME"
fi
# Non-fatal reachability hint (Traefik does the real resolution from its container)
check_external_service_reachable
# Middleware chain: strip our sub-path, then re-apply the origin base path
local prefix_middlewares=()
if [ -n "$path_part" ]; then
prefix_middlewares+=("${domain_part}-strip-prefix")
fi
if [ -n "$EXT_BASE_PATH" ]; then
prefix_middlewares+=("${domain_part}-add-prefix")
fi
# Router rule: whole domain, or domain plus sub-path
local router_rule="Host(\`${domain_part}\`)"
if [ -n "$path_part" ]; then
router_rule="${router_rule} && PathPrefix(\`${path_part}\`)"
fi
local temp_config="/tmp/app-ext-service.yml"
# HTTPS router (TLS terminated by Traefik, Let's Encrypt certificate)
cat > "$temp_config" << EOF
http:
routers:
${domain_part}-https:
rule: '${router_rule}'
service: ${domain_part}-ext
tls:
certResolver: letsencrypt
entryPoints:
- websecure
EOF
if [ ${#prefix_middlewares[@]} -gt 0 ]; then
printf ' middlewares:\n' >> "$temp_config"
for mw in "${prefix_middlewares[@]}"; do
printf ' - %s\n' "$mw" >> "$temp_config"
done
fi
# HTTP router (redirect to HTTPS)
cat >> "$temp_config" << EOF
${domain_part}-http:
rule: '${router_rule}'
service: ${domain_part}-ext
entryPoints:
- web
middlewares:
- redirect-to-https
EOF
if [ ${#prefix_middlewares[@]} -gt 0 ]; then
for mw in "${prefix_middlewares[@]}"; do
printf ' - %s\n' "$mw" >> "$temp_config"
done
fi
# Middleware definitions (only when a sub-path or origin base path is used)
if [ ${#prefix_middlewares[@]} -gt 0 ]; then
printf ' middlewares:\n' >> "$temp_config"
if [ -n "$path_part" ]; then
cat >> "$temp_config" << EOF
${domain_part}-strip-prefix:
stripPrefix:
prefixes:
- ${path_part}
EOF
fi
if [ -n "$EXT_BASE_PATH" ]; then
cat >> "$temp_config" << EOF
${domain_part}-add-prefix:
addPrefix:
prefix: ${EXT_BASE_PATH}
EOF
fi
fi
# Backend service: absolute URL of the external service, origin Host header
cat >> "$temp_config" << EOF
services:
${domain_part}-ext:
loadBalancer:
servers:
- url: '${EXT_SCHEME}://${EXT_HOST}:${EXT_PORT}'
passHostHeader: false
EOF
# Write the dynamic configuration into the Traefik volume
local config_file_name="app-ext-${domain_part}.yml"
local TRAEFIK_VOLUME_NAME="traefik_data"
local config_exists
config_exists=$(docker run --rm -v "${TRAEFIK_VOLUME_NAME}:/etc/traefik" alpine:latest ls -la /etc/traefik/config/ 2>/dev/null | grep "$config_file_name" || true)
if [ -n "$config_exists" ]; then
log_warn "Configuration file $config_file_name already exists, it will be overwritten"
log_info "This will update the existing external service configuration for ${DOMAIN_NAME}"
else
log_info "Creating new external service configuration for domain ${domain_part}"
fi
docker run --rm -v "${TRAEFIK_VOLUME_NAME}:/etc/traefik" -v "/tmp:/tmp" alpine:latest sh -c "cp /tmp/app-ext-service.yml /etc/traefik/config/$config_file_name"
rm -f "$temp_config"
log_info "Added external service configuration ${EXTERNAL_URL} for domain ${domain_part} to Traefik (file: $config_file_name)"
# Restart is optional - the file provider watches /etc/traefik/config
if [ "${RESTART_TRAEFIK:-false}" = "true" ]; then
docker restart traefik
log_info "Traefik restarted to apply new configuration"
else
log_info "Traefik dynamic configuration added (restart not performed, Traefik should auto-load the configuration)"
log_info "If needed, you can manually restart Traefik with: docker restart traefik"
fi
log_info "Traefik configured to proxy https://${DOMAIN_NAME} → ${EXTERNAL_URL}"
log_info "Origin receives the Host header of ${EXT_HOST} (passHostHeader=false)"
if [ -n "$path_part" ] || [ -n "$EXT_BASE_PATH" ]; then
log_info "Path mapping: https://${DOMAIN_NAME}<path> → ${EXT_SCHEME}://${EXT_HOST}:${EXT_PORT}${EXT_BASE_PATH}<path>"
fi
}
# Function to set up a domain routed to an external web service (Traefik Mode)
setup_external_domain() {
log_info "Setting up external service domain: ${DOMAIN_NAME}"
log_info "External service configuration loaded from: ${EXT_ENV_FILE:-.env-ext-*}"
# External URL routing is a Traefik-only capability
if [ "$FRP" = "true" ]; then
log_error "External service routing is supported in Traefik mode only (FRP=false)."
log_error "FRP mode detected - the tunnel cannot proxy to an external URL."
exit 1
fi
# Check if Traefik is running
if ! docker ps --filter name=^${INSTANCE_NAME}$ --format '{{.Names}}' | grep -q "^traefik$"; then
log_error "Traefik reverse proxy is not running!"
log_error "Please ensure the reverse proxy is setup first"
exit 1
fi
# No container / docker-network checks: the backend is an external URL
configure_traefik_external
log_info "External service domain ${DOMAIN_NAME} has been configured in Traefik"
log_info "Service will be accessible at: https://${DOMAIN_NAME}"
}
# Function to check if Application Service is running locally (Traefik Mode)
check_app_service_local() {
log_info "Checking if Application Service is running locally..."
local service_exists
service_exists=$(docker ps --filter name="$LOCAL_SERVICE_NAME" --format '{{.Names}}' 2>/dev/null || true)
if [ -n "$service_exists" ] && [ "$service_exists" = "$LOCAL_SERVICE_NAME" ]; then
log_info "Application Service is running locally"
return 0
else
log_error "Application Service is not running locally: $LOCAL_SERVICE_NAME"
return 1
fi
}
# Function to check status of Traefik and Application Service locally (Traefik Mode)
check_status_local() {
log_info "Checking status of Traefik and Application Service locally..."
local traefik_status
local app_status
traefik_status=$(docker ps --filter name=traefik --format '{{.Status}}' 2>/dev/null || echo 'Not running')
app_status=$(docker ps --filter name="$LOCAL_SERVICE_NAME" --format '{{.Status}}' 2>/dev/null || echo 'Not running')
echo -e "Traefik status: ${GREEN}$traefik_status${NC}"
echo -e "Application Service ($LOCAL_SERVICE_NAME) status: ${GREEN}$app_status${NC}"
}
# Function to view Traefik logs locally (Traefik Mode)
view_traefik_logs_local() {
log_info "Viewing Traefik logs..."
docker logs --tail 50 traefik
}
# Function to update docker-compose.app.yml on proxy server with dynamic ports (FRP Mode)
update_proxy_server_config() {
if [ "$FRP" = false ]; then
log_info "Skipping proxy server configuration update as FRP=false"
return 0
fi
local proxy_server_addr="$1"
local has_web_proxy="$2"
shift 2 # Remove the first two arguments (proxy_server_addr and has_web_proxy), rest are ports
# The remaining arguments are all the ports
local all_ports=("$@")
# Make sure we have at least 1 port to work with
if [ ${#all_ports[@]} -lt 1 ]; then
log_error "Need at least 1 port for proxy configuration"
return 1
fi
# Use the first available port as the web port for domain routing
local web_port="${all_ports[0]}"
echo -e "${BLUE}=== Updating Proxy Server Configuration ===${NC}" >&2
# Check if SSH key exists
if [ ! -f ~/.ssh/itl-devs1 ]; then
log_error "SSH key ~/.ssh/itl-devs1 not found"
return 1
fi
# Prompt for domain name if not already set
if [ -z "$DOMAIN_NAME" ]; then
DOMAIN_NAME=$(prompt_domain_name)
fi
# Create temporary script to run on proxy server that contains the direct update functions
local temp_script="/tmp/update-compose-$THIS_INSTANCE.sh"
cat > "$temp_script" << 'EOF'
#!/bin/bash
# Function to check if the instance configuration is complete and up-to-date in ports section
# This function is a simplified version - in a real implementation, this would check dynamically
check_ports_config_status() {
local INSTANCE_NAME="$1"
local PORTS_CONFIG=("${@:2}") # All remaining arguments are ports
local COMPOSE_FILE="/data/ssd/rvs-prxy/frps/apps/zzthis/docker-compose.app.yml"
# Set the instance marker for ports section
local INSTANCE_MARKER="# ${INSTANCE_NAME} Self-hosting ports ===="
# Check if the instance marker exists
if [ ! -f "${COMPOSE_FILE}" ] || ! grep -q "${INSTANCE_MARKER}" "${COMPOSE_FILE}"; then
echo "not_found"
return
fi
# Find the exact lines of the configuration block
local start_line=$(grep -n "${INSTANCE_MARKER}" "${COMPOSE_FILE}" | head -1 | cut -d: -f1)
local end_line=$(grep -n "# =========================" "${COMPOSE_FILE}" | awk -F: -v start="$start_line" '$1 > start {print $1; exit}')
# If we can't find the end marker, use a default range
if [ -z "$end_line" ]; then
end_line=$((start_line + ${#PORTS_CONFIG[@]} + 5))
fi
# Extract the configuration block
local config_block=$(sed -n "${start_line},${end_line}p" "${COMPOSE_FILE}")
# Count the port lines
local port_lines=$(echo "$config_block" | grep -c '^ - "[0-9]*:[0-9]*/tcp"')
# For simplicity in this implementation, we'll mark as 'outdated' to update
echo "outdated"
}
# Function to check if the instance configuration is complete and up-to-date in labels section
check_labels_config_status() {
local INSTANCE_NAME="$1"
local WEB_PORT="$2"
local DOMAIN_NAME="$3"
local COMPOSE_FILE="/data/ssd/rvs-prxy/frps/apps/zzthis/docker-compose.app.yml"
# Set the instance marker for labels section (different from ports)
local INSTANCE_MARKER="# ${INSTANCE_NAME} Self-hosting labels ===="
# Check if the instance marker exists in labels section
if [ ! -f "${COMPOSE_FILE}" ] || ! grep -q "${INSTANCE_MARKER}" "${COMPOSE_FILE}"; then
echo "not_found"
return
fi
# Find the exact lines of the configuration block
local start_line=$(grep -n "${INSTANCE_MARKER}" "${COMPOSE_FILE}" | head -1 | cut -d: -f1)
local end_line=$(grep -n "# =========================" "${COMPOSE_FILE}" | awk -F: -v start="$start_line" '$1 > start {print $1; exit}')
# If we can't find the end marker, use a default range
if [ -z "$end_line" ]; then
end_line=$((start_line + 15))
fi
# Extract the configuration block
local config_block=$(sed -n "${start_line},${end_line}p" "${COMPOSE_FILE}")
# Count the label lines (should be 11 lines plus markers)
local label_lines=$(echo "$config_block" | grep -c '^ - "traefik\.')
if [ "$label_lines" -ge 11 ]; then
# Check if the domain and port match our current configuration
local current_domain=$(echo "$config_block" | grep 'rule=Host(' | head -1 | sed 's/.*Host(`\([^)]*\)`).*/\1/')
local current_port=$(echo "$config_block" | grep 'loadbalancer.server.port' | sed 's/.*port=\([0-9]*\).*/\1/')
if [ -n "$current_domain" ] && [ -n "$current_port" ] && \
[ "$current_domain" = "$DOMAIN_NAME" ] && [ "$current_port" = "$WEB_PORT" ]; then
echo "up_to_date"
else
echo "outdated"
fi
else
echo "incomplete"
fi
}
# Function to find the correct insertion point in a specific section
find_section_insertion_point() {
local section_name="$1"
local COMPOSE_FILE="/data/ssd/rvs-prxy/frps/apps/zzthis/docker-compose.app.yml"
# Find the section start line
local section_start=$(grep -n "${section_name}:" "${COMPOSE_FILE}" | head -1 | cut -d: -f1)
if [ -z "$section_start" ]; then
echo "0"
return
fi
# Look for the next section after this one or end of section
local current_line=$((section_start + 1))
local total_lines=$(wc -l < "${COMPOSE_FILE}")
while [ "$current_line" -le "$total_lines" ]; do
local line_content=$(sed -n "${current_line}p" "${COMPOSE_FILE}")
# If we hit a line that starts a new section (not indented or comment)
if [[ "$line_content" =~ ^[[:space:]]*[a-zA-Z] ]] && [[ ! "$line_content" =~ ^[[:space:]]*# ]] && [[ ! "$line_content" =~ ^[[:space:]]*- ]]; then
echo "$current_line"
return
fi
current_line=$((current_line + 1))
done
# If we didn't find the end, insert at the end of the file
echo $((total_lines + 1))
}
# Function to update ports section with dynamic ports
update_ports_config() {
local INSTANCE_NAME="$1"
shift # Remove first argument, remaining are ports
local PORTS=("$@") # All remaining arguments are ports
local COMPOSE_FILE="/data/ssd/rvs-prxy/frps/apps/zzthis/docker-compose.app.yml"
local INSTANCE_MARKER="# ${INSTANCE_NAME} Self-hosting ports ===="
local INSTANCE_END_MARKER="# ========================="
local config_status=$(check_ports_config_status "$INSTANCE_NAME" "${PORTS[@]}")
echo "Ports configuration status: $config_status"
case "$config_status" in
"not_found")
echo "Adding new instance to ports section"
local insertion_line=$(find_section_insertion_point "ports")
# Build the port configuration dynamically, excluding standard web ports (80, 443)
local port_config=""
local non_standard_ports=()
for port in "${PORTS[@]}"; do
# Skip standard web ports since they may be managed by the system
if [ "$port" != "80" ] && [ "$port" != "443" ]; then
port_config+=" - \"${port}:${port}/tcp\"\\n"
non_standard_ports+=("$port")
fi
done
# Only add ports section if there are non-standard ports to map
if [ -n "$port_config" ]; then
# Insert the configuration at the correct location
sed -i "${insertion_line}i\\
\\
${INSTANCE_MARKER}\\
${port_config} ${INSTANCE_END_MARKER}\\
" "${COMPOSE_FILE}"
echo "Ports configuration added successfully for ${#non_standard_ports[@]} non-standard port(s): ${non_standard_ports[*]}"
else
echo "No non-standard ports to add to ports section (ports 80 and 443 are standard)"
fi
;;
"up_to_date")
echo "Ports configuration is already up to date"
;;
"outdated"|"incomplete")
echo "Updating existing ports configuration"
# Find the exact lines of the existing configuration
local start_line=$(grep -n "${INSTANCE_MARKER}" "${COMPOSE_FILE}" | head -1 | cut -d: -f1)
local end_line=$(grep -n "${INSTANCE_END_MARKER}" "${COMPOSE_FILE}" | awk -F: -v start="$start_line" '$1 > start {print $1; exit}')
# If we can't find the end marker, use a default range
if [ -n "$start_line" ] && [ -z "$end_line" ]; then
end_line=$((start_line + ${#PORTS[@]} + 5))
fi
# Remove only the specific configuration block (from start marker to end marker)
if [ -n "$start_line" ] && [ -n "$end_line" ] && [ "$start_line" -gt 0 ] && [ "$end_line" -gt "$start_line" ]; then
sed -i "${start_line},${end_line}d" "${COMPOSE_FILE}"
# Build the updated port configuration dynamically, excluding standard web ports (80, 443)
local port_config=""
local non_standard_ports=()
for port in "${PORTS[@]}"; do
# Skip standard web ports since they may be managed by the system
if [ "$port" != "80" ] && [ "$port" != "443" ]; then
port_config+=" - \"${port}:${port}/tcp\"\\n"
non_standard_ports+=("$port")
fi
done
# Only update ports section if there are non-standard ports to map
if [ -n "$port_config" ]; then
# Insert the updated configuration at the same location
sed -i "${start_line}i\\
${INSTANCE_MARKER}\\
${port_config} ${INSTANCE_END_MARKER}" "${COMPOSE_FILE}"
echo "Ports configuration updated successfully for ${#non_standard_ports[@]} non-standard port(s): ${non_standard_ports[*]}"
else
echo "No non-standard ports to update in ports section (ports 80 and 443 are standard)"
fi
else
echo "Warning: Could not determine valid line range for ports update"
fi
;;
esac
}
# Function to update labels section
update_labels_config() {
local INSTANCE_NAME="$1"
local WEB_PORT="$2"
local DOMAIN_NAME="$3"
local COMPOSE_FILE="/data/ssd/rvs-prxy/frps/apps/zzthis/docker-compose.app.yml"
local INSTANCE_MARKER="# ${INSTANCE_NAME} Self-hosting labels ===="
local INSTANCE_END_MARKER="# ========================="
local config_status=$(check_labels_config_status "$INSTANCE_NAME" "$WEB_PORT" "$DOMAIN_NAME")
echo "Labels configuration status: $config_status"
case "$config_status" in
"not_found")
echo "Adding new instance to labels section"
local insertion_line=$(find_section_insertion_point "labels")
# Insert the configuration at the correct location
sed -i "${insertion_line}i\\
\\
${INSTANCE_MARKER}\\
- \"traefik.http.routers.frps-${INSTANCE_NAME}-http.entrypoints=web\"\\
- \"traefik.http.routers.frps-${INSTANCE_NAME}-http.rule=Host(\`${DOMAIN_NAME}\`)\"\\
- \"traefik.http.routers.frps-${INSTANCE_NAME}-http.service=frps-${INSTANCE_NAME}\"\\
- \"traefik.http.routers.frps-${INSTANCE_NAME}-http.middlewares=http2https@file\"\\
\\
- \"traefik.http.routers.frps-${INSTANCE_NAME}-https.entrypoints=websecure\"\\
- \"traefik.http.routers.frps-${INSTANCE_NAME}-https.rule=Host(\`${DOMAIN_NAME}\`)\"\\
- \"traefik.http.routers.frps-${INSTANCE_NAME}-https.service=frps-${INSTANCE_NAME}\"\\
- \"traefik.http.routers.frps-${INSTANCE_NAME}-https.tls=true\"\\
- \"traefik.http.routers.frps-${INSTANCE_NAME}-https.tls.certresolver=letsencrypt\"\\
- \"traefik.http.routers.frps-${INSTANCE_NAME}-https.tls.domains[0].main=${DOMAIN_NAME}\"\\
\\
- \"traefik.http.services.frps-${INSTANCE_NAME}.loadbalancer.server.port=${WEB_PORT}\"\\
${INSTANCE_END_MARKER}\\
" "${COMPOSE_FILE}"
echo "Labels configuration added successfully"
;;
"up_to_date")
echo "Labels configuration is already up to date"
;;
"outdated"|"incomplete")
echo "Updating existing labels configuration"
# Find the exact lines of the existing configuration
local start_line=$(grep -n "${INSTANCE_MARKER}" "${COMPOSE_FILE}" | head -1 | cut -d: -f1)
local end_line=$(grep -n "${INSTANCE_END_MARKER}" "${COMPOSE_FILE}" | awk -F: -v start="$start_line" '$1 > start {print $1; exit}')
# If we can't find the end marker, use a default range
if [ -z "$end_line" ]; then
end_line=$((start_line + 15))
fi
# Remove only the specific configuration block (from start marker to end marker)
if [ -n "$start_line" ] && [ -n "$end_line" ] && [ "$start_line" -gt 0 ] && [ "$end_line" -gt "$start_line" ]; then
sed -i "${start_line},${end_line}d" "${COMPOSE_FILE}"
# Insert the updated configuration at the same location
sed -i "${start_line}i\\
${INSTANCE_MARKER}\\
- \"traefik.http.routers.frps-${INSTANCE_NAME}-http.entrypoints=web\"\\
- \"traefik.http.routers.frps-${INSTANCE_NAME}-http.rule=Host(\`${DOMAIN_NAME}\`)\"\\
- \"traefik.http.routers.frps-${INSTANCE_NAME}-http.service=frps-${INSTANCE_NAME}\"\\
- \"traefik.http.routers.frps-${INSTANCE_NAME}-http.middlewares=http2https@file\"\\
\\
- \"traefik.http.routers.frps-${INSTANCE_NAME}-https.entrypoints=websecure\"\\
- \"traefik.http.routers.frps-${INSTANCE_NAME}-https.rule=Host(\`${DOMAIN_NAME}\`)\"\\
- \"traefik.http.routers.frps-${INSTANCE_NAME}-https.service=frps-${INSTANCE_NAME}\"\\
- \"traefik.http.routers.frps-${INSTANCE_NAME}-https.tls=true\"\\
- \"traefik.http.routers.frps-${INSTANCE_NAME}-https.tls.certresolver=letsencrypt\"\\
- \"traefik.http.routers.frps-${INSTANCE_NAME}-https.tls.domains[0].main=${DOMAIN_NAME}\"\\
\\
- \"traefik.http.services.frps-${INSTANCE_NAME}.loadbalancer.server.port=${WEB_PORT}\"\\
${INSTANCE_END_MARKER}" "${COMPOSE_FILE}"
echo "Labels configuration updated successfully"
else
echo "Warning: Could not determine valid line range for labels update"
fi
;;
esac
}
# Function to restart Docker Compose service
restart_docker_compose() {
local COMPOSE_DIR="/data/ssd/rvs-prxy/frps/"
echo "Restarting Docker Compose service..."
# Change to the compose directory
cd "${COMPOSE_DIR}" || {
echo "Error: Could not change to directory ${COMPOSE_DIR}"
return 1
}
# Stop the service
echo "Stopping the service..."
if [ -f "./stop.sh" ]; then
./stop.sh
else
docker-compose -f docker-compose.app.yml down
fi
# Start the service
echo "Starting the service..."
if [ -f "./start.sh" ]; then
./start.sh
else
docker-compose -f docker-compose.app.yml up -d
fi
# Wait a moment for the service to start
sleep 3
# Show the logs
echo "Showing the last 50 lines of logs:"
docker logs --tail 50 frps_frps_1 2>/dev/null || docker logs --tail 50 "${COMPOSE_DIR##*/}_frps_1" 2>/dev/null || {
echo "Warning: Could not retrieve logs for frps_frps_1. Trying alternative container names..."
docker ps -a | grep frps
}
echo ""
echo "Docker Compose service has been restarted."
echo "You can view the logs with: docker logs --tail 50 frps_frps_1"
}
# Function to update the docker-compose configuration
update_compose_config() {
local INSTANCE_NAME="$1"
local HAS_WEB_PROXY="$2"
shift 2 # Remove first two arguments, remaining are ports
local PORTS=("$@") # Get all port arguments
local LAST_ARG="${PORTS[-1]}" # Last argument should be domain name
unset 'PORTS[-1]' # Remove domain from ports array
local DOMAIN_NAME="$LAST_ARG"
local COMPOSE_FILE="/data/ssd/rvs-prxy/frps/apps/zzthis/docker-compose.app.yml"
# Use the first port as the web port for domain routing
local WEB_PORT="${PORTS[0]}"
# Create a backup of the original file with timestamp
if [ -f "${COMPOSE_FILE}" ]; then
local TIMESTAMP=$(date +%Y%m%d_%H%M%S)
local BACKUP_FILE="${COMPOSE_FILE}.backup_${TIMESTAMP}"
cp "${COMPOSE_FILE}" "${BACKUP_FILE}"
echo "Created backup of docker-compose.app.yml as ${BACKUP_FILE}"
else
echo "NOTE: docker-compose.app.yml does not exist yet, will create it"
fi
# Update ports section based on whether we have a web proxy
if [ "$HAS_WEB_PROXY" = "true" ]; then
echo "Skipping port mappings for configuration with web proxy (access via domain only)"
else
# Update ports section with all ports (don't pass domain name to ports config)
update_ports_config "$INSTANCE_NAME" "${PORTS[@]}"
fi
# Update labels section - only needs the web port for routing
update_labels_config "$INSTANCE_NAME" "$WEB_PORT" "$DOMAIN_NAME"
# Ask user if they want to restart the service
echo ""
read -p "Do you want to restart the Docker Compose service now? (y/N): " -n 1 -r
echo ""
if [[ $REPLY =~ ^[Yy]$ ]]; then
restart_docker_compose
else
echo "You can restart the service later with:"
echo " cd /data/ssd/rvs-prxy/frps/ && ./stop.sh && ./start.sh"
echo "And view logs with:"
echo " docker logs --tail 50 frps_frps_1"
fi
}
# Execute the configuration update with passed parameters
# Pass all ports and domain name as arguments to update_compose_config
update_compose_config "$@"
EOF
# Make the script executable
chmod +x "$temp_script"
# Copy script to proxy server and execute it with all ports and domain
log_info "Connecting to proxy server at $proxy_server_addr"
if scp -i ~/.ssh/itl-devs1 "$temp_script" ${SERVER_USER}@$proxy_server_addr:/tmp/; then
# Construct command with instance name, web proxy flag, all ports and domain name
local cmd_args=("$THIS_INSTANCE" "$has_web")
for port in "${all_ports[@]}"; do
cmd_args+=("$port")
done
cmd_args+=("$DOMAIN_NAME") # Add domain name as last argument
if ssh -i ~/.ssh/itl-devs1 ${SERVER_USER}@$proxy_server_addr "bash /tmp/$(basename $temp_script) ${cmd_args[*]}"; then
log_info "Proxy server configuration updated successfully"
# Clean up remote script
ssh -i ~/.ssh/itl-devs1 ${SERVER_USER}@$proxy_server_addr "rm /tmp/$(basename $temp_script)" >/dev/null 2>&1 || true
else
log_error "Failed to execute script on proxy server"
# Clean up remote script
ssh -i ~/.ssh/itl-devs1 ${SERVER_USER}@$proxy_server_addr "rm /tmp/$(basename $temp_script)" >/dev/null 2>&1 || true
rm -f "$temp_script"
return 1
fi
else
log_error "Failed to copy script to proxy server"
rm -f "$temp_script"
return 1
fi
# Clean up local script
rm -f "$temp_script"
echo "" >&2
}
# Function to setup proxy in Traefik mode (runs locally on the same server)
setup_traefik_only() {
log_info "Setting up Traefik-only proxy for instance: ${THIS_INSTANCE}"
# Pre-request checks
echo -e "${YELLOW}[PRE-REQUEST]${NC} Make sure your current user has permission to execute docker command." >&2
echo "" >&2
# Auto-proceed with deployment (smooth operation)
echo "Auto-proceeding with Traefik setup..." >&2
echo "" >&2
# Note: Application service check is NOT performed here
# Application service is only checked during domain setup (setup_new_domain)
# This function only sets up the Traefik reverse proxy instance
# Create Traefik container locally if it doesn't exist
create_traefik_container_local
log_info "Traefik reverse proxy instance setup completed successfully!"
log_info "Next: Run the script again to setup your first domain."
# Show helpful commands to the user using unified function
show_traefik_commands
}
# Function to setup reverse proxy (main entry point for proxy setup mode)
setup_reverse_proxy() {
log_info "Setting up reverse proxy..."
# Proceed with setup based on mode
if [ "$FRP" = false ]; then
log_info "Setting up Traefik reverse proxy..."
setup_traefik_only
else
log_info "Setting up FRP tunnel with Traefik..."
setup_frp_with_traefik
fi
}
# Function to setup port exposure - expose a host port to connect to a container port
setup_port_exposure() {
log_info "Setting up port exposure: Host port ${HOST_PORT} -> Container ${CONTAINER_NAME}:${CONTAINER_PORT}"
log_info "Port configuration loaded from: ${PORT_ENV_FILE}"
# Check if Traefik is running
if ! docker ps --filter name=^${INSTANCE_NAME}$ --format '{{.Names}}' | grep -q "^traefik$"; then
log_error "Traefik reverse proxy is not running!"
log_error "Please ensure the reverse proxy is setup first"
exit 1
fi
# Check if target container is running
local container_running
container_running=$(docker ps --filter name="$CONTAINER_NAME" --format '{{.Names}}' 2>/dev/null || true)
if [ -z "$container_running" ]; then
log_error "Target container '$CONTAINER_NAME' is not running."
log_error "Please make sure the container is started."
exit 1
fi
# Configure port routing in Traefik
configure_port_routing_local
log_info "Port exposure ${HOST_PORT} has been configured in Traefik"
log_info "Service will be accessible at: http://<host-ip>:${HOST_PORT}"
}
# Function to configure port routing in Traefik for port-based access
configure_port_routing_local() {
log_info "Configuring Traefik port routing for container ${CONTAINER_NAME}..."
# Check if the target container is running
local container_running
container_running=$(docker ps --filter name="$CONTAINER_NAME" --format '{{.Names}}' 2>/dev/null || true)
if [ -z "$container_running" ]; then
log_error "Target container '$CONTAINER_NAME' is not running."
log_error "Please make sure the container is started."
exit 1
fi
# Get the container's IP address on the ${DOCKER_NETWORK} network
local container_ip
container_ip=$(docker inspect "$CONTAINER_NAME" 2>/dev/null | grep -A 20 '"${DOCKER_NETWORK}"' | grep '"IPAddress"' | grep -o '[0-9.]*' | head -1)
if [ -z "$container_ip" ]; then
# Try to get any IP address from the container
container_ip=$(docker inspect "$CONTAINER_NAME" 2>/dev/null | grep '"IPAddress"' | grep -o '[0-9.]*' | head -1)
fi
if [ -z "$container_ip" ]; then
log_warn "Could not determine container IP address, using container name for routing"
container_ip="$CONTAINER_NAME"
else
log_info "Container IP address: ${container_ip}"
fi
# Create dynamic configuration file for port-based routing
local config_filename="port-${HOST_PORT}-${CONTAINER_NAME}.yml"
# Build the YAML config content using printf to avoid escaping issues
local yaml_content
yaml_content=$(printf 'http:\n routers:\n port-%s-router:\n rule: '"'"'PathPrefix(`/`)'"'"'\n service: port-%s-service\n entryPoints:\n - port%s\n services:\n port-%s-service:\n loadBalancer:\n servers:\n - url: '"'"'http://%s:%s'"'"'\n passHostHeader: true' \
"$HOST_PORT" "$HOST_PORT" "$HOST_PORT" "$HOST_PORT" "$container_ip" "$CONTAINER_PORT")
# Base64 encode to avoid escaping issues
local encoded_config
encoded_config=$(echo "$yaml_content" | base64 -w 0)
# Create the configuration directly in the Traefik volume using docker
docker run --rm -v "traefik_data:/etc/traefik" alpine:latest sh -c "mkdir -p /etc/traefik/config && echo '${encoded_config}' | base64 -d > /etc/traefik/config/${config_filename}"
# Add the port entry point and port mapping to Traefik
add_traefik_entry_point "$HOST_PORT"
log_info "Port routing configuration created for host port ${HOST_PORT}"
log_info "Traefik has been updated with new entry point and port mapping"
}
# Function to add entry point to Traefik configuration
add_traefik_entry_point() {
local port="$1"
local entry_point_name="port${port}"
log_info "Adding entry point '${entry_point_name}' for port ${port} to Traefik..."
# Entry points in Traefik are defined via command-line arguments
# We need to recreate the container with the new entry point
# Get current Traefik configuration from running container
local current_image
current_image=$(docker inspect traefik --format='{{.Config.Image}}' 2>/dev/null || echo "traefik:latest")
local current_cmd
current_cmd=$(docker inspect traefik --format='{{json .Config.Cmd}}' 2>/dev/null || echo "[]")
# Check if entry point already exists in command
if echo "$current_cmd" | grep -q "entrypoints.${entry_point_name}.address"; then
log_info "Entry point '${entry_point_name}' already exists in Traefik command"
log_info "Entry point exists, port mapping should already be active"
return 0
fi
# Extract Let's Encrypt email from existing config
local letsencrypt_email
letsencrypt_email=$(echo "$current_cmd" | grep -oP 'acme\.email=\K[^",]+' 2>/dev/null || echo "")
if [ -z "$letsencrypt_email" ]; then
letsencrypt_email=$(echo "$current_cmd" | grep -o 'acme\.email=[^",]*' | sed 's/acme\.email=//' | head -1)
fi
if [ -z "$letsencrypt_email" ]; then
letsencrypt_email="admin@example.com"
fi
log_info "Updating Traefik container with new entry point '${entry_point_name}'..."
log_info "Using Let's Encrypt email: ${letsencrypt_email}"
# Parse extra entrypoints from config to include them in the recreation
parse_extra_entrypoints
# Build extra entrypoint args and port mappings
local extra_ep_args=""
local extra_port_maps=""
for ep_arg in "${EXTRA_ENTRYPOINT_ARGS[@]}"; do
extra_ep_args="${extra_ep_args} ${ep_arg}"
done
for pm in "${EXTRA_PORT_MAPPINGS[@]}"; do
extra_port_maps="${extra_port_maps} -p ${pm}"
done
# Stop current container
log_info "Stopping Traefik container..."
docker stop traefik 2>/dev/null || true
sleep 2
# Remove old container (keep volume)
docker rm traefik 2>/dev/null || true
# Recreate container with all standard entry points plus the new one and extras
docker run -d \
--name ${INSTANCE_NAME} \
--restart=unless-stopped \
--network ${DOCKER_NETWORK} \
-p 80:80 \
-p 443:443 \
-p "127.0.0.1:${DASHBOARD_PORT}:8080" \
-p "${port}:${port}" \
${extra_port_maps} \
-v /var/run/docker.sock:/var/run/docker.sock:ro \
-v traefik_data:/etc/traefik \
"${current_image}" \
--api.insecure=true \
--providers.docker=true \
--providers.docker.exposedbydefault=false \
--entrypoints.web.address=:80 \
--entrypoints.websecure.address=:443 \
"--entrypoints.${entry_point_name}.address=:${port}" \
${extra_ep_args} \
--certificatesresolvers.letsencrypt.acme.httpchallenge=true \
--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web \
"--certificatesresolvers.letsencrypt.acme.email=${letsencrypt_email}" \
--certificatesresolvers.letsencrypt.acme.storage=/etc/traefik/acme/acme.json \
--providers.file.directory=/etc/traefik/config \
--providers.file.watch=true \
2>/dev/null
if [ $? -eq 0 ]; then
log_info "Traefik container recreated with entry point '${entry_point_name}' on port ${port}"
sleep 3
log_info "Traefik is now running with the new entry point"
else
log_error "Failed to recreate Traefik container"
log_error "Please manually update Traefik configuration"
return 1
fi
}
# Function to setup new domain in existing reverse proxy
setup_new_domain() {
log_info "Setting up new domain: ${DOMAIN_NAME}"
log_info "Domain configuration loaded from: ${DOMAIN_ENV_FILE:-.env}"
if [ "$FRP" = false ]; then
# Check if Traefik is running
if ! docker ps --filter name=^${INSTANCE_NAME}$ --format '{{.Names}}' | grep -q "^traefik$"; then
log_error "Traefik reverse proxy is not running!"
log_error "Please ensure the reverse proxy is setup first"
exit 1
fi
# Check if extra entrypoints need to be applied to the Traefik container
if [ -n "$EXTRA_ENTRYPOINTS" ]; then
parse_extra_entrypoints
if [ ${#EXTRA_ENTRYPOINT_ARGS[@]} -gt 0 ]; then
local current_cmd
current_cmd=$(docker inspect traefik --format='{{json .Config.Cmd}}' 2>/dev/null || echo "[]")
local needs_recreate=false
for ep_arg in "${EXTRA_ENTRYPOINT_ARGS[@]}"; do
if ! echo "$current_cmd" | grep -qF -- "${ep_arg}"; then
needs_recreate=true
break
fi
done
if [ "$needs_recreate" = true ]; then
log_info "Extra entrypoints changed, recreating Traefik container to apply..."
docker stop traefik 2>/dev/null || true
sleep 2
docker rm traefik 2>/dev/null || true
create_traefik_container_local
fi
fi
fi
# Check if Application Service exists locally
if ! check_app_service_local; then
log_error "Application Service not found locally. Please ensure the service '$LOCAL_SERVICE_NAME' is running."
exit 1
fi
# Configure the new domain in Traefik
configure_traefik_local
log_info "New domain ${DOMAIN_NAME} has been configured in Traefik"
log_info "Service will be accessible at: https://${DOMAIN_NAME}"
else
# FRP Mode domain setup
log_info "Setting up new domain in FRP mode..."
# Ensure INSTANCE_NAME is set (from .env or detected from running container)
if [ -z "$INSTANCE_NAME" ]; then
# Try to detect from running FRP containers
local frpc_containers=$(docker ps --filter name="-frpc$" --format '{{.Names}}' 2>/dev/null || true)
if [ -n "$frpc_containers" ]; then
local first_frpc=$(echo "$frpc_containers" | head -1)
INSTANCE_NAME="${first_frpc%-frpc}"
log_info "Detected instance name from running container: ${INSTANCE_NAME}"
else
log_error "INSTANCE_NAME is not set and no FRP container detected."
log_error "Please ensure your .env.rvs-prxy file contains the reverse proxy configuration."
exit 1
fi
fi
THIS_INSTANCE="$INSTANCE_NAME"
log_info "Using instance name: ${THIS_INSTANCE}"
# Check if FRP client container is running
local frpc_container="${THIS_INSTANCE}-frpc"
if ! docker ps --filter name=^${frpc_container}$ --format '{{.Names}}' | grep -q "^${frpc_container}$"; then
log_error "FRP client container '${frpc_container}' is not running!"
log_error "Please ensure the reverse proxy is setup first"
exit 1
fi
# Check if Application Service exists locally
if ! check_app_service_local; then
log_error "Application Service not found locally. Please ensure the service '$LOCAL_SERVICE_NAME' is running."
exit 1
fi
# Generate a new remote port for this domain
local new_remote_port
new_remote_port=$(generate_remote_port)
log_info "Generated remote port ${new_remote_port} for domain ${DOMAIN_NAME}"
# Update frpc.toml with the new proxy configuration
update_frpc_config_for_domain "$new_remote_port"
# Update proxy server configuration
local server_addr
server_addr=$(extract_frp_server_address)
if [ -z "$server_addr" ]; then
log_error "Failed to extract FRP server address from configuration"
exit 1
fi
log_info "Updating proxy server configuration at ${server_addr}..."
update_proxy_server_config "$server_addr" "true" "$new_remote_port"
log_info "New domain ${DOMAIN_NAME} has been configured in FRP tunnel"
log_info "Service will be accessible at: https://${DOMAIN_NAME}"
fi
}
# Function to setup FRP with Traefik
setup_frp_with_traefik() {
log_info "Setting up FRP client for instance: ${THIS_INSTANCE}"
# Pre-request checks
echo -e "${YELLOW}[PRE-REQUEST]${NC} Make sure your current user has permission to execute docker command." >&2
echo -e "${YELLOW}[PRE-REQUEST]${NC} Make sure you have key pairs ~/.ssh/authorized_keys and ~/.ssh/devs1" >&2
echo -e "${YELLOW}[PRE-REQUEST]${NC} Make sure you have key pair ~/.ssh/itl-devs1 for proxy server access" >&2
echo "" >&2
# Auto-proceed with deployment (smooth operation)
echo "Auto-proceeding with deployment..." >&2
echo "" >&2
# Use Docker network from environment variable if available, otherwise use default
if [ -n "$DOCKER_NETWORK" ]; then
SELECTED_NETWORK="$DOCKER_NETWORK"
log_info "Using Docker network from environment variable: ${SELECTED_NETWORK}"
else
SELECTED_NETWORK=""
log_info "Using default Docker network"
fi
# Save the network selection
local network_to_save="$SELECTED_NETWORK"
if [ -z "$network_to_save" ]; then
network_to_save="none"
fi
save_parameters "$network_to_save"
if [ -n "$SELECTED_NETWORK" ]; then
log_info "Selected Docker network: ${SELECTED_NETWORK}"
else
log_info "Using default Docker network"
fi
# Generate FRP client configuration
generate_frpc_config
# Start Docker container
start_docker_container
# Extract FRP server address and remote ports
local server_addr
server_addr=$(extract_frp_server_address)
if [ -z "$server_addr" ]; then
log_error "Failed to extract FRP server address from configuration"
log_info "Skipping proxy server configuration update"
else
log_info "Extracted FRP server address: $server_addr"
# Extract all remote ports dynamically from configuration
local all_ports=()
local ports_string=$(extract_all_remote_ports)
if [ -n "$ports_string" ]; then
all_ports=($ports_string)
fi
# Check if we have at least one port to configure
if [ ${#all_ports[@]} -ge 1 ]; then
log_info "Extracted ${#all_ports[@]} remote port(s) for proxy server update: ${all_ports[*]}"
# Check if we have any web proxies
local has_web=$(has_web_proxy "${proxy_configs[@]}")
# Update proxy server configuration with all extracted ports
update_proxy_server_config "$server_addr" "$has_web" "${all_ports[@]}"
else
log_error "Failed to extract any remote ports from configuration"
log_info "Skipping proxy server configuration update"
fi
fi
# Display container operation commands
display_container_commands
log_info "FRP client instance ${THIS_INSTANCE} deployment completed"
}
# Setup rvs-prxy function (main function that decides FRP vs Traefik)
setup_rvs_prxy() {
if [ "$FRP" = false ]; then
# Run Traefik-only setup
log_info "Running Traefik-only setup as FRP=false"
setup_traefik_only
return 0
fi
log_info "Setting up FRP client for instance: ${THIS_INSTANCE}"
# Pre-request checks
echo -e "${YELLOW}[PRE-REQUEST]${NC} Make sure your current user has permission to execute docker command." >&2
echo -e "${YELLOW}[PRE-REQUEST]${NC} Make sure you have key pairs ~/.ssh/authorized_keys and ~/.ssh/devs1" >&2
echo -e "${YELLOW}[PRE-REQUEST]${NC} Make sure you have key pair ~/.ssh/itl-devs1 for proxy server access" >&2
echo "" >&2
# Auto-proceed with deployment (smooth operation)
echo "Auto-proceeding with deployment..." >&2
echo "" >&2
# Use Docker network from environment variable if available, otherwise use default
if [ -n "$DOCKER_NETWORK" ]; then
SELECTED_NETWORK="$DOCKER_NETWORK"
log_info "Using Docker network from environment variable: ${SELECTED_NETWORK}"
else
SELECTED_NETWORK=""
log_info "Using default Docker network"
fi
# Save the network selection
local network_to_save="$SELECTED_NETWORK"
if [ -z "$network_to_save" ]; then
network_to_save="none"
fi
save_parameters "$network_to_save"
if [ -n "$SELECTED_NETWORK" ]; then
log_info "Selected Docker network: ${SELECTED_NETWORK}"
else
log_info "Using default Docker network"
fi
# Generate FRP client configuration
generate_frpc_config
# Start Docker container
start_docker_container
# Extract FRP server address and remote ports
local server_addr
server_addr=$(extract_frp_server_address)
if [ -z "$server_addr" ]; then
log_error "Failed to extract FRP server address from configuration"
log_info "Skipping proxy server configuration update"
else
log_info "Extracted FRP server address: $server_addr"
# Extract all remote ports dynamically from configuration
local all_ports=()
local ports_string=$(extract_all_remote_ports)
if [ -n "$ports_string" ]; then
all_ports=($ports_string)
fi
# Check if we have at least one port to configure
if [ ${#all_ports[@]} -ge 1 ]; then
log_info "Extracted ${#all_ports[@]} remote port(s) for proxy server update: ${all_ports[*]}"
# Check if we have any web proxies
local has_web=$(has_web_proxy "${proxy_configs[@]}")
# Update proxy server configuration with all extracted ports
update_proxy_server_config "$server_addr" "$has_web" "${all_ports[@]}"
else
log_error "Failed to extract any remote ports from configuration"
log_info "Skipping proxy server configuration update"
fi
fi
# Display container operation commands
display_container_commands
log_info "FRP client instance ${THIS_INSTANCE} deployment completed"
}
# List available systems
list_systems() {
echo -e "${GREEN}Available deployment systems:${NC}" >&2
echo -e "${YELLOW}============================${NC}" >&2
echo -e "${BLUE}•${NC} setup-rvs-prxy" >&2
echo -e "${YELLOW}============================${NC}" >&2
echo -e "Use: ${GREEN}$0 <system-name>${NC} to deploy a system" >&2
}
# Usage function
usage() {
cat << EOF
Usage: curl -s "<url>/deploy-rvs-prxy.sh" | bash
Automated deployment script for Reverse Proxy setup.
Modes:
1) Setup Reverse Proxy - Deploys Traefik or FRP Tunnel with Traefik
2) Setup New Domain - Adds a new domain to a local Docker application container
3) Setup Port Exposure - Exposes a host port to connect to a container port
4) Setup External Service Domain - Routes a domain to an external web service URL (Traefik mode only)
5) Manage Configurations - List and remove existing domains, external services and ports
The script automatically detects the mode based on your configuration:
- If no reverse proxy exists: Setup Reverse Proxy mode
- If reverse proxy exists and DOMAIN_NAME is set: Setup New Domain mode
- If reverse proxy exists and HOST_PORT is set: Setup Port Exposure mode
- If reverse proxy exists and EXTERNAL_URL is set: Setup External Service Domain mode
Environment Variables (in .env.rvs-prxy file):
FRP=true|false Set to 'false' for Traefik mode, 'true' for FRP mode
INSTANCE_NAME Container name for Traefik (default: traefik)
DOCKER_NETWORK Docker network name (default: traefik-public)
DASHBOARD_PORT Traefik dashboard port on localhost (default: 18080)
Traefik Mode Variables (FRP=false):
LOCAL_SERVICE_NAME Name of the local Docker application container
LOCAL_SERVICE_PORT Port(s) of the local app service, e.g. 80 or 80,4443/udp
DOMAIN_NAME Domain name for reverse proxy routing
TRAEFIK_VER Traefik version (default: v3.6.9)
LETSENCRYPT_EMAIL Email for Let's Encrypt certificates
FRP Mode Variables (FRP=true):
FRP_SERVER_ADDR FRP server address (IP or hostname)
FRP_SERVER_PORT FRP server port (default: 16999)
INTERNAL_SERVICE_PORT Internal service port (default: 3000)
Port Exposure Variables (in .env-port-<port> file):
HOST_PORT Port number to expose on the host (e.g., 18081)
CONTAINER_NAME Name of the target Docker container
CONTAINER_PORT Port number inside the container to connect to
External Service Variables (in .env-ext-<domain> file):
DOMAIN_NAME Public domain to route (sub-path allowed, e.g. app.example.com/api)
EXTERNAL_URL Upstream URL of the external web service
(e.g. https://cornersendbox-impact-vms.lovable.app/)
Examples:
# First run - will trigger interactive wizard
curl -s "<url>/deploy-rvs-prxy.sh" | bash
# With existing .env.rvs-prxy file
curl -s "<url>/deploy-rvs-prxy.sh" | bash
# Setup port exposure - create .env-port-18081 file and run
curl -s "<url>/deploy-rvs-prxy.sh" | bash
# Manage existing domains and ports (interactive menu)
curl -s "<url>/deploy-rvs-prxy.sh" | bash -s -- --manage
Note: This script runs on the same computer as the service it connects to.
IMPORTANT: Configuration is primarily handled through .env.rvs-prxy file.
The .env.rvs-prxy file contains all parameters for your setup and is essential for operation.
Port Exposure:
Create a file named .env-port-<port> (e.g., .env-port-18081) with:
HOST_PORT="18081"
CONTAINER_NAME="your-container-name"
CONTAINER_PORT="80"
Then run the script to expose host port 18081 -> container port 80
External Service Domain:
Create a file named .env-ext-<domain> (e.g., .env-ext-app-example-com) with:
DOMAIN_NAME="app.example.com"
EXTERNAL_URL="https://cornersendbox-impact-vms.lovable.app/"
Then run the script. Traefik terminates TLS for app.example.com (Let's Encrypt)
and proxies to the external URL. The origin receives its own Host header
(passHostHeader=false); a sub-path in DOMAIN_NAME is stripped, and any path in
EXTERNAL_URL is added back to the origin request. Traefik mode only.
EOF
}
# Manage existing domain, external service and port configurations
manage_configurations() {
echo -e "${BLUE}=============================================${NC}" >&2
echo -e "${GREEN}Reverse Proxy Configuration Manager${NC}" >&2
echo -e "${BLUE}=============================================${NC}" >&2
echo "" >&2
# Collect domain files
local domain_files=()
for f in .env-*; do
[ -f "$f" ] || continue
[[ "$f" == .env.rvs-prxy* ]] && continue
[[ "$f" == .env-port-* ]] && continue
[[ "$f" == .env-ext-* ]] && continue
domain_files+=("$f")
done
# Collect port files
local port_files=()
for f in .env-port-*; do
[ -f "$f" ] && port_files+=("$f")
done
# Collect external service files
local ext_files=()
for f in .env-ext-*; do
[ -f "$f" ] && ext_files+=("$f")
done
local total_configs=$((${#domain_files[@]} + ${#port_files[@]} + ${#ext_files[@]}))
if [ "$total_configs" -eq 0 ]; then
log_info "No domain, external service or port configurations found."
return 0
fi
# Display configurations
echo -e "${YELLOW}Current Configurations:${NC}" >&2
echo "" >&2
local idx=1
if [ ${#domain_files[@]} -gt 0 ]; then
echo -e "${BLUE}Domains:${NC}" >&2
for f in "${domain_files[@]}"; do
local domain_name
domain_name=$(grep -o 'DOMAIN_NAME="[^"]*"' "$f" 2>/dev/null | sed 's/DOMAIN_NAME="//;s/"//' || echo "unknown")
local service_name
service_name=$(grep -o 'LOCAL_SERVICE_NAME="[^"]*"' "$f" 2>/dev/null | sed 's/LOCAL_SERVICE_NAME="//;s/"//' || echo "unknown")
echo -e " ${GREEN}${idx}.${NC} ${f} - ${domain_name} (${service_name})" >&2
idx=$((idx + 1))
done
echo "" >&2
fi
if [ ${#port_files[@]} -gt 0 ]; then
echo -e "${BLUE}Ports:${NC}" >&2
for f in "${port_files[@]}"; do
local host_port
host_port=$(grep -o 'HOST_PORT="[^"]*"' "$f" 2>/dev/null | sed 's/HOST_PORT="//;s/"//' || echo "unknown")
local container_name
container_name=$(grep -o 'CONTAINER_NAME="[^"]*"' "$f" 2>/dev/null | sed 's/CONTAINER_NAME="//;s/"//' || echo "unknown")
echo -e " ${GREEN}${idx}.${NC} ${f} - Host port ${host_port} -> ${container_name}" >&2
idx=$((idx + 1))
done
echo "" >&2
fi
if [ ${#ext_files[@]} -gt 0 ]; then
echo -e "${BLUE}External Services:${NC}" >&2
for f in "${ext_files[@]}"; do
local domain_name
domain_name=$(grep -o 'DOMAIN_NAME="[^"]*"' "$f" 2>/dev/null | sed 's/DOMAIN_NAME="//;s/"//' || echo "unknown")
local external_url
external_url=$(grep -o 'EXTERNAL_URL="[^"]*"' "$f" 2>/dev/null | sed 's/EXTERNAL_URL="//;s/"//' || echo "unknown")
echo -e " ${GREEN}${idx}.${NC} ${f} - ${domain_name} → ${external_url}" >&2
idx=$((idx + 1))
done
echo "" >&2
fi
# Prompt for removal
echo -e "${YELLOW}Select configurations to remove (enter numbers separated by space, or 'q' to quit):${NC}" >&2
read -r selection </dev/tty
if [ "$selection" = "q" ] || [ -z "$selection" ]; then
log_info "No changes made."
return 0
fi
# Build array of all config files
local all_files=("${domain_files[@]}" "${port_files[@]}" "${ext_files[@]}")
# Track if Traefik needs restart (for port removals)
local traefik_restart_needed=false
local ports_to_remove=()
# Process selection
for num in $selection; do
if [[ "$num" =~ ^[0-9]+$ ]] && [ "$num" -ge 1 ] && [ "$num" -le "$total_configs" ]; then
local file_to_remove="${all_files[$((num-1))]}"
if [ -f "$file_to_remove" ]; then
log_info "Removing: ${file_to_remove}"
# Check if it's an external service config
if [[ "$file_to_remove" == .env-ext-* ]]; then
# Remove external service config from Traefik
local domain_name
domain_name=$(grep -o 'DOMAIN_NAME="[^"]*"' "$file_to_remove" 2>/dev/null | sed 's/DOMAIN_NAME="//;s/"//' || echo "")
if [ -n "$domain_name" ]; then
log_info "Removing external service config from Traefik: ${domain_name}"
# Remove the dynamic config file from Traefik volume
# Same domain_base logic as configure_traefik_external():
# cut -d'/' -f1 to match the actual filename
local domain_base
domain_base=$(echo "$domain_name" | cut -d'/' -f1)
docker run --rm -v "traefik_data:/etc/traefik" alpine:latest sh -c "rm -f /etc/traefik/config/app-ext-${domain_base}.yml 2>/dev/null" || true
fi
# Check if it's a domain config
elif [[ "$file_to_remove" == .env-* ]] && [[ "$file_to_remove" != .env-port-* ]] && [[ "$file_to_remove" != .env-ext-* ]]; then
# Remove domain config from Traefik volume
local domain_name
domain_name=$(grep -o 'DOMAIN_NAME="[^"]*"' "$file_to_remove" 2>/dev/null | sed 's/DOMAIN_NAME="//;s/"//' || echo "")
local service_name
service_name=$(grep -o 'LOCAL_SERVICE_NAME="[^"]*"' "$file_to_remove" 2>/dev/null | sed 's/LOCAL_SERVICE_NAME="//;s/"//' || echo "")
if [ -n "$domain_name" ] && [ -n "$service_name" ]; then
log_info "Removing domain config from Traefik: ${domain_name}"
# Remove the dynamic config file from Traefik volume
# Must use same domain_base logic as configure_traefik_local():
# cut -d'/' -f1 (not dots-to-dashes) to match actual filename
local domain_base
domain_base=$(echo "$domain_name" | cut -d'/' -f1)
docker run --rm -v "traefik_data:/etc/traefik" alpine:latest sh -c "rm -f /etc/traefik/config/app-service-${domain_base}-${service_name}.yml 2>/dev/null" || true
fi
elif [[ "$file_to_remove" == .env-port-* ]]; then
# Remove port config from Traefik
local host_port
host_port=$(grep -o 'HOST_PORT="[^"]*"' "$file_to_remove" 2>/dev/null | sed 's/HOST_PORT="//;s/"//' || echo "")
local container_name
container_name=$(grep -o 'CONTAINER_NAME="[^"]*"' "$file_to_remove" 2>/dev/null | sed 's/CONTAINER_NAME="//;s/"//' || echo "")
if [ -n "$host_port" ]; then
log_info "Removing port config from Traefik: port ${host_port}"
# Remove the dynamic config file from Traefik volume
# Use exact filename: port-{host_port}-{container_name}.yml
docker run --rm -v "traefik_data:/etc/traefik" alpine:latest sh -c "rm -f /etc/traefik/config/port-${host_port}-${container_name}.yml 2>/dev/null" || true
# Mark for entry point removal
ports_to_remove+=("$host_port")
traefik_restart_needed=true
fi
fi
# Remove the config file
rm -f "$file_to_remove"
log_info "Removed: ${file_to_remove}"
fi
else
log_warn "Invalid selection: ${num}"
fi
done
# If ports were removed, we need to recreate Traefik container without those entry points
if [ "$traefik_restart_needed" = true ]; then
log_info "Updating Traefik container to remove entry points..."
# Get current Traefik configuration
local current_image
current_image=$(docker inspect traefik --format='{{.Config.Image}}' 2>/dev/null || echo "traefik:latest")
local current_cmd
current_cmd=$(docker inspect traefik --format='{{json .Config.Cmd}}' 2>/dev/null || echo "[]")
# Extract Let's Encrypt email from existing config
local letsencrypt_email
letsencrypt_email=$(echo "$current_cmd" | grep -o 'acme\.email=[^",]*' | sed 's/acme\.email=//' | head -1)
if [ -z "$letsencrypt_email" ]; then
letsencrypt_email="admin@example.com"
fi
# Extract existing entry points (excluding the ones we're removing)
local existing_entrypoints
existing_entrypoints=$(echo "$current_cmd" | grep -o 'entrypoints\.[a-zA-Z0-9_-]*\.address=[^"]*' | tr -d '"' || echo "")
# Also include extra entrypoints from EXTRA_ENTRYPOINTS config
parse_extra_entrypoints
local extra_ep_args=""
local extra_port_maps=""
for ep_arg in "${EXTRA_ENTRYPOINT_ARGS[@]}"; do
extra_ep_args="${extra_ep_args} ${ep_arg}"
done
for pm in "${EXTRA_PORT_MAPPINGS[@]}"; do
extra_port_maps="${extra_port_maps} -p ${pm}"
done
# Build new entry point list excluding removed ports
local new_entrypoint_args="--entrypoints.web.address=:80 --entrypoints.websecure.address=:443 ${extra_ep_args}"
local new_port_mappings="-p 80:80 -p 443:443 -p 127.0.0.1:${DASHBOARD_PORT}:8080 ${extra_port_maps}"
# Parse existing entry points (skip web/websecure and extra entries already covered)
while IFS= read -r ep; do
if [ -n "$ep" ]; then
local ep_name
ep_name=$(echo "$ep" | sed 's/entrypoints\.\(.*\)\.address=.*/\1/')
local ep_address
ep_address=$(echo "$ep" | sed 's/entrypoints\.[a-zA-Z0-9_-]*\.address=//')
local ep_port
ep_port=$(echo "$ep_address" | sed 's/^://' | sed 's/\/.*//')
# Check if this is already covered by extra entrypoints
local already_covered=false
for ep_arg in "${EXTRA_ENTRYPOINT_ARGS[@]}"; do
if echo "$ep_arg" | grep -q "entrypoints.${ep_name}.address"; then
already_covered=true
break
fi
done
# Check if this port is being removed
local should_skip=false
for remove_port in "${ports_to_remove[@]}"; do
if [ "$ep_port" = "$remove_port" ]; then
should_skip=true
break
fi
done
if [ "$should_skip" = false ] && [ "$ep_name" != "web" ] && [ "$ep_name" != "websecure" ] && [ "$already_covered" = false ]; then
new_entrypoint_args="${new_entrypoint_args} --entrypoints.${ep_name}.address=${ep_address}"
# Handle protocol suffix in port mapping
if [[ "$ep_address" == *"/udp" ]]; then
new_port_mappings="${new_port_mappings} -p ${ep_port}:${ep_port}/udp"
else
new_port_mappings="${new_port_mappings} -p ${ep_port}:${ep_port}"
fi
fi
fi
done <<< "$existing_entrypoints"
# Stop and remove current container
log_info "Stopping Traefik container..."
docker stop traefik 2>/dev/null || true
sleep 2
docker rm traefik 2>/dev/null || true
# Recreate container with updated entry points
eval docker run -d \
--name traefik \
--restart=unless-stopped \
--network ${DOCKER_NETWORK} \
${new_port_mappings} \
-v /var/run/docker.sock:/var/run/docker.sock:ro \
-v traefik_data:/etc/traefik \
"\"${current_image}\"" \
--api.insecure=true \
--providers.docker=true \
--providers.docker.exposedbydefault=false \
${new_entrypoint_args} \
--certificatesresolvers.letsencrypt.acme.httpchallenge=true \
--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web \
--certificatesresolvers.letsencrypt.acme.email="\"${letsencrypt_email}\"" \
--certificatesresolvers.letsencrypt.acme.storage=/etc/traefik/acme/acme.json \
--providers.file.directory=/etc/traefik/config \
--providers.file.watch=true \
2>/dev/null
if [ $? -eq 0 ]; then
log_info "Traefik container recreated without removed entry points"
sleep 3
else
log_error "Failed to recreate Traefik container"
fi
fi
echo "" >&2
log_info "Configuration cleanup complete."
}
# Main execution
main() {
# Check for help options first (before loading .env)
if [ "$1" = "-h" ] || [ "$1" = "--help" ] || [ "$1" = "help" ]; then
usage
exit 0
fi
# Check for manage option
if [ "$1" = "--manage" ] || [ "$1" = "-m" ]; then
manage_configurations
exit 0
fi
# Load environment variables from .env.rvs-prxy files (or trigger interactive wizard)
load_env
# Execute based on detected deployment mode
if [ "$DEPLOY_MODE" = "port" ]; then
log_info "Setting up port exposure in existing Traefik proxy..."
# Process all port config files in batch mode
if [ ${#PORT_ENV_FILES[@]} -gt 0 ]; then
local total=${#PORT_ENV_FILES[@]}
local count=0
for env_file in "${PORT_ENV_FILES[@]}"; do
count=$((count + 1))
echo "" >&2
log_info "========================================="
log_info "Processing port $count of $total: $env_file"
log_info "========================================="
echo "" >&2
# Source the port config file
source "$env_file"
PORT_ENV_FILE="$env_file"
# Validate port config
if [ -z "$HOST_PORT" ]; then
log_error "HOST_PORT is required in $env_file"
continue
fi
if [ -z "$CONTAINER_NAME" ] || [ -z "$CONTAINER_PORT" ]; then
log_error "CONTAINER_NAME and CONTAINER_PORT are required in $env_file"
continue
fi
# Setup the port exposure
setup_port_exposure
done
echo "" >&2
log_info "========================================="
log_info "Batch processing completed: $count port(s) processed"
log_info "========================================="
else
# Fallback for single port config
setup_port_exposure
fi
elif [ "$DEPLOY_MODE" = "domain" ]; then
log_info "Setting up new domain in existing reverse proxy..."
# Process all domain config files in batch mode
if [ ${#DOMAIN_ENV_FILES[@]} -gt 0 ]; then
local total=${#DOMAIN_ENV_FILES[@]}
local count=0
for env_file in "${DOMAIN_ENV_FILES[@]}"; do
count=$((count + 1))
# Skip .env.rvs-prxy.development and .env.rvs-prxy.production
if [ "$env_file" = ".env.rvs-prxy.development" ] || [ "$env_file" = ".env.rvs-prxy.production" ]; then
continue
fi
echo "" >&2
log_info "========================================="
log_info "Processing domain $count of $total: $env_file"
log_info "========================================="
echo "" >&2
# Source the domain config file
source "$env_file"
DOMAIN_ENV_FILE="$env_file"
# Validate domain config
if [ -z "$DOMAIN_NAME" ]; then
log_error "DOMAIN_NAME is required in $env_file"
continue
fi
if [ -z "$LOCAL_SERVICE_NAME" ] || [ -z "$LOCAL_SERVICE_PORT" ]; then
log_error "LOCAL_SERVICE_NAME and LOCAL_SERVICE_PORT are required in $env_file"
continue
fi
# Setup the domain
setup_new_domain
done
echo "" >&2
log_info "========================================="
log_info "Batch processing completed: $count domain(s) processed"
log_info "========================================="
else
# Fallback for backward compatibility
setup_new_domain
fi
elif [ "$DEPLOY_MODE" = "external" ]; then
log_info "Setting up external service domain in existing Traefik proxy..."
# Process all external service config files in batch mode
if [ ${#EXT_ENV_FILES[@]} -gt 0 ]; then
local total=${#EXT_ENV_FILES[@]}
local count=0
for env_file in "${EXT_ENV_FILES[@]}"; do
count=$((count + 1))
echo "" >&2
log_info "========================================="
log_info "Processing external service $count of $total: $env_file"
log_info "========================================="
echo "" >&2
# Source the external service config file
source "$env_file"
EXT_ENV_FILE="$env_file"
# Validate external service config
if [ -z "$DOMAIN_NAME" ]; then
log_error "DOMAIN_NAME is required in $env_file"
continue
fi
if [ -z "$EXTERNAL_URL" ]; then
log_error "EXTERNAL_URL is required in $env_file"
continue
fi
# Setup the external service domain
setup_external_domain
done
echo "" >&2
log_info "========================================="
log_info "Batch processing completed: $count external service domain(s) processed"
log_info "========================================="
else
# Fallback for single external service config
setup_external_domain
fi
elif [ "$DEPLOY_MODE" = "domain_wizard" ]; then
log_info "Starting interactive wizard for domain setup..."
interactive_setup_wizard
# The wizard may create a domain, port exposure or external service config
if [ "$DEPLOY_MODE" = "external" ] && [ -n "$EXT_ENV_FILE" ]; then
source "$EXT_ENV_FILE"
setup_external_domain
elif [ "$DEPLOY_MODE" = "port" ] && [ -n "$PORT_ENV_FILE" ]; then
source "$PORT_ENV_FILE"
setup_port_exposure
else
if [ -n "$DOMAIN_ENV_FILE" ]; then
source "$DOMAIN_ENV_FILE"
fi
setup_new_domain
fi
else
log_info "Setting up reverse proxy..."
setup_reverse_proxy
fi
}
# Function to prompt for domain name
prompt_domain_name() {
# Check if DOMAIN_NAME is set from environment
if [ -n "$DOMAIN_NAME" ]; then
log_info "Using environment variable for domain name: ${DOMAIN_NAME}"
echo "$DOMAIN_NAME"
return
fi
echo -e "${BLUE}=== Domain Name Configuration ===${NC}" >&2
echo -e "${YELLOW}Please enter the domain name for this instance${NC}" >&2
echo -e "${YELLOW}(e.g., cmty-repos2.ivantito.com)${NC}" >&2
echo "" >&2
local domain_name=""
while [ -z "$domain_name" ]; do
domain_name=$(read_terminal "Enter domain name: " "")
if [ -z "$domain_name" ]; then
log_error "Domain name cannot be empty" >&2
fi
done
echo "$domain_name"
}
# Function to display post-execution command reference for Traefik
show_traefik_commands() {
local domain_name="${DOMAIN_NAME:-your-domain.com}"
echo ""
echo "============================================="
echo "Container Operation Commands (Traefik Instance)"
echo ""
echo "Configuration Note:"
echo " By default, Traefik does not restart after configuration (RESTART_TRAEFIK=false)"
echo " Traefik will dynamically load new configurations automatically."
echo " Set RESTART_TRAEFIK=true in .env to force restart after configuration."
echo ""
echo "Run Container locally:"
echo " # Use the stored docker-compose.yml from volume"
echo " # First try to copy with 1000:1000 ownership, fallback to default if chown fails"
echo " docker run -d --name ${INSTANCE_NAME} --restart=unless-stopped \\"
echo " -p 80:80 -p 443:443 -p 127.0.0.1:${DASHBOARD_PORT}:8080 \\"
echo " -v /var/run/docker.sock:/var/run/docker.sock:ro \\"
echo " -v traefik_data:/etc/traefik traefik:latest"
echo ""
echo "Restart Container: docker restart traefik"
echo "------------------------------------------------------------"
echo "Container Accessing by ubuntu: docker exec -it traefik /bin/sh"
echo "Container Accessing by root: docker exec -it -u root traefik /bin/sh"
echo "------------------------------------------------------------"
echo "View the Logs: docker logs --tail 50 traefik"
echo "------------------------------------------------------------"
echo "Inspect: docker inspect traefik"
echo "------------------------------------------------------------"
echo "Remove Container: docker rm -f traefik"
echo "------------------------------------------------------------"
echo "View Traefik Dashboard: http://localhost:8080 (only accessible locally for security)"
echo "------------------------------------------------------------"
echo "Your application is accessible via: https://$domain_name"
echo "------------------------------------------------------------"
echo "Totally Remove (Pay Attention):"
echo " docker rm -f traefik"
echo " docker volume rm traefik_data"
echo " docker rmi traefik:$TRAEFIK_VER"
echo "============================================="
}
# Execute main function with arguments
main "$@"