#!/bin/bash # deploy-rvs-prxy.sh - Automated deployment script for Reverse Proxy setup # This script supports two modes: # 1) Setup Reverse Proxy (Traefik or FRP Tunnel with Traefik) # 2) Setup New Domain into existing reverse proxy # This script runs on the same computer as the service it connects to 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 # 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), or "port" (expose host port) DEPLOY_MODE="" # Domain configuration file pattern: .env-your-domain-com DOMAIN_ENV_FILE="" # Port configuration file pattern: .env-port- PORT_ENV_FILE="" # 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=() # 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 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 # 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 # 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 [ "$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" 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- 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- 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- 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- file" 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- file save_port_env() { local host_port="$1" local container_name="$2" local container_port="$3" # Create port-specific filename: .env-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 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 &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 "" >&2 while true; do setup_choice=$(read_terminal "Select option [1-2] (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 echo -e "Enter your application's internal port:" >&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]+$ ]] || [ "$LOCAL_SERVICE_PORT" -lt 1 ] || [ "$LOCAL_SERVICE_PORT" -gt 65535 ]; then echo -e "${RED}Invalid port. Please enter a number between 1 and 65535.${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 ;; *) echo -e "${RED}Invalid choice. Please enter 1 or 2.${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 # 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 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" # Only update the config files in the volume, don't recreate the container log_info "Updating configuration in existing Traefik container..." # 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" # 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' - '--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' - "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' - '--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' - "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 rm -f "$temp_compose" 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 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..." # 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: ${LOCAL_SERVICE_NAME}-https: rule: 'Host(\`${domain_part}\`) && PathPrefix(\`${path_part}\`)' service: ${LOCAL_SERVICE_NAME}-service tls: certResolver: letsencrypt entryPoints: - websecure middlewares: - ${LOCAL_SERVICE_NAME}-strip-prefix ${LOCAL_SERVICE_NAME}-http: rule: 'Host(\`${domain_part}\`) && PathPrefix(\`${path_part}\`)' service: ${LOCAL_SERVICE_NAME}-service entryPoints: - web middlewares: - redirect-to-https - ${LOCAL_SERVICE_NAME}-strip-prefix middlewares: ${LOCAL_SERVICE_NAME}-strip-prefix: stripPrefix: prefixes: - ${path_part} services: ${LOCAL_SERVICE_NAME}-service: loadBalancer: servers: - url: 'http://${LOCAL_SERVICE_NAME}:${LOCAL_SERVICE_PORT}' passHostHeader: true EOF else # Configure for root domain routing cat > "$temp_config" << EOF http: routers: ${LOCAL_SERVICE_NAME}-https: rule: 'Host(\`${DOMAIN_NAME}\`)' service: ${LOCAL_SERVICE_NAME}-service tls: certResolver: letsencrypt entryPoints: - websecure ${LOCAL_SERVICE_NAME}-http: rule: 'Host(\`${DOMAIN_NAME}\`)' service: ${LOCAL_SERVICE_NAME}-service entryPoints: - web middlewares: - redirect-to-https services: ${LOCAL_SERVICE_NAME}-service: loadBalancer: servers: - url: 'http://${LOCAL_SERVICE_NAME}:${LOCAL_SERVICE_PORT}' passHostHeader: true EOF 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 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_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}" # 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 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}" \ -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}" \ --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 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 ${NC} to deploy a system" >&2 } # Usage function usage() { cat << EOF Usage: curl -s "/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 existing reverse proxy 3) Setup Port Exposure - Exposes a host port to connect to a container port 4) Manage Configurations - List and remove existing domains 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 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 number of the local application service 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- 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 Examples: # First run - will trigger interactive wizard curl -s "/deploy-rvs-prxy.sh" | bash # With existing .env.rvs-prxy file curl -s "/deploy-rvs-prxy.sh" | bash # Setup port exposure - create .env-port-18081 file and run curl -s "/deploy-rvs-prxy.sh" | bash # Manage existing domains and ports (interactive menu) curl -s "/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- (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 EOF } # Function to manage existing domain 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 domain_files+=("$f") done # Collect port files local port_files=() for f in .env-port-*; do [ -f "$f" ] && port_files+=("$f") done local total_configs=$((${#domain_files[@]} + ${#port_files[@]})) if [ "$total_configs" -eq 0 ]; then log_info "No domain 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 # 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/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 # Use specific pattern: app-service-{domain}-{service}.yml # This avoids accidentally removing port config files that share the same service name local domain_base domain_base=$(echo "$domain_name" | sed 's/\./-/g') 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 "") # Build new entry point list excluding removed ports local new_entrypoint_args="--entrypoints.web.address=:80 --entrypoints.websecure.address=:443" local new_port_mappings="-p 80:80 -p 443:443 -p 127.0.0.1:${DASHBOARD_PORT}:8080" # Parse existing entry points while IFS= read -r ep; do if [ -n "$ep" ]; then local ep_name ep_name=$(echo "$ep" | sed 's/entrypoints\.\(.*\)\.address=.*/\1/') local ep_port ep_port=$(echo "$ep" | grep -o ':[0-9]*' | sed 's/://') # 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" ]; then new_entrypoint_args="${new_entrypoint_args} --entrypoints.${ep_name}.address=:${ep_port}" new_port_mappings="${new_port_mappings} -p ${ep_port}:${ep_port}" 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" = "domain_wizard" ]; then log_info "Starting interactive wizard for domain setup..." interactive_setup_wizard # Source the newly created domain config if [ -n "$DOMAIN_ENV_FILE" ]; then source "$DOMAIN_ENV_FILE" fi setup_new_domain 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 "$@"