#!/bin/bash # auto-sys-deploy.sh - Unified system deployment script for both FRP and Traefik # This script supports both FRP (Fast Reverse Proxy) and Traefik modes based on environment configuration # This script runs on the same computer as the service it connects to set -e # Exit on any error # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Global variables THIS_INSTANCE="" SELECTED_NETWORK="" INTERNAL_SERVICE_PORT="" SERVER_ADDR="" SERVER_PORT="16999" RAD_REMOTE_PORT="" SSH_REMOTE_PORT="" WEB_REMOTE_PORT="" DOMAIN_NAME="" # FRP Mode variables FRP=true # Target server variables (for Traefik mode) TARGET_SERVER="" TARGET_USER="" SSH_PORT="22" SSH_KEY_FILE_NAME="" SSH_KEY_PATH="" LOCAL_FOLDER="../out/" DEST_PATH="/" DEBUG="false" TARGET_SERVICE_NAME="" TARGET_SERVICE_PORT="" TARGET_DOCKER_NETWORK="" # Function to load environment variables from .env files load_env() { # Check for .env files in priority order: .env.development, .env.production, .env # If no .env file is found in current directory, look for one in the script's directory if [ -f ".env.development" ]; then echo -e "${BLUE}[INFO]${NC} Loading development environment from .env.development" >&2 source .env.development elif [ -f ".env.production" ]; then echo -e "${BLUE}[INFO]${NC} Loading production environment from .env.production" >&2 source .env.production elif [ -f ".env" ]; then echo -e "${BLUE}[INFO]${NC} Loading default environment from .env" >&2 source .env elif [ -f "$(dirname "$0")/.env.development" ]; then echo -e "${BLUE}[INFO]${NC} Loading development environment from $(dirname "$0")/.env.development" >&2 source "$(dirname "$0")/.env.development" elif [ -f "$(dirname "$0")/.env.production" ]; then echo -e "${BLUE}[INFO]${NC} Loading production environment from $(dirname "$0")/.env.production" >&2 source "$(dirname "$0")/.env.production" elif [ -f "$(dirname "$0")/.env" ]; then echo -e "${BLUE}[INFO]${NC} Loading default environment from $(dirname "$0")/.env" >&2 source "$(dirname "$0")/.env" else echo -e "${RED}[ERROR]${NC} No .env file found in current directory or script directory." >&2 echo -e "${YELLOW}[WARN]${NC} Please create an .env.development file (or .env) with your configuration." >&2 echo -e "${YELLOW}Example .env file:${NC}" >&2 echo "# See .env.sample for a complete parameter reference" >&2 exit 1 fi # Normalize boolean values if [ "$FRP" = "false" ] || [ "$FRP" = "False" ] || [ "$FRP" = "FALSE" ]; then FRP=false else FRP=true fi # Set SSH key path if in Traefik mode if [ "$FRP" = false ]; then if [ -n "$SSH_KEY_FILE_NAME" ]; then SSH_KEY_PATH="$HOME/.ssh/$SSH_KEY_FILE_NAME" if [ ! -f "$SSH_KEY_PATH" ]; then log_error "SSH key file not found: $SSH_KEY_PATH" exit 1 fi else log_error "SSH_KEY_FILE_NAME must be set when FRP=false" exit 1 fi # Validate required Traefik parameters if [ -z "$TARGET_SERVER" ] || [ -z "$TARGET_USER" ] || [ -z "$TARGET_SERVICE_NAME" ] || [ -z "$TARGET_SERVICE_PORT" ]; then log_error "Required Traefik parameters are missing when FRP=false" exit 1 fi fi } # Logging functions log_info() { echo -e "${GREEN}[INFO]${NC} $1" >&2 } log_error() { echo -e "${RED}[ERROR]${NC} $1" >&2 } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1" >&2 } log_debug() { if [ "$DEBUG" = "true" ]; then echo -e "${BLUE}[DEBUG]${NC} $1" >&2 fi } # Function to save answered values to .env file save_to_env() { local var_name="$1" local var_value="$2" # Create or update .env file with the new value if [ -f ".env" ]; then # Check if variable already exists if grep -q "^${var_name}=" .env; then # Update existing variable sed -i "s/^${var_name}=.*/${var_name}=${var_value}/" .env else # Add new variable echo "${var_name}=${var_value}" >> .env fi else # Create new .env file echo "${var_name}=${var_value}" >> .env fi } # Function to read from terminal instead of pipe read_terminal() { local prompt="$1" local default_value="$2" # Read from terminal (/dev/tty) instead of stdin read -p "$prompt" input_name &2 echo -e "System: ${GREEN}${system_name}${NC}" >&2 if [ ${#instances[@]} -eq 1 ]; then THIS_INSTANCE="${instances[0]}" log_info "Found single existing instance: ${THIS_INSTANCE}" elif [ ${#instances[@]} -gt 1 ]; then echo -e "${YELLOW}Existing instances found:${NC}" >&2 for i in "${!instances[@]}"; do echo -e "${BLUE}$((i+1)).${NC} ${instances[i]}" >&2 done echo -e "Enter number to select existing instance, or press Enter for new default: ${YELLOW}${default_name}${NC}" >&2 local selection selection=$(read_terminal "Selection: " "") if [ -n "$selection" ] && [[ "$selection" =~ ^[0-9]+$ ]] && [ "$selection" -ge 1 ] && [ "$selection" -le ${#instances[@]} ]; then THIS_INSTANCE="${instances[$((selection-1))]}" else THIS_INSTANCE="$default_name" fi else THIS_INSTANCE="$default_name" fi # Allow user to override input_name=$(read_terminal "Enter instance name [${THIS_INSTANCE}]: " "${THIS_INSTANCE}") THIS_INSTANCE="${input_name:-${THIS_INSTANCE}}" # Save to env file if not already set if [ -z "$INSTANCE_NAME" ]; then save_to_env "INSTANCE_NAME" "$THIS_INSTANCE" fi fi # Validate instance name (alphanumeric, hyphens, underscores) if [[ "$THIS_INSTANCE" =~ ^[a-zA-Z0-9_-]+$ ]]; then log_info "Using instance name: ${THIS_INSTANCE}" else log_error "Invalid instance name. Only letters, numbers, hyphens, and underscores are allowed." >&2 exit 1 fi echo "" >&2 } # Function to list and select Docker networks select_docker_network() { # Check if DOCKER_NETWORK is set from environment if [ -n "$DOCKER_NETWORK" ]; then SELECTED_NETWORK="$DOCKER_NETWORK" log_info "Using environment variable for Docker network: ${SELECTED_NETWORK}" return 0 fi echo -e "${BLUE}=== Docker Network Selection ===${NC}" >&2 # Load existing network selection if available local existing_network existing_network=$(load_parameters) local default_option="1" # Get list of available Docker networks local networks networks=$(docker network ls --format '{{.Name}}' 2>/dev/null || true) if [ -z "$networks" ]; then log_warn "No Docker networks found" >&2 return 1 fi # Convert to array local network_array=() while IFS= read -r line; do network_array+=("$line") done <<< "$networks" # Display networks as a list echo -e "${YELLOW}Available Docker Networks:${NC}" >&2 for i in "${!network_array[@]}"; do echo -e "${BLUE}$((i+1)).${NC} ${network_array[i]}" >&2 done # Ask user to select or enter custom network echo -e "" >&2 echo -e "${YELLOW}Options:${NC}" >&2 echo -e "${BLUE}1-${#network_array[@]}.${NC} Select from list above" >&2 echo -e "${BLUE}C.${NC} Custom network name" >&2 echo -e "${BLUE}N.${NC} No network (default bridge)${NC}" >&2 echo -e "" >&2 # Set default option based on existing network if [ -n "$existing_network" ]; then # Check if existing network is in the list for i in "${!network_array[@]}"; do if [ "${network_array[i]}" = "$existing_network" ]; then default_option=$((i+1)) break fi done # If not in list, check if it's a custom network or none if [ "$default_option" = "1" ] && [ -n "$existing_network" ]; then if [ "$existing_network" = "none" ]; then default_option="n" else default_option="c" fi fi log_info "Using existing network selection as default: $existing_network" fi while true; do local selection selection=$(read_terminal "Select option [${default_option}]: " "$default_option") case "${selection,,}" in c) # Custom network name local custom_network if [ -n "$existing_network" ] && [ "$existing_network" != "none" ]; then custom_network=$(read_terminal "Enter custom network name [${existing_network}]: " "$existing_network") else custom_network=$(read_terminal "Enter custom network name: " "") fi if [ -n "$custom_network" ]; then SELECTED_NETWORK="$custom_network" save_to_env "DOCKER_NETWORK" "$SELECTED_NETWORK" return 0 else log_error "Network name cannot be empty" >&2 fi ;; n|"") # No network (default) SELECTED_NETWORK="" save_to_env "DOCKER_NETWORK" "none" return 0 ;; *) # Numeric selection if [[ "$selection" =~ ^[0-9]+$ ]] && [ "$selection" -ge 1 ] && [ "$selection" -le ${#network_array[@]} ]; then SELECTED_NETWORK="${network_array[$((selection-1))]}" save_to_env "DOCKER_NETWORK" "$SELECTED_NETWORK" return 0 else log_error "Invalid selection. Please choose a valid option." >&2 fi ;; esac done } # Function to prompt for internal service port prompt_internal_service_port() { # Check if INTERNAL_SERVICE_PORT is set from environment if [ -n "$INTERNAL_SERVICE_PORT" ]; then log_info "Using environment variable for internal service port: ${INTERNAL_SERVICE_PORT}" return fi echo -e "${BLUE}=== Internal Service Configuration ===${NC}" >&2 while [ -z "$INTERNAL_SERVICE_PORT" ]; do INTERNAL_SERVICE_PORT=$(read_terminal "Enter internal service port (exposed by Docker container) [3000]: " "3000") if [ -z "$INTERNAL_SERVICE_PORT" ]; then log_error "Internal service port cannot be empty" >&2 elif ! [[ "$INTERNAL_SERVICE_PORT" =~ ^[0-9]+$ ]] || [ "$INTERNAL_SERVICE_PORT" -lt 1 ] || [ "$INTERNAL_SERVICE_PORT" -gt 65535 ]; then log_error "Invalid port number. Please enter a number between 1 and 65535." >&2 INTERNAL_SERVICE_PORT="" fi done save_to_env "INTERNAL_SERVICE_PORT" "$INTERNAL_SERVICE_PORT" } # Function to save parameters to .para file save_parameters() { local volume_name="${THIS_INSTANCE}-frpc" local temp_container="temp-para-config-$(date +%s)" local network_selection="$1" # Create a temporary directory for the config file local temp_dir="/tmp/para-config-${THIS_INSTANCE}" mkdir -p "$temp_dir" # Create JSON content for .para file local para_content="{\"network\":\"$network_selection\"}" # Write the parameters to a temporary file echo "$para_content" > "${temp_dir}/.para" # Copy the .para file to the volume # Create a temporary container to copy the file if docker run --rm -d --name "$temp_container" -v "$volume_name:/etc/frp" -v "${temp_dir}:/tmp/config" alpine:latest sleep 10 >/dev/null 2>&1; then docker exec "$temp_container" cp /tmp/config/.para /etc/frp/.para 2>/dev/null || true docker stop "$temp_container" >/dev/null 2>&1 fi # Clean up temporary directory rm -rf "$temp_dir" } # Function to load parameters from .para file load_parameters() { local volume_name="${THIS_INSTANCE}-frpc" local temp_container="temp-para-config-$(date +%s)" local existing_network="" # Create a temporary container to check for existing config if docker volume inspect "$volume_name" >/dev/null 2>&1; then # Create temporary container to access volume if docker run --rm -d --name "$temp_container" -v "$volume_name:/etc/frp" alpine:latest sleep 10 >/dev/null 2>&1; then # Check if .para exists if docker exec "$temp_container" test -f /etc/frp/.para >/dev/null 2>&1; then # Try to extract existing network selection from JSON existing_network=$(docker exec "$temp_container" grep '"network"' /etc/frp/.para | cut -d':' -f2 | tr -d ' "{}' 2>/dev/null || true) if [ -n "$existing_network" ]; then log_info "Found existing network selection: $existing_network" fi fi # Stop the temporary container docker stop "$temp_container" >/dev/null 2>&1 fi fi echo "$existing_network" } # Function to generate frpc.toml configuration (FRP Mode) generate_frpc_config() { if [ "$FRP" = false ]; then log_info "Skipping FRP client configuration as FRP=false" return 0 fi echo -e "${BLUE}=== FRP Client Configuration ===${NC}" >&2 # Check if there's an existing frpc.toml in the volume local volume_name="${THIS_INSTANCE}-frpc" local temp_container="temp-frpc-config-$(date +%s)" local existing_server_addr="" local existing_server_port="" local existing_proxy1_remote_port="" local existing_proxy2_remote_port="" local existing_proxy3_remote_port="" # Create a temporary container to check for existing config if docker volume inspect "$volume_name" >/dev/null 2>&1; then log_info "Found existing volume: $volume_name" # Create temporary container to access volume if docker run --rm -d --name "$temp_container" -v "$volume_name:/etc/frp" alpine:latest sleep 10 >/dev/null 2>&1; then # Check if frpc.toml exists if docker exec "$temp_container" test -f /etc/frp/frpc.toml >/dev/null 2>&1; then # Try to extract existing values existing_server_addr=$(docker exec "$temp_container" grep "^serverAddr" /etc/frp/frpc.toml | cut -d'"' -f2 || true) existing_server_port=$(docker exec "$temp_container" grep "^serverPort" /etc/frp/frpc.toml | cut -d'=' -f2 | tr -d ' ' || true) existing_proxy1_remote_port=$(docker exec "$temp_container" grep -A 5 '\[\[proxies\]\]' /etc/frp/frpc.toml | grep "name = \"rad-${THIS_INSTANCE}\"" -A 5 | grep "remotePort" | cut -d'=' -f2 | tr -d ' ' | head -1 || true) existing_proxy2_remote_port=$(docker exec "$temp_container" grep -A 5 '\[\[proxies\]\]' /etc/frp/frpc.toml | grep "name = \"ssh-${THIS_INSTANCE}\"" -A 5 | grep "remotePort" | cut -d'=' -f2 | tr -d ' ' | head -1 || true) existing_proxy3_remote_port=$(docker exec "$temp_container" grep -A 5 '\[\[proxies\]\]' /etc/frp/frpc.toml | grep "name = \"web-${THIS_INSTANCE}\"" -A 5 | grep "remotePort" | cut -d'=' -f2 | tr -d ' ' | head -1 || true) if [ -n "$existing_server_addr" ]; then log_info "Found existing server address: $existing_server_addr" fi if [ -n "$existing_server_port" ]; then log_info "Found existing server port: $existing_server_port" fi if [ -n "$existing_proxy1_remote_port" ]; then log_info "Found existing rad proxy remote port: $existing_proxy1_remote_port" fi if [ -n "$existing_proxy2_remote_port" ]; then log_info "Found existing ssh proxy remote port: $existing_proxy2_remote_port" fi if [ -n "$existing_proxy3_remote_port" ]; then log_info "Found existing web proxy remote port: $existing_proxy3_remote_port" fi fi # Stop the temporary container docker stop "$temp_container" >/dev/null 2>&1 fi fi # Prompt for server address if [ -n "$FRP_SERVER_ADDR" ]; then SERVER_ADDR="$FRP_SERVER_ADDR" log_info "Using environment variable for FRP server address: $SERVER_ADDR" else if [ -n "$existing_server_addr" ]; then SERVER_ADDR=$(read_terminal "Enter FRP server address IP [${existing_server_addr}]: " "$existing_server_addr") else while [ -z "$SERVER_ADDR" ]; do SERVER_ADDR=$(read_terminal "Enter FRP server address IP: " "") if [ -z "$SERVER_ADDR" ]; then log_error "Server address cannot be empty" >&2 fi done fi save_to_env "FRP_SERVER_ADDR" "$SERVER_ADDR" fi # Use FRP server user from environment or default to 'teenfuture' if [ -n "$FRP_SERVER_USER" ]; then SERVER_USER="$FRP_SERVER_USER" log_info "Using environment variable for FRP server user: $SERVER_USER" else # Use default 'teenfuture' user if not specified SERVER_USER="teenfuture" log_info "Using default FRP server user: $SERVER_USER" fi # Use default server port (16999) without prompting, but use existing if available if [ -n "$FRP_SERVER_PORT" ]; then SERVER_PORT="$FRP_SERVER_PORT" log_info "Using environment variable for FRP server port: ${SERVER_PORT}" elif [ -n "$existing_server_port" ]; then SERVER_PORT="$existing_server_port" else # SERVER_PORT remains default 16999 save_to_env "FRP_SERVER_PORT" "$SERVER_PORT" fi log_info "Using FRP server port: ${SERVER_PORT}" # Generate the frpc.toml content local config_content="" config_content+="serverAddr = \"${SERVER_ADDR}\"\n" config_content+="serverPort = ${SERVER_PORT}\n\n" # Dynamic proxy configuration based on environment variables # Look for environment variables in the format PROXY_*_PORT local proxy_configs=() # Process PROXY_*_PORT variables if they exist in environment local env_vars=($(compgen -v | grep -E "^PROXY_[A-Z0-9_]*_PORT$" 2>/dev/null)) for var_name in "${env_vars[@]}"; do local var_value="${!var_name}" if [ -n "$var_value" ] && [[ "$var_value" =~ ^[0-9]+$ ]]; then # Extract proxy name from variable (e.g., PROXY_CUSTOM_PORT -> custom) local proxy_name=$(echo "$var_name" | sed 's/^PROXY_\([A-Z0-9_]*\)_PORT$/\1/' | tr '[:upper:]' '[:lower:]' | tr '_' '-') local local_port_var="PROXY_${proxy_name^^}_LOCAL_PORT" # Convert back to uppercase with underscores # Get local port - either from PROXY_*_LOCAL_PORT or use defaults local local_port="$INTERNAL_SERVICE_PORT" # Default to the main service port if [ -n "${!local_port_var}" ]; then local_port="${!local_port_var}" fi # Validate that local_port is not empty if [ -z "$local_port" ] || ! [[ "$local_port" =~ ^[0-9]+$ ]]; then log_error "Invalid or empty local port for ${proxy_name} proxy: '$local_port'" log_error "Make sure INTERNAL_SERVICE_PORT or ${local_port_var} is set correctly in your .env file" exit 1 fi # Create the proxy configuration string proxy_configs+=("${proxy_name}-${local_port}:${var_value}") log_info "Adding ${proxy_name} proxy: local port ${local_port} to remote port ${var_value}" fi done # If no proxy variables were found, add a default proxy if needed if [ ${#proxy_configs[@]} -eq 0 ]; then # Add a default proxy using WEB_REMOTE_PORT if available, otherwise use a default local default_remote_port="${WEB_REMOTE_PORT:-8080}" # Validate that INTERNAL_SERVICE_PORT is not empty if [ -z "$INTERNAL_SERVICE_PORT" ] || ! [[ "$INTERNAL_SERVICE_PORT" =~ ^[0-9]+$ ]]; then log_error "Invalid or empty internal service port: '$INTERNAL_SERVICE_PORT'" log_error "Make sure INTERNAL_SERVICE_PORT is set correctly in your .env file" exit 1 fi proxy_configs+=("default-${INTERNAL_SERVICE_PORT}:${default_remote_port}") log_info "Adding default proxy: local port $INTERNAL_SERVICE_PORT to remote port $default_remote_port" fi # Add all proxy configurations to the config content for proxy_config in "${proxy_configs[@]}"; do # Parse the proxy config format: proxy_name-local_port:remote_port # Example: web-3000:12077 -> proxy_name="web", local_port=3000, remote_port=12077 local proxy_name="" local local_remote_port="" local local_port="" local remote_port="" # First, split on the LAST occurrence of '-' to get proxy name and local:remote part # This handles cases where the proxy name might contain hyphens after processing if [[ "$proxy_config" =~ ^(.*)-([0-9]+:[0-9]+)$ ]]; then proxy_name="${BASH_REMATCH[1]}" local_remote_port="${BASH_REMATCH[2]}" else log_error "Invalid proxy configuration format: $proxy_config" log_error "Expected format: proxy_name-local_port:remote_port (e.g., web-3000:12077)" exit 1 fi # Then, split the local_remote_port on ':' to separate local and remote ports IFS=':' read -r local_port remote_port <<< "$local_remote_port" # Validate that both local and remote ports are set and are numeric if [ -z "$local_port" ] || [ -z "$remote_port" ] || ! [[ "$local_port" =~ ^[0-9]+$ ]] || ! [[ "$remote_port" =~ ^[0-9]+$ ]]; then log_error "Invalid proxy configuration format: $proxy_config" log_error "Local port: '$local_port', Remote port: '$remote_port'" exit 1 fi config_content+="[[proxies]]\n" config_content+="name = \"${proxy_name}-${THIS_INSTANCE}\"\n" config_content+="type = \"tcp\"\n" config_content+="localPort = ${local_port}\n" # Use SERVICE_DOCKER_CONTAINER_NAME from .env* files if available, otherwise fall back to ${THIS_INSTANCE}-app if [ -n "$SERVICE_DOCKER_CONTAINER_NAME" ]; then config_content+="localIP = \"${SERVICE_DOCKER_CONTAINER_NAME}\"\n" else config_content+="localIP = \"${THIS_INSTANCE}-app\"\n" fi config_content+="remotePort = ${remote_port}\n\n" done # Create or update the frpc.toml file in the volume log_info "Creating/updating frpc.toml configuration" # Create a temporary directory for the config file local temp_dir="/tmp/frpc-config-${THIS_INSTANCE}" mkdir -p "$temp_dir" # Write the config to a temporary file echo -e "$config_content" > "${temp_dir}/frpc.toml" # Create the volume if it doesn't exist if ! docker volume inspect "$volume_name" >/dev/null 2>&1; then log_info "Creating Docker volume: $volume_name" docker volume create "$volume_name" >/dev/null fi # Copy the config file to the volume # Create a temporary container to copy the file if docker run --rm -d --name "$temp_container" -v "$volume_name:/etc/frp" -v "${temp_dir}:/tmp/config" alpine:latest sleep 10 >/dev/null 2>&1; then docker exec "$temp_container" cp /tmp/config/frpc.toml /etc/frp/frpc.toml docker stop "$temp_container" >/dev/null 2>&1 fi # Clean up temporary directory rm -rf "$temp_dir" log_info "FRP client configuration created successfully" echo "" >&2 } # Function to check if any proxy configurations are web proxies has_web_proxy() { # Check if any proxy config has "web" as the proxy name # Accept proxy configs as parameters local proxy_configs=("$@") for proxy_config in "${proxy_configs[@]}"; do # Split the proxy config into proxy_name-local_port:remote_port IFS='-' read -r proxy_name local_remote_port <<< "$proxy_config" if [ "$proxy_name" = "web" ]; then echo "true" return 0 fi done echo "false" return 0 } # Function to start the Docker container (FRP Mode) start_docker_container() { if [ "$FRP" = false ]; then log_info "Skipping Docker container start as FRP=false" return 0 fi echo -e "${BLUE}=== Starting Docker Container ===${NC}" >&2 local container_name="${THIS_INSTANCE}-frpc" local volume_name="${THIS_INSTANCE}-frpc" # Check if container already exists if docker ps -a -f name=^${container_name}$ --format '{{.ID}}' | grep -q .; then log_info "Container ${container_name} already exists" if docker ps -f name=^${container_name}$ --format '{{.ID}}' | grep -q .; then log_info "Container is already running" return 0 else log_info "Starting existing container ${container_name}" if ! docker start "${container_name}"; then log_error "Failed to start existing container" return 1 fi fi else log_info "Starting new Docker container: ${container_name}" # Build docker run command local docker_run_cmd="docker run --restart=always -d" # Add network option if selected if [ -n "$SELECTED_NETWORK" ]; then docker_run_cmd+=" --network ${SELECTED_NETWORK}" fi # Add volume mount docker_run_cmd+=" -v ${volume_name}:/etc/frp" # Add container name docker_run_cmd+=" --name ${container_name}" # Add image docker_run_cmd+=" snowdreamtech/frpc:0.62.0-alpine3.21" # Execute the command log_info "Executing: $docker_run_cmd" if ! eval $docker_run_cmd; then log_error "Failed to start Docker container" return 1 fi fi log_info "Docker container started successfully" echo "" >&2 } # Function to display container operation commands (FRP Mode) display_container_commands() { if [ "$FRP" = false ]; then log_info "Skipping container commands display as FRP=false" return 0 fi local container_name="${THIS_INSTANCE}-frpc" local volume_name="${THIS_INSTANCE}-frpc" # Build the docker run command string with network option local display_docker_run_cmd="docker run --restart=always -d" if [ -n "$SELECTED_NETWORK" ]; then display_docker_run_cmd+=" --network ${SELECTED_NETWORK}" fi display_docker_run_cmd+=" -v ${volume_name}:/etc/frp --name ${container_name} snowdreamtech/frpc:0.62.0-alpine3.21" # Display container operation commands in a rectangle box without side borders echo -e "${BLUE}=============================================${NC}" >&2 echo -e "${GREEN}Container Operation Commands${NC}" >&2 echo -e "" >&2 echo -e "Run Container: ${GREEN}${display_docker_run_cmd}${NC}" >&2 echo -e "------------------------------------------------------------" >&2 echo -e "Container Accessing: ${GREEN}docker exec -it ${container_name} sh${NC}" >&2 echo -e "------------------------------------------------------------" >&2 echo -e "View the Logs: ${GREEN}docker logs --tail 50 ${container_name}${NC}" >&2 echo -e "------------------------------------------------------------" >&2 echo -e "Inspect: ${GREEN}docker inspect ${container_name}${NC}" >&2 echo -e "------------------------------------------------------------" >&2 echo -e "Remove Container: ${GREEN}docker rm -f ${container_name}${NC}" >&2 echo -e "${BLUE}=============================================${NC}" >&2 echo -e "${YELLOW}[REMARK]${NC} Please copy down these operation commands for future use." >&2 echo "" >&2 } # Function to extract FRP server address from frpc.toml extract_frp_server_address() { if [ "$FRP" = false ]; then log_info "Skipping FRP server address extraction as FRP=false" return 0 fi local volume_name="${THIS_INSTANCE}-frpc" local temp_container="temp-extract-config-$(date +%s)" local server_addr="" # Create a temporary container to check for existing config if docker volume inspect "$volume_name" >/dev/null 2>&1; then # Create temporary container to access volume if docker run --rm -d --name "$temp_container" -v "$volume_name:/etc/frp" alpine:latest sleep 10 >/dev/null 2>&1; then # Check if frpc.toml exists if docker exec "$temp_container" test -f /etc/frp/frpc.toml >/dev/null 2>&1; then # Try to extract server address server_addr=$(docker exec "$temp_container" grep "^serverAddr" /etc/frp/frpc.toml | cut -d'"' -f2 || true) fi # Stop the temporary container docker stop "$temp_container" >/dev/null 2>&1 fi fi echo "$server_addr" } # Function to extract all remote ports from frpc.toml extract_all_remote_ports() { if [ "$FRP" = false ]; then log_info "Skipping remote port extraction as FRP=false" return 0 fi local volume_name="${THIS_INSTANCE}-frpc" local temp_container="temp-extract-ports-$(date +%s)" local ports=() # Create a temporary container to check for existing config if docker volume inspect "$volume_name" >/dev/null 2>&1; then # Create temporary container to access volume if docker run --rm -d --name "$temp_container" -v "$volume_name:/etc/frp" alpine:latest sleep 10 >/dev/null 2>&1; then # Check if frpc.toml exists if docker exec "$temp_container" test -f /etc/frp/frpc.toml >/dev/null 2>&1; then # Get all remotePort values from the configuration local remote_ports=$(docker exec "$temp_container" grep "remotePort" /etc/frp/frpc.toml | cut -d'=' -f2 | tr -d ' ') # Convert to array while IFS= read -r port; do if [ -n "$port" ]; then ports+=("$port") fi done <<< "$remote_ports" fi # Stop the temporary container docker stop "$temp_container" >/dev/null 2>&1 fi fi # Output all ports as space-separated values echo "${ports[@]}" } # Function to connect to target server via SSH and execute command (Traefik Mode) # ssh_exec() { # local command="$1" # log_debug "Executing SSH command: $command" # ssh -i "$SSH_KEY_PATH" -p "${SSH_PORT:-22}" "$TARGET_USER@$TARGET_SERVER" "$command" # } ssh_exec() { local command="$1" # Build the full SSH command for logging local full_ssh_cmd=(ssh) [[ -n "${SSH_KEY_PATH:-}" ]] && full_ssh_cmd+=(-i "$SSH_KEY_PATH") [[ -n "${SSH_PORT:-}" ]] && full_ssh_cmd+=(-p "$SSH_PORT") full_ssh_cmd+=("$TARGET_USER@$TARGET_SERVER" "$command") # Log the complete command (quoted for clarity) log_debug "Executing SSH command: ${full_ssh_cmd[*]}" # Actually run it ssh -i "$SSH_KEY_PATH" -p "${SSH_PORT:-22}" "$TARGET_USER@$TARGET_SERVER" "$command" } # Function to upload files to target server via SCP (Traefik Mode) scp_upload() { local source="$1" local destination="$2" log_debug "Uploading $source to $TARGET_USER@$TARGET_SERVER:$destination" scp -i "$SSH_KEY_PATH" -P "${SSH_PORT:-22}" -r "$source" "$TARGET_USER@$TARGET_SERVER:$destination" } # Function to check if Traefik container exists on target server (Traefik Mode) check_traefik_container() { log_info "Checking if Traefik container exists on target server ($TARGET_SERVER)..." local container_exists container_exists=$(ssh_exec "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 on target server" return 0 else log_info "Traefik container does not exist on target server" return 1 fi } # Function to create Traefik Docker container on target server if it doesn't exist (Traefik Mode) create_traefik_container_on_target() { log_info "Checking if Traefik container exists on target server ($TARGET_SERVER)..." local container_exists container_exists=$(ssh_exec "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 on target server" # 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.5.4 local TRAEFIK_VER="${TRAEFIK_VER:-v3.5.4}" 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 # Upload the configuration to target server scp_upload "$temp_config" "/tmp/traefik.yml" rm -f "$temp_config" # Copy the configuration file and create necessary directories in the volume with proper permissions ssh_exec "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'" 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 on target server 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' on target server..." # Create the Docker volume for persistent data using fixed name ssh_exec "docker volume create $TRAEFIK_VOLUME_NAME" fi # Use specified Traefik version or default to v3.5.4 local TRAEFIK_VER="${TRAEFIK_VER:-v3.5.4}" 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 # Upload the configuration to target server scp_upload "$temp_config" "/tmp/traefik.yml" rm -f "$temp_config" # Determine Docker version to decide if we need the version field local compose_version_field compose_version_field=$(ssh_exec " 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 echo \"version: '3.8'\" else echo \"\" fi ") # Create docker-compose.yml for Traefik using the specified version and volume local temp_compose="/tmp/traefik-compose-$$.yml" if [ -n "$compose_version_field" ]; then # Include version field for older Docker versions cat > "$temp_compose" << EOF $compose_version_field services: traefik: image: $TRAEFIK_IMAGE container_name: $TRAEFIK_CONTAINER_NAME restart: unless-stopped command: - '--api.insecure=true' - '--providers.docker=true' - '--providers.docker.exposedbydefault=false' - '--entrypoints.web.address=:80' - '--entrypoints.websecure.address=:443' - '--certificatesresolvers.letsencrypt.acme.httpchallenge=true' - '--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web' - '--certificatesresolvers.letsencrypt.acme.email=$LETSENCRYPT_EMAIL' - '--certificatesresolvers.letsencrypt.acme.storage=/etc/traefik/acme/acme.json' - '--providers.file.directory=/etc/traefik/config' - '--providers.file.watch=true' ports: - '80:80' - '443:443' - '127.0.0.1:8080:8080' # Bind dashboard to localhost only for security volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - $TRAEFIK_VOLUME_NAME:/etc/traefik labels: - 'traefik.enable=true' - 'traefik.http.routers.traefik.rule=Host(\`traefik.${DOMAIN_NAME:-traefik.localhost}\`)' - 'traefik.http.routers.traefik.service=api@internal' - 'traefik.http.routers.traefik.middlewares=auth' - 'traefik.http.middlewares.auth.basicauth.users=admin:$$apr1$$H6usmud2$$YqJ2a7vLp9J9l5zK8Y4hD1' volumes: $TRAEFIK_VOLUME_NAME: external: true networks: default: name: traefik-public external: true EOF else # No version field for newer Docker versions cat > "$temp_compose" << EOF services: traefik: image: $TRAEFIK_IMAGE container_name: $TRAEFIK_CONTAINER_NAME restart: unless-stopped command: - '--api.insecure=true' - '--providers.docker=true' - '--providers.docker.exposedbydefault=false' - '--entrypoints.web.address=:80' - '--entrypoints.websecure.address=:443' - '--certificatesresolvers.letsencrypt.acme.httpchallenge=true' - '--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web' - '--certificatesresolvers.letsencrypt.acme.email=$LETSENCRYPT_EMAIL' - '--certificatesresolvers.letsencrypt.acme.storage=/etc/traefik/acme/acme.json' - '--providers.file.directory=/etc/traefik/config' - '--providers.file.watch=true' ports: - '80:80' - '443:443' - '127.0.0.1:8080:8080' # Bind dashboard to localhost only for security volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - $TRAEFIK_VOLUME_NAME:/etc/traefik labels: - 'traefik.enable=true' - 'traefik.http.routers.traefik.rule=Host(\`traefik.${DOMAIN_NAME:-traefik.localhost}\`)' - 'traefik.http.routers.traefik.service=api@internal' - 'traefik.http.routers.traefik.middlewares=auth' - 'traefik.http.middlewares.auth.basicauth.users=admin:$$apr1$$H6usmud2$$YqJ2a7vLp9J9l5zK8Y4hD1' volumes: $TRAEFIK_VOLUME_NAME: external: true networks: default: name: traefik-public external: true EOF fi # Upload the docker-compose file to target server scp_upload "$temp_compose" "/tmp/traefik-compose.yml" rm -f "$temp_compose" # Create the network if it doesn't exist ssh_exec "docker network create traefik-public 2>/dev/null || true" # Copy the configuration file and create necessary directories in the volume with proper permissions ssh_exec "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'" # Determine which docker compose command to use based on Docker version # Execute all version checking logic on the remote server to ensure proper variable assignment local compose_cmd compose_cmd=$(ssh_exec " 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) if [ \"\$major_version\" -gt 20 ] || ([ \"\$major_version\" -eq 20 ] && [ \"\$minor_version\" -ge 10 ]); then echo \"docker compose\" else echo \"docker-compose\" fi ") # Start Traefik container using appropriate command ssh_exec "$compose_cmd -f /tmp/traefik-compose.yml up -d" log_info "Traefik container '$TRAEFIK_CONTAINER_NAME' is running on target server 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 upload files to target server via SCP (Traefik Mode) scp_upload() { local source="$1" local destination="$2" log_debug "Uploading $source to $TARGET_USER@$TARGET_SERVER:$destination" scp -i "$SSH_KEY_PATH" -P "${SSH_PORT:-22}" "$source" "$TARGET_USER@$TARGET_SERVER:$destination" } # Function to configure Traefik on target server to connect to Application Service (Traefik Mode) configure_traefik_on_target() { log_info "Configuring Traefik on target server to connect to Application Service..." # Check if the target service is running local service_running service_running=$(ssh_exec "docker ps --filter name=$TARGET_SERVICE_NAME --format '{{.Names}}' 2>/dev/null || true") if [ -z "$service_running" ]; then log_error "Target application service '$TARGET_SERVICE_NAME' is not running." log_error "Please make sure your Application container is started." exit 1 fi # Check if the target service is connected to the traefik-public network local service_in_network service_in_network=$(ssh_exec "docker inspect $TARGET_SERVICE_NAME 2>/dev/null | grep -o '\"traefik-public\"' || true") if [ -z "$service_in_network" ]; then log_error "Target application service '$TARGET_SERVICE_NAME' is not connected to 'traefik-public' network." log_error "Please ensure the target application container is connected to the 'traefik-public' network." log_error "Application should connecting to traefik-public network, here is the docker compose yml example:" log_error " services:" log_error " you-app:" log_error " networks:" log_error " - traefik-public" log_error " networks:" log_error " traefik-public:" log_error " external: true" log_error "" log_error "You can connect it with: docker network connect traefik-public $TARGET_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: ${TARGET_SERVICE_NAME}-https: rule: 'Host(\`${domain_part}\`) && PathPrefix(\`${path_part}\`)' service: ${TARGET_SERVICE_NAME}-service tls: certResolver: letsencrypt entryPoints: - websecure middlewares: - ${TARGET_SERVICE_NAME}-strip-prefix ${TARGET_SERVICE_NAME}-http: rule: 'Host(\`${domain_part}\`) && PathPrefix(\`${path_part}\`)' service: ${TARGET_SERVICE_NAME}-service entryPoints: - web middlewares: - redirect-to-https - ${TARGET_SERVICE_NAME}-strip-prefix middlewares: ${TARGET_SERVICE_NAME}-strip-prefix: stripPrefix: prefixes: - ${path_part} services: ${TARGET_SERVICE_NAME}-service: loadBalancer: servers: - url: 'http://${TARGET_SERVICE_NAME}:${TARGET_SERVICE_PORT}' passHostHeader: true EOF else # Configure for root domain routing cat > "$temp_config" << EOF http: routers: ${TARGET_SERVICE_NAME}-https: rule: 'Host(\`${DOMAIN_NAME}\`)' service: ${TARGET_SERVICE_NAME}-service tls: certResolver: letsencrypt entryPoints: - websecure ${TARGET_SERVICE_NAME}-http: rule: 'Host(\`${DOMAIN_NAME}\`)' service: ${TARGET_SERVICE_NAME}-service entryPoints: - web middlewares: - redirect-to-https services: ${TARGET_SERVICE_NAME}-service: loadBalancer: servers: - url: 'http://${TARGET_SERVICE_NAME}:${TARGET_SERVICE_PORT}' passHostHeader: true EOF fi # Upload the configuration to target server scp_upload "$temp_config" "/tmp/app-service.yml" rm -f "$temp_config" # 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}-${TARGET_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=$(ssh_exec "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" fi # Copy the configuration file into the volume using a temporary container ssh_exec "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\"" log_info "Added configuration for service ${TARGET_SERVICE_NAME} on domain $domain_base to Traefik (file: $config_file_name)" # Connect the traefik container to the traefik-public network to ensure communication ssh_exec "docker network connect traefik-public traefik 2>/dev/null || true" # Check if restart is needed based on environment variable (default: false for dynamic loading) if [ "${RESTART_TRAEFIK:-false}" = "true" ]; then # Restart Traefik to apply the new configuration ssh_exec "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 (on target server)" 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 on target server (Traefik Mode) check_app_service_on_target() { log_info "Checking if Application Service is running on target server..." local service_exists service_exists=$(ssh_exec "docker ps --filter name=$TARGET_SERVICE_NAME --format '{{.Names}}' 2>/dev/null || true") if [ -n "$service_exists" ] && [ "$service_exists" = "$TARGET_SERVICE_NAME" ]; then log_info "Application Service is running on target server" return 0 else log_error "Application Service is not running on target server: $TARGET_SERVICE_NAME" return 1 fi } # Function to deploy files to target server (Traefik Mode) deploy_files_to_target() { if [ -d "$LOCAL_FOLDER" ]; then log_info "Deploying files from $LOCAL_FOLDER to $TARGET_SERVER:$DEST_PATH" # Create destination directory ssh_exec "mkdir -p $DEST_PATH" # Sync files to target ssh_exec "rsync -avz --delete $LOCAL_FOLDER $TARGET_USER@$TARGET_SERVER:$DEST_PATH" log_info "Files deployed successfully" else log_warn "Local folder $LOCAL_FOLDER does not exist. Skipping file deployment." fi } # Function to check status of Traefik and Application Service (Traefik Mode) check_status_on_target() { log_info "Checking status of Traefik and Application Service on $TARGET_SERVER..." local traefik_status local app_status traefik_status=$(ssh_exec "docker ps --filter name=traefik --format '{{.Status}}' 2>/dev/null || echo 'Not running'") app_status=$(ssh_exec "docker ps --filter name=$TARGET_SERVICE_NAME --format '{{.Status}}' 2>/dev/null || echo 'Not running'") echo -e "Traefik status: ${GREEN}$traefik_status${NC}" echo -e "Application Service ($TARGET_SERVICE_NAME) status: ${GREEN}$app_status${NC}" } # Function to view Traefik logs on target server (Traefik Mode) view_traefik_logs() { log_info "Viewing Traefik logs..." ssh_exec "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 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 -e "${YELLOW}[PRE-REQUEST]${NC} Make sure you have key pair $SSH_KEY_PATH for target server access" >&2 echo "" >&2 # Auto-proceed with deployment (smooth operation) echo "Auto-proceeding with Traefik setup..." >&2 echo "" >&2 # Check if Application Service exists on target server if ! check_app_service_on_target; then log_error "Application Service not found on target server. Please ensure the service '$TARGET_SERVICE_NAME' is running on the target server." exit 1 fi # Create Traefik container on target server if it doesn't exist create_traefik_container_on_target # Configure Traefik on target server for the Application Service configure_traefik_on_target log_info "Traefik-only proxy setup completed successfully!" log_info "Your service should now be accessible via: https://$DOMAIN_NAME" # Show helpful commands to the user using unified function show_traefik_commands } # Setup rvs-prxy function (main function that decides FRP vs Traefik) setup_rvs_prxy() { if [ "$FRP" = false ]; then # Run Traefik-only setup log_info "Running Traefik-only setup as FRP=false" setup_traefik_only return 0 fi log_info "Setting up FRP client for instance: ${THIS_INSTANCE}" # Pre-request checks echo -e "${YELLOW}[PRE-REQUEST]${NC} Make sure your current user has permission to execute docker command." >&2 echo -e "${YELLOW}[PRE-REQUEST]${NC} Make sure you have key pairs ~/.ssh/authorized_keys and ~/.ssh/devs1" >&2 echo -e "${YELLOW}[PRE-REQUEST]${NC} Make sure you have key pair ~/.ssh/itl-devs1 for proxy server access" >&2 echo "" >&2 # Auto-proceed with deployment (smooth operation) echo "Auto-proceeding with deployment..." >&2 echo "" >&2 # Use Docker network from environment variable if available, otherwise use default if [ -n "$DOCKER_NETWORK" ]; then SELECTED_NETWORK="$DOCKER_NETWORK" log_info "Using Docker network from environment variable: ${SELECTED_NETWORK}" else SELECTED_NETWORK="" log_info "Using default Docker network" fi # Save the network selection local network_to_save="$SELECTED_NETWORK" if [ -z "$network_to_save" ]; then network_to_save="none" fi save_parameters "$network_to_save" if [ -n "$SELECTED_NETWORK" ]; then log_info "Selected Docker network: ${SELECTED_NETWORK}" else log_info "Using default Docker network" fi # Generate FRP client configuration generate_frpc_config # Start Docker container start_docker_container # Extract FRP server address and remote ports local server_addr server_addr=$(extract_frp_server_address) if [ -z "$server_addr" ]; then log_error "Failed to extract FRP server address from configuration" log_info "Skipping proxy server configuration update" else log_info "Extracted FRP server address: $server_addr" # Extract all remote ports dynamically from configuration local all_ports=() local ports_string=$(extract_all_remote_ports) if [ -n "$ports_string" ]; then all_ports=($ports_string) fi # Check if we have at least one port to configure if [ ${#all_ports[@]} -ge 1 ]; then log_info "Extracted ${#all_ports[@]} remote port(s) for proxy server update: ${all_ports[*]}" # Check if we have any web proxies local has_web=$(has_web_proxy "${proxy_configs[@]}") # Update proxy server configuration with all extracted ports update_proxy_server_config "$server_addr" "$has_web" "${all_ports[@]}" else log_error "Failed to extract any remote ports from configuration" log_info "Skipping proxy server configuration update" fi fi # Display container operation commands display_container_commands log_info "FRP client instance ${THIS_INSTANCE} deployment completed" } # List available systems list_systems() { echo -e "${GREEN}Available deployment systems:${NC}" >&2 echo -e "${YELLOW}============================${NC}" >&2 echo -e "${BLUE}•${NC} setup-rvs-prxy" >&2 echo -e "${YELLOW}============================${NC}" >&2 echo -e "Use: ${GREEN}$0 ${NC} to deploy a system" >&2 } # Usage function usage() { cat << EOF Usage: $0 [COMMAND] [OPTIONS] Available commands: setup-rvs-prxy Setup reverse proxy (either FRP or Traefik based on environment) deploy Deploy files to target server (Traefik mode only) status Check status of Traefik and Application Service (Traefik mode only) logs View Traefik logs (Traefik mode only) Environment Variables: FRP=true|false Set to 'false' for Traefik mode, 'true' for FRP mode (default: true) SKIP_FRP=true Override FRP setting to skip FRP setup (default: false) Examples: $0 setup-rvs-prxy # Run based on .env configuration FRP=false $0 setup-rvs-prxy # Force Traefik mode FRP=true $0 setup-rvs-prxy # Force FRP mode $0 deploy # Deploy files to target server $0 status # Check status $0 logs # View Traefik logs Note: This script runs on the same computer as the service it connects to. IMPORTANT: Configuration is primarily handled through .env files (.env, .env.development, .env.production) located in the current directory or script directory. The .env file contains all parameters for your setup and is essential for operation. Make sure to properly configure your .env file before running the script. EOF } # Main execution main() { # Load environment variables from .env files first load_env local command="${1:-help}" # Handle options first case "$command" in -l|--list) list_systems exit 0 ;; -h|--help|help) usage exit 0 ;; --) # Skip the -- separator and use the next argument shift command="${1:-help}" ;; "deploy") # Deploy command (Traefik mode only) if [ "$FRP" = false ]; then deploy_files_to_target log_info "File deployment completed" else log_error "Deploy command is only available in Traefik mode (FRP=false)" exit 1 fi ;; "status") # Status command (Traefik mode only) if [ "$FRP" = false ]; then check_status_on_target else log_error "Status command is only available in Traefik mode (FRP=false)" exit 1 fi ;; "logs") # Logs command (Traefik mode only) if [ "$FRP" = false ]; then view_traefik_logs else log_error "Logs command is only available in Traefik mode (FRP=false)" exit 1 fi ;; "setup-rvs-prxy") # Prompt user for instance name first prompt_instance_name "$command" # Prompt for internal service port prompt_internal_service_port # Prompt for domain name if not in environment if [ -z "$DOMAIN_NAME" ]; then DOMAIN_NAME=$(prompt_domain_name) save_to_env "DOMAIN_NAME" "$DOMAIN_NAME" fi log_info "Starting deployment: $command" setup_rvs_prxy log_info "Completed deployment: $command for instance: ${THIS_INSTANCE}" ;; *) log_error "Unknown deployment system: $command" echo "" list_systems exit 1 ;; esac } # Function to prompt for domain name prompt_domain_name() { # Check if DOMAIN_NAME is set from environment if [ -n "$DOMAIN_NAME" ]; then log_info "Using environment variable for domain name: ${DOMAIN_NAME}" echo "$DOMAIN_NAME" return fi echo -e "${BLUE}=== Domain Name Configuration ===${NC}" >&2 echo -e "${YELLOW}Please enter the domain name for this instance${NC}" >&2 echo -e "${YELLOW}(e.g., cmty-repos2.ivantito.com)${NC}" >&2 echo "" >&2 local domain_name="" while [ -z "$domain_name" ]; do domain_name=$(read_terminal "Enter domain name: " "") if [ -z "$domain_name" ]; then log_error "Domain name cannot be empty" >&2 fi done echo "$domain_name" } # Function to display post-execution command reference for Traefik show_traefik_commands() { local domain_name="${DOMAIN_NAME:-your-domain.com}" echo "" echo "=============================================" echo "Container Operation Commands (Traefik Instance)" echo "" echo "Configuration Note:" echo " By default, Traefik does not restart after configuration (RESTART_TRAEFIK=false)" echo " Traefik will dynamically load new configurations automatically." echo " Set RESTART_TRAEFIK=true in .env to force restart after configuration." echo "" echo "Run Container locally:" echo " # Use the stored docker-compose.yml from volume" echo " # First try to copy with 1000:1000 ownership, fallback to default if chown fails" echo " docker run -d --name traefik --restart=unless-stopped \\" echo " -p 80:80 -p 443:443 -p 127.0.0.1:8080:8080 \\" echo " -v /var/run/docker.sock:/var/run/docker.sock:ro \\" echo " -v traefik_data:/etc/traefik traefik:latest" echo "" echo "Restart Container: docker restart traefik" echo "------------------------------------------------------------" echo "Container Accessing by ubuntu: docker exec -it traefik /bin/sh" echo "Container Accessing by root: docker exec -it -u root traefik /bin/sh" echo "------------------------------------------------------------" echo "View the Logs: docker logs --tail 50 traefik" echo "------------------------------------------------------------" echo "Inspect: docker inspect traefik" echo "------------------------------------------------------------" echo "Remove Container: docker rm -f traefik" echo "------------------------------------------------------------" echo "View Traefik Dashboard: http://localhost:8080 (only accessible locally for security)" echo "------------------------------------------------------------" echo "Your application is accessible via: https://$domain_name" echo "------------------------------------------------------------" echo "Totally Remove (Pay Attention):" echo " docker rm -f traefik" echo " docker volume rm traefik_data" echo "=============================================" } # Execute main function with arguments main "$@"