3302 lines
134 KiB
Bash
Executable File
3302 lines
134 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
|
|
|
|
# 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_number>
|
|
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-<port> file"
|
|
exit 1
|
|
fi
|
|
if [ -z "$CONTAINER_NAME" ] || [ -z "$CONTAINER_PORT" ]; then
|
|
log_error "CONTAINER_NAME and CONTAINER_PORT are required for port exposure"
|
|
log_error "Please set these in your .env-port-<port> file"
|
|
exit 1
|
|
fi
|
|
elif [ "$DEPLOY_MODE" = "domain" ]; then
|
|
# Domain config file exists - validate it
|
|
if [ -z "$DOMAIN_NAME" ]; then
|
|
log_error "DOMAIN_NAME is required for domain setup mode"
|
|
log_error "Please set DOMAIN_NAME in your .env-<domain> file"
|
|
exit 1
|
|
fi
|
|
if [ -z "$LOCAL_SERVICE_NAME" ] || [ -z "$LOCAL_SERVICE_PORT" ]; then
|
|
log_error "LOCAL_SERVICE_NAME and LOCAL_SERVICE_PORT are required for domain setup"
|
|
log_error "Please set these in your .env-<domain> file"
|
|
exit 1
|
|
fi
|
|
elif [ "$DEPLOY_MODE" = "proxy" ]; then
|
|
# For proxy setup mode, validate proxy-specific settings
|
|
if [ "$FRP" = false ]; then
|
|
# Traefik mode - check for Traefik-specific settings
|
|
if [ -z "$TRAEFIK_VER" ]; then
|
|
log_error "TRAEFIK_VER is required for Traefik proxy setup"
|
|
log_error "Please set TRAEFIK_VER in your .env.rvs-prxy file"
|
|
exit 1
|
|
fi
|
|
if [ -z "$LETSENCRYPT_EMAIL" ]; then
|
|
log_error "LETSENCRYPT_EMAIL is required for Traefik proxy setup"
|
|
log_error "Please set LETSENCRYPT_EMAIL in your .env.rvs-prxy file"
|
|
exit 1
|
|
fi
|
|
else
|
|
# FRP mode - check for FRP-specific settings
|
|
if [ -z "$FRP_SERVER_ADDR" ]; then
|
|
log_error "FRP_SERVER_ADDR is required for FRP proxy setup"
|
|
log_error "Please set FRP_SERVER_ADDR in your .env.rvs-prxy file"
|
|
exit 1
|
|
fi
|
|
fi
|
|
fi
|
|
# Note: domain_wizard mode skips validation here - wizard will collect the required values
|
|
}
|
|
|
|
# Logging functions
|
|
log_info() {
|
|
echo -e "${GREEN}[INFO]${NC} $1" >&2
|
|
}
|
|
|
|
log_error() {
|
|
echo -e "${RED}[ERROR]${NC} $1" >&2
|
|
}
|
|
|
|
log_warn() {
|
|
echo -e "${YELLOW}[WARN]${NC} $1" >&2
|
|
}
|
|
|
|
log_debug() {
|
|
if [ "$DEBUG" = "true" ]; then
|
|
echo -e "${BLUE}[DEBUG]${NC} $1" >&2
|
|
fi
|
|
}
|
|
|
|
# Function to save answered values to .env.rvs-prxy file
|
|
save_to_env() {
|
|
local var_name="$1"
|
|
local var_value="$2"
|
|
local target_file="${3:-.env.rvs-prxy}"
|
|
|
|
# Create or update .env.rvs-prxy file with the new value
|
|
if [ -f "$target_file" ]; then
|
|
# Check if variable already exists
|
|
if grep -q "^${var_name}=" "$target_file"; then
|
|
# Update existing variable
|
|
sed -i "s/^${var_name}=.*/${var_name}=${var_value}/" "$target_file"
|
|
else
|
|
# Add new variable
|
|
echo "${var_name}=${var_value}" >> "$target_file"
|
|
fi
|
|
else
|
|
# Create new .env.rvs-prxy file
|
|
echo "${var_name}=${var_value}" >> "$target_file"
|
|
fi
|
|
}
|
|
|
|
# Function to save domain-specific configuration
|
|
save_domain_env() {
|
|
local domain_name="$1"
|
|
local service_name="$2"
|
|
local service_port="$3"
|
|
|
|
# Convert domain name to filename format: your-domain-com -> .env-your-domain-com
|
|
local domain_file=".env-$(echo "$domain_name" | sed 's/\./-/g')"
|
|
|
|
echo -e "${BLUE}[INFO]${NC} Saving domain configuration to $domain_file" >&2
|
|
|
|
# Create domain-specific env file
|
|
cat > "$domain_file" << EOF
|
|
# Domain configuration for ${domain_name}
|
|
# Generated by deploy-rvs-prxy.sh on $(date)
|
|
|
|
DOMAIN_NAME="${domain_name}"
|
|
LOCAL_SERVICE_NAME="${service_name}"
|
|
LOCAL_SERVICE_PORT="${service_port}"
|
|
EOF
|
|
|
|
echo -e "${GREEN}[INFO]${NC} Domain configuration saved to $domain_file" >&2
|
|
DOMAIN_ENV_FILE="$domain_file"
|
|
}
|
|
|
|
# Function to save port exposure configuration to .env-port-<port> file
|
|
save_port_env() {
|
|
local host_port="$1"
|
|
local container_name="$2"
|
|
local container_port="$3"
|
|
|
|
# Create port-specific filename: .env-port-<port>
|
|
local port_file=".env-port-${host_port}"
|
|
|
|
echo -e "${BLUE}[INFO]${NC} Saving port configuration to $port_file" >&2
|
|
|
|
# Create port-specific env file
|
|
cat > "$port_file" << EOF
|
|
# Port-based routing configuration
|
|
# Generated by deploy-rvs-prxy.sh on $(date)
|
|
|
|
HOST_PORT="${host_port}"
|
|
CONTAINER_NAME="${container_name}"
|
|
CONTAINER_PORT="${container_port}"
|
|
EOF
|
|
|
|
echo -e "${GREEN}[INFO]${NC} Port configuration saved to $port_file" >&2
|
|
PORT_ENV_FILE="$port_file"
|
|
}
|
|
|
|
# Function to read from terminal instead of pipe
|
|
read_terminal() {
|
|
local prompt="$1"
|
|
local default_value="$2"
|
|
|
|
# Read from terminal (/dev/tty) instead of stdin
|
|
read -p "$prompt" input_name </dev/tty
|
|
echo "${input_name:-$default_value}"
|
|
}
|
|
|
|
# Interactive setup wizard for missing .env.rvs-prxy file
|
|
interactive_setup_wizard() {
|
|
echo -e "${BLUE}=============================================${NC}" >&2
|
|
echo -e "${GREEN}Welcome to Deploy Reverse Proxy Setup Wizard${NC}" >&2
|
|
echo -e "${BLUE}=============================================${NC}" >&2
|
|
echo "" >&2
|
|
|
|
# Check if reverse proxy already exists
|
|
local proxy_exists=false
|
|
if docker ps -a --filter name=^${INSTANCE_NAME}$ --format '{{.Names}}' | grep -q "^traefik$"; then
|
|
proxy_exists=true
|
|
fi
|
|
|
|
if [ "$proxy_exists" = true ]; then
|
|
# Reverse proxy exists, ask whether to setup new domain or port exposure
|
|
echo -e "${YELLOW}Existing reverse proxy (Traefik) detected.${NC}" >&2
|
|
echo "" >&2
|
|
|
|
# Ask what to setup
|
|
echo -e "${BLUE}--- What would you like to setup? ---${NC}" >&2
|
|
echo -e " 1) ${GREEN}Setup New Domain${NC} - Route traffic based on domain name" >&2
|
|
echo -e " 2) ${GREEN}Setup Port Exposure${NC} - Expose host port to container port" >&2
|
|
echo "" >&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-ip>:${HOST_PORT}"
|
|
}
|
|
|
|
# Function to configure port routing in Traefik for port-based access
|
|
configure_port_routing_local() {
|
|
log_info "Configuring Traefik port routing for container ${CONTAINER_NAME}..."
|
|
|
|
# Check if the target container is running
|
|
local container_running
|
|
container_running=$(docker ps --filter name="$CONTAINER_NAME" --format '{{.Names}}' 2>/dev/null || true)
|
|
|
|
if [ -z "$container_running" ]; then
|
|
log_error "Target container '$CONTAINER_NAME' is not running."
|
|
log_error "Please make sure the container is started."
|
|
exit 1
|
|
fi
|
|
|
|
# Get the container's IP address on the ${DOCKER_NETWORK} network
|
|
local container_ip
|
|
container_ip=$(docker inspect "$CONTAINER_NAME" 2>/dev/null | grep -A 20 '"${DOCKER_NETWORK}"' | grep '"IPAddress"' | grep -o '[0-9.]*' | head -1)
|
|
|
|
if [ -z "$container_ip" ]; then
|
|
# Try to get any IP address from the container
|
|
container_ip=$(docker inspect "$CONTAINER_NAME" 2>/dev/null | grep '"IPAddress"' | grep -o '[0-9.]*' | head -1)
|
|
fi
|
|
|
|
if [ -z "$container_ip" ]; then
|
|
log_warn "Could not determine container IP address, using container name for routing"
|
|
container_ip="$CONTAINER_NAME"
|
|
else
|
|
log_info "Container IP address: ${container_ip}"
|
|
fi
|
|
|
|
# Create dynamic configuration file for port-based routing
|
|
local config_filename="port-${HOST_PORT}-${CONTAINER_NAME}.yml"
|
|
|
|
# Build the YAML config content using printf to avoid escaping issues
|
|
local yaml_content
|
|
yaml_content=$(printf 'http:\n routers:\n port-%s-router:\n rule: '"'"'PathPrefix(`/`)'"'"'\n service: port-%s-service\n entryPoints:\n - port%s\n services:\n port-%s-service:\n loadBalancer:\n servers:\n - url: '"'"'http://%s:%s'"'"'\n passHostHeader: true' \
|
|
"$HOST_PORT" "$HOST_PORT" "$HOST_PORT" "$HOST_PORT" "$container_ip" "$CONTAINER_PORT")
|
|
|
|
# Base64 encode to avoid escaping issues
|
|
local encoded_config
|
|
encoded_config=$(echo "$yaml_content" | base64 -w 0)
|
|
|
|
# Create the configuration directly in the Traefik volume using docker
|
|
docker run --rm -v "traefik_data:/etc/traefik" alpine:latest sh -c "mkdir -p /etc/traefik/config && echo '${encoded_config}' | base64 -d > /etc/traefik/config/${config_filename}"
|
|
|
|
# Add the port entry point and port mapping to Traefik
|
|
add_traefik_entry_point "$HOST_PORT"
|
|
|
|
log_info "Port routing configuration created for host port ${HOST_PORT}"
|
|
log_info "Traefik has been updated with new entry point and port mapping"
|
|
}
|
|
|
|
# Function to add entry point to Traefik configuration
|
|
add_traefik_entry_point() {
|
|
local port="$1"
|
|
local entry_point_name="port${port}"
|
|
|
|
log_info "Adding entry point '${entry_point_name}' for port ${port} to Traefik..."
|
|
|
|
# Entry points in Traefik are defined via command-line arguments
|
|
# We need to recreate the container with the new entry point
|
|
|
|
# Get current Traefik configuration from running container
|
|
local current_image
|
|
current_image=$(docker inspect traefik --format='{{.Config.Image}}' 2>/dev/null || echo "traefik:latest")
|
|
local current_cmd
|
|
current_cmd=$(docker inspect traefik --format='{{json .Config.Cmd}}' 2>/dev/null || echo "[]")
|
|
|
|
# Check if entry point already exists in command
|
|
if echo "$current_cmd" | grep -q "entrypoints.${entry_point_name}.address"; then
|
|
log_info "Entry point '${entry_point_name}' already exists in Traefik command"
|
|
log_info "Entry point exists, port mapping should already be active"
|
|
return 0
|
|
fi
|
|
|
|
# Extract Let's Encrypt email from existing config
|
|
local letsencrypt_email
|
|
letsencrypt_email=$(echo "$current_cmd" | grep -oP 'acme\.email=\K[^",]+' 2>/dev/null || echo "")
|
|
if [ -z "$letsencrypt_email" ]; then
|
|
letsencrypt_email=$(echo "$current_cmd" | grep -o 'acme\.email=[^",]*' | sed 's/acme\.email=//' | head -1)
|
|
fi
|
|
if [ -z "$letsencrypt_email" ]; then
|
|
letsencrypt_email="admin@example.com"
|
|
fi
|
|
|
|
log_info "Updating Traefik container with new entry point '${entry_point_name}'..."
|
|
log_info "Using Let's Encrypt email: ${letsencrypt_email}"
|
|
|
|
# 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 <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
|
|
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-<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 "<url>/deploy-rvs-prxy.sh" | bash
|
|
|
|
# With existing .env.rvs-prxy file
|
|
curl -s "<url>/deploy-rvs-prxy.sh" | bash
|
|
|
|
# Setup port exposure - create .env-port-18081 file and run
|
|
curl -s "<url>/deploy-rvs-prxy.sh" | bash
|
|
|
|
# Manage existing domains and ports (interactive menu)
|
|
curl -s "<url>/deploy-rvs-prxy.sh" | bash -s -- --manage
|
|
|
|
Note: This script runs on the same computer as the service it connects to.
|
|
IMPORTANT: Configuration is primarily handled through .env.rvs-prxy file.
|
|
The .env.rvs-prxy file contains all parameters for your setup and is essential for operation.
|
|
|
|
Port Exposure:
|
|
Create a file named .env-port-<port> (e.g., .env-port-18081) with:
|
|
HOST_PORT="18081"
|
|
CONTAINER_NAME="your-container-name"
|
|
CONTAINER_PORT="80"
|
|
Then run the script to expose host port 18081 -> container port 80
|
|
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/tty
|
|
|
|
if [ "$selection" = "q" ] || [ -z "$selection" ]; then
|
|
log_info "No changes made."
|
|
return 0
|
|
fi
|
|
|
|
# Build array of all config files
|
|
local all_files=("${domain_files[@]}" "${port_files[@]}")
|
|
|
|
# Track if Traefik needs restart (for port removals)
|
|
local traefik_restart_needed=false
|
|
local ports_to_remove=()
|
|
|
|
# Process selection
|
|
for num in $selection; do
|
|
if [[ "$num" =~ ^[0-9]+$ ]] && [ "$num" -ge 1 ] && [ "$num" -le "$total_configs" ]; then
|
|
local file_to_remove="${all_files[$((num-1))]}"
|
|
if [ -f "$file_to_remove" ]; then
|
|
log_info "Removing: ${file_to_remove}"
|
|
|
|
# Check if it's a domain config
|
|
if [[ "$file_to_remove" == .env-* ]] && [[ "$file_to_remove" != .env-port-* ]]; then
|
|
# Remove domain config from Traefik volume
|
|
local domain_name
|
|
domain_name=$(grep -o 'DOMAIN_NAME="[^"]*"' "$file_to_remove" 2>/dev/null | sed 's/DOMAIN_NAME="//;s/"//' || echo "")
|
|
local service_name
|
|
service_name=$(grep -o 'LOCAL_SERVICE_NAME="[^"]*"' "$file_to_remove" 2>/dev/null | sed 's/LOCAL_SERVICE_NAME="//;s/"//' || echo "")
|
|
|
|
if [ -n "$domain_name" ] && [ -n "$service_name" ]; then
|
|
log_info "Removing domain config from Traefik: ${domain_name}"
|
|
# Remove the dynamic config file from Traefik volume
|
|
# 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 "$@" |