rvs-prxy/deploy-rvs-prxy.sh
2026-02-26 06:54:15 +00:00

2419 lines
96 KiB
Bash
Executable File

#!/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
# Target server variables (for Traefik mode - now running locally)
LOCAL_SERVICE_NAME=""
LOCAL_SERVICE_PORT=""
LOCAL_DOCKER_NETWORK=""
# Deployment mode: "proxy" (setup reverse proxy) or "domain" (setup new domain)
DEPLOY_MODE=""
# Domain configuration file pattern: .env-your-domain-com
DOMAIN_ENV_FILE=""
# Function to load environment variables from .env files
load_env() {
local proxy_env_loaded=false
local domain_env_loaded=false
# Step 1: Load reverse proxy instance configuration from .env
# Check for .env files in priority order: .env.development, .env.production, .env
if [ -f ".env.development" ]; then
echo -e "${BLUE}[INFO]${NC} Loading reverse proxy config from .env.development" >&2
source .env.development
proxy_env_loaded=true
elif [ -f ".env.production" ]; then
echo -e "${BLUE}[INFO]${NC} Loading reverse proxy config from .env.production" >&2
source .env.production
proxy_env_loaded=true
elif [ -f ".env" ]; then
echo -e "${BLUE}[INFO]${NC} Loading reverse proxy config from .env" >&2
source .env
proxy_env_loaded=true
elif [ -f "$(dirname "$0")/.env.development" ]; then
echo -e "${BLUE}[INFO]${NC} Loading reverse proxy config from $(dirname "$0")/.env.development" >&2
source "$(dirname "$0")/.env.development"
proxy_env_loaded=true
elif [ -f "$(dirname "$0")/.env.production" ]; then
echo -e "${BLUE}[INFO]${NC} Loading reverse proxy config from $(dirname "$0")/.env.production" >&2
source "$(dirname "$0")/.env.production"
proxy_env_loaded=true
elif [ -f "$(dirname "$0")/.env" ]; then
echo -e "${BLUE}[INFO]${NC} Loading reverse proxy config from $(dirname "$0")/.env" >&2
source "$(dirname "$0")/.env"
proxy_env_loaded=true
fi
# Step 2: Look for 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.
for env_file in .env-*; do
# Skip .env.development and .env.production
if [ "$env_file" = ".env.development" ] || [ "$env_file" = ".env.production" ]; then
continue
fi
if [ -f "$env_file" ]; then
echo -e "${BLUE}[INFO]${NC} Loading domain config from $env_file" >&2
source "$env_file"
domain_env_loaded=true
DOMAIN_ENV_FILE="$env_file"
break # Load only the first domain file found
fi
done
# Determine deployment mode based on what configuration files exist
if [ "$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_env_loaded" = true ]; then
# Only proxy config exists - check if proxy is already running
local proxy_exists=false
if [ "$FRP" = "false" ] || [ "$FRP" = "False" ] || [ "$FRP" = "FALSE" ]; then
FRP=false
if docker ps -a --filter name=^traefik$ --format '{{.Names}}' | grep -q "^traefik$"; then
proxy_exists=true
fi
else
FRP=true
if [ -n "$INSTANCE_NAME" ]; then
local frpc_container="${INSTANCE_NAME}-frpc"
if docker ps -a --filter name=^${frpc_container}$ --format '{{.Names}}' | grep -q "^${frpc_container}$"; then
proxy_exists=true
fi
fi
fi
if [ "$proxy_exists" = true ]; then
# Proxy exists but no domain config - need domain setup via wizard
DEPLOY_MODE="domain_wizard"
log_info "Deployment mode: Setup New Domain (proxy running, no domain config)"
else
# Proxy config exists but proxy not running - setup proxy
DEPLOY_MODE="proxy"
log_info "Deployment mode: Setup Reverse Proxy (config exists, proxy not running)"
fi
else
# No configuration files found - 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"
else
source .env
fi
fi
# Validate required parameters based on mode (skip validation for domain_wizard - will be validated after wizard)
if [ "$DEPLOY_MODE" = "domain" ]; then
# Domain config file exists - validate it
if [ -z "$DOMAIN_NAME" ]; then
log_error "DOMAIN_NAME is required for domain setup mode"
log_error "Please set DOMAIN_NAME in your .env-<domain> file"
exit 1
fi
if [ -z "$LOCAL_SERVICE_NAME" ] || [ -z "$LOCAL_SERVICE_PORT" ]; then
log_error "LOCAL_SERVICE_NAME and LOCAL_SERVICE_PORT are required for domain setup"
log_error "Please set these in your .env-<domain> file"
exit 1
fi
elif [ "$DEPLOY_MODE" = "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 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 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 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 file
save_to_env() {
local var_name="$1"
local var_value="$2"
local target_file="${3:-.env}"
# Create or update .env 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 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 read from terminal instead of pipe
read_terminal() {
local prompt="$1"
local default_value="$2"
# Read from terminal (/dev/tty) instead of stdin
read -p "$prompt" input_name </dev/tty
echo "${input_name:-$default_value}"
}
# Interactive setup wizard for missing .env file
interactive_setup_wizard() {
echo -e "${BLUE}=============================================${NC}" >&2
echo -e "${GREEN}Welcome to Deploy Reverse Proxy Setup Wizard${NC}" >&2
echo -e "${BLUE}=============================================${NC}" >&2
echo "" >&2
# Check if reverse proxy already exists
local proxy_exists=false
if docker ps -a --filter name=^traefik$ --format '{{.Names}}' | grep -q "^traefik$"; then
proxy_exists=true
fi
if [ "$proxy_exists" = true ]; then
# Reverse proxy exists, offer to setup new domain
echo -e "${YELLOW}Existing reverse proxy (Traefik) detected.${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 file
save_domain_env "$DOMAIN_NAME" "$LOCAL_SERVICE_NAME" "$LOCAL_SERVICE_PORT"
# Load FRP setting from existing .env if it exists
if [ -f ".env" ]; then
source .env
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)"
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
# Create new .env file
> .env
# 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
read_terminal "Select mode [1-2] (default: 1 for Traefik): " "1"
mode_choice="$REPLY"
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"
echo "" >&2
# Step 2: Mode-specific configuration
if [ "$FRP" = false ]; then
# Traefik Mode configuration - Reverse Proxy Instance settings only
echo -e "${BLUE}--- Step 2: 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"
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"
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"
else
save_to_env "DEBUG" "false" ".env"
fi
echo "" >&2
else
# FRP Mode configuration - Reverse Proxy Instance settings only
echo -e "${BLUE}--- Step 2: 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"
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"
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"
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
else
echo -e "${GREEN}Configuration saved to .env${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 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 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 execute Docker commands locally (Traefik Mode - runs on same server)
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:8080: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: traefik-public
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:8080: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: traefik-public
external: true
EOF
fi
# Create the network if it doesn't exist
docker network create traefik-public 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 traefik-public network
local service_in_network
service_in_network=$(docker inspect "$LOCAL_SERVICE_NAME" 2>/dev/null | grep -o '"traefik-public"' || true)
if [ -z "$service_in_network" ]; then
log_error "Target application service '$LOCAL_SERVICE_NAME' is not connected to 'traefik-public' network."
log_error "Please ensure the target application container is connected to the 'traefik-public' network."
log_error "Application should connecting to traefik-public network, here is the docker compose yml example:"
log_error " services:"
log_error " you-app:"
log_error " networks:"
log_error " - traefik-public"
log_error " networks:"
log_error " traefik-public:"
log_error " external: true"
log_error ""
log_error "You can connect it with: docker network connect traefik-public $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 traefik-public network to ensure communication
docker network connect traefik-public 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 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=^traefik$ --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
log_error "Domain setup for FRP mode is not yet implemented"
exit 1
fi
}
# Function to setup FRP with Traefik
setup_frp_with_traefik() {
log_info "Setting up FRP client for instance: ${THIS_INSTANCE}"
# Pre-request checks
echo -e "${YELLOW}[PRE-REQUEST]${NC} Make sure your current user has permission to execute docker command." >&2
echo -e "${YELLOW}[PRE-REQUEST]${NC} Make sure you have key pairs ~/.ssh/authorized_keys and ~/.ssh/devs1" >&2
echo -e "${YELLOW}[PRE-REQUEST]${NC} Make sure you have key pair ~/.ssh/itl-devs1 for proxy server access" >&2
echo "" >&2
# Auto-proceed with deployment (smooth operation)
echo "Auto-proceeding with deployment..." >&2
echo "" >&2
# Use Docker network from environment variable if available, otherwise use default
if [ -n "$DOCKER_NETWORK" ]; then
SELECTED_NETWORK="$DOCKER_NETWORK"
log_info "Using Docker network from environment variable: ${SELECTED_NETWORK}"
else
SELECTED_NETWORK=""
log_info "Using default Docker network"
fi
# Save the network selection
local network_to_save="$SELECTED_NETWORK"
if [ -z "$network_to_save" ]; then
network_to_save="none"
fi
save_parameters "$network_to_save"
if [ -n "$SELECTED_NETWORK" ]; then
log_info "Selected Docker network: ${SELECTED_NETWORK}"
else
log_info "Using default Docker network"
fi
# Generate FRP client configuration
generate_frpc_config
# Start Docker container
start_docker_container
# Extract FRP server address and remote ports
local server_addr
server_addr=$(extract_frp_server_address)
if [ -z "$server_addr" ]; then
log_error "Failed to extract FRP server address from configuration"
log_info "Skipping proxy server configuration update"
else
log_info "Extracted FRP server address: $server_addr"
# Extract all remote ports dynamically from configuration
local all_ports=()
local ports_string=$(extract_all_remote_ports)
if [ -n "$ports_string" ]; then
all_ports=($ports_string)
fi
# Check if we have at least one port to configure
if [ ${#all_ports[@]} -ge 1 ]; then
log_info "Extracted ${#all_ports[@]} remote port(s) for proxy server update: ${all_ports[*]}"
# Check if we have any web proxies
local has_web=$(has_web_proxy "${proxy_configs[@]}")
# Update proxy server configuration with all extracted ports
update_proxy_server_config "$server_addr" "$has_web" "${all_ports[@]}"
else
log_error "Failed to extract any remote ports from configuration"
log_info "Skipping proxy server configuration update"
fi
fi
# Display container operation commands
display_container_commands
log_info "FRP client instance ${THIS_INSTANCE} deployment completed"
}
# Setup rvs-prxy function (main function that decides FRP vs Traefik)
setup_rvs_prxy() {
if [ "$FRP" = false ]; then
# Run Traefik-only setup
log_info "Running Traefik-only setup as FRP=false"
setup_traefik_only
return 0
fi
log_info "Setting up FRP client for instance: ${THIS_INSTANCE}"
# Pre-request checks
echo -e "${YELLOW}[PRE-REQUEST]${NC} Make sure your current user has permission to execute docker command." >&2
echo -e "${YELLOW}[PRE-REQUEST]${NC} Make sure you have key pairs ~/.ssh/authorized_keys and ~/.ssh/devs1" >&2
echo -e "${YELLOW}[PRE-REQUEST]${NC} Make sure you have key pair ~/.ssh/itl-devs1 for proxy server access" >&2
echo "" >&2
# Auto-proceed with deployment (smooth operation)
echo "Auto-proceeding with deployment..." >&2
echo "" >&2
# Use Docker network from environment variable if available, otherwise use default
if [ -n "$DOCKER_NETWORK" ]; then
SELECTED_NETWORK="$DOCKER_NETWORK"
log_info "Using Docker network from environment variable: ${SELECTED_NETWORK}"
else
SELECTED_NETWORK=""
log_info "Using default Docker network"
fi
# Save the network selection
local network_to_save="$SELECTED_NETWORK"
if [ -z "$network_to_save" ]; then
network_to_save="none"
fi
save_parameters "$network_to_save"
if [ -n "$SELECTED_NETWORK" ]; then
log_info "Selected Docker network: ${SELECTED_NETWORK}"
else
log_info "Using default Docker network"
fi
# Generate FRP client configuration
generate_frpc_config
# Start Docker container
start_docker_container
# Extract FRP server address and remote ports
local server_addr
server_addr=$(extract_frp_server_address)
if [ -z "$server_addr" ]; then
log_error "Failed to extract FRP server address from configuration"
log_info "Skipping proxy server configuration update"
else
log_info "Extracted FRP server address: $server_addr"
# Extract all remote ports dynamically from configuration
local all_ports=()
local ports_string=$(extract_all_remote_ports)
if [ -n "$ports_string" ]; then
all_ports=($ports_string)
fi
# Check if we have at least one port to configure
if [ ${#all_ports[@]} -ge 1 ]; then
log_info "Extracted ${#all_ports[@]} remote port(s) for proxy server update: ${all_ports[*]}"
# Check if we have any web proxies
local has_web=$(has_web_proxy "${proxy_configs[@]}")
# Update proxy server configuration with all extracted ports
update_proxy_server_config "$server_addr" "$has_web" "${all_ports[@]}"
else
log_error "Failed to extract any remote ports from configuration"
log_info "Skipping proxy server configuration update"
fi
fi
# Display container operation commands
display_container_commands
log_info "FRP client instance ${THIS_INSTANCE} deployment completed"
}
# List available systems
list_systems() {
echo -e "${GREEN}Available deployment systems:${NC}" >&2
echo -e "${YELLOW}============================${NC}" >&2
echo -e "${BLUE}•${NC} setup-rvs-prxy" >&2
echo -e "${YELLOW}============================${NC}" >&2
echo -e "Use: ${GREEN}$0 <system-name>${NC} to deploy a system" >&2
}
# Usage function
usage() {
cat << EOF
Usage: curl -s "<url>/deploy-rvs-prxy.sh" | bash
Automated deployment script for Reverse Proxy setup.
Modes:
1) Setup Reverse Proxy - Deploys Traefik or FRP Tunnel with Traefik
2) Setup New Domain - Adds a new domain to existing reverse proxy
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
Environment Variables (in .env file):
FRP=true|false Set to 'false' for Traefik mode, 'true' for FRP mode
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)
INSTANCE_NAME Unique instance identifier
Examples:
# First run - will trigger interactive wizard
curl -s "<url>/deploy-rvs-prxy.sh" | bash
# With existing .env file
curl -s "<url>/deploy-rvs-prxy.sh" | bash
Note: This script runs on the same computer as the service it connects to.
IMPORTANT: Configuration is primarily handled through .env files (.env, .env.development, .env.production)
located in the current directory or script directory. The .env file contains all parameters
for your setup and is essential for operation. Make sure to properly configure your .env file
before running the script.
EOF
}
# Main execution
main() {
# Check for help options first (before loading .env)
if [ "$1" = "-h" ] || [ "$1" = "--help" ] || [ "$1" = "help" ]; then
usage
exit 0
fi
# Load environment variables from .env files (or trigger interactive wizard)
load_env
# Execute based on detected deployment mode
if [ "$DEPLOY_MODE" = "domain" ]; then
log_info "Setting up new domain in existing reverse proxy..."
setup_new_domain
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 traefik --restart=unless-stopped \\"
echo " -p 80:80 -p 443:443 -p 127.0.0.1:8080: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 "$@"