#!/bin/bash # ============================================================ # frpc-console 一键部署脚本 (Docker 优先) # 用法: # run-deploy.sh 引导后自动执行 # ============================================================ set -e # ---------- 颜色检测 ---------- if [ -t 1 ]; then RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' BLUE='\033[0;34m'; CYAN='\033[0;36m'; MAGENTA='\033[0;35m'; NC='\033[0m' else RED=''; GREEN=''; YELLOW=''; BLUE=''; CYAN=''; MAGENTA=''; NC='' fi # ---------- 配置 ---------- REPO_URL="https://git.whitetop.xyz/lxh2875931338/frpc-console.git" WORK_DIR="/tmp/frpc-console-build" DEFAULT_PORT=9300 DEFAULT_DEPLOY_DIR="/opt/frpc-console" IMAGE_NAME="frpc-console" KEEP_IMAGES=3 # ---------- 镜像加速配置 ---------- DEFAULT_MIRRORS=( "https://docker.1panel.live" "https://docker.1panel.dev" "https://docker.1ms.run" ) DAEMON_JSON="/etc/docker/daemon.json" MIRROR_CHECK_TIMEOUT=5 # ---------- 镜像加速状态变量 ---------- MIRROR_STATUS="" # no_config | has_mirrors | default_unavailable NEED_RESTART=false # 是否需要重启 Docker NEED_REORDER=false # 是否需要重排已有镜像源 HAS_REGISTRY_MIRRORS_KEY=false # daemon.json 中是否存在 registry-mirrors 字段 AVAILABLE_MIRRORS=() # 可用的默认地址 USER_MIRRORS_AVAILABLE=() # 用户配置中可用的地址 USER_MIRRORS_UNAVAILABLE=() # 用户配置中不可用的地址 # ---------- 其他状态变量 ---------- OS=""; OS_VERSION=""; ARCH="" HAS_GIT=false; HAS_CURL=false; HAS_WGET=false; HAS_DOCKER=false HAS_JQ=false NEED_INSTALL_GIT=false; NEED_INSTALL_CURL=false; NEED_INSTALL_WGET=false PORT=${DEFAULT_PORT} DEPLOY_DIR=${DEFAULT_DEPLOY_DIR} DATA_DIR="${DEPLOY_DIR}/data" CONTAINER_EXISTS=false; CONTAINER_RUNNING=false SKIP_CONFIRM=false; CHECK_ONLY=false; DRY_RUN=false CHANNEL=""; BRANCH=""; TARGET_VERSION=""; IMAGE_TAG="" CURRENT_VERSION="" # ---------- 打印函数 ---------- print_info() { echo -e "${BLUE}[INFO]${NC} $1"; } print_success() { echo -e "${GREEN}[✓]${NC} $1"; } print_warn() { echo -e "${YELLOW}[⚠]${NC} $1"; } print_error() { echo -e "${RED}[✗]${NC} $1"; } print_step() { echo -e "\n${CYAN}▶${NC} $1"; } print_title() { echo -e "\n${MAGENTA}════════════════════════════════════════════════════════${NC}"; } print_subtitle() { echo -e "${MAGENTA} $1${NC}"; } # ---------- 解析命令行参数 ---------- parse_args() { for arg in "$@"; do case $arg in --yes|-y) SKIP_CONFIRM=true ;; --check) CHECK_ONLY=true ;; --dry-run) DRY_RUN=true ;; --help|-h) echo "用法: ./deploy.sh [选项]" echo "选项:" echo " --yes, -y 跳过所有确认提示" echo " --check 只检测环境,不执行部署" echo " --dry-run 显示将执行的操作,不实际执行" echo " --help, -h 显示帮助信息" exit 0 ;; *) print_error "未知选项: $arg"; exit 1 ;; esac done } # ---------- 权限检测 ---------- check_root() { if [ "$EUID" -ne 0 ]; then print_error "请使用 root 权限运行此脚本" echo " sudo bash deploy.sh" exit 1 fi } # ---------- 系统检测 ---------- detect_os() { if [ -f /etc/os-release ]; then . /etc/os-release OS=$ID; OS_VERSION=$VERSION_ID else print_error "无法识别操作系统" exit 1 fi } detect_arch() { ARCH=$(uname -m) case $ARCH in x86_64|amd64|aarch64|arm64|armv7l|armhf) print_success "CPU 架构: $ARCH" ;; *) print_error "不支持的 CPU 架构: $ARCH" exit 1 ;; esac } # ---------- 工具检测 ---------- check_tools() { command -v git &> /dev/null && HAS_GIT=true || NEED_INSTALL_GIT=true command -v curl &> /dev/null && HAS_CURL=true || NEED_INSTALL_CURL=true command -v wget &> /dev/null && HAS_WGET=true || NEED_INSTALL_WGET=true command -v docker &> /dev/null && HAS_DOCKER=true command -v jq &> /dev/null && HAS_JQ=true || HAS_JQ=false } check_container() { if docker ps -a --format '{{.Names}}' 2>/dev/null | grep -q "^frpc-console$"; then CONTAINER_EXISTS=true if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^frpc-console$"; then CONTAINER_RUNNING=true fi fi } # ---------- 镜像加速:连通性检测 ---------- check_mirror_available() { local url="$1" local http_code http_code=$(curl -s --max-time "$MIRROR_CHECK_TIMEOUT" -o /dev/null -w "%{http_code}" \ "${url}/v2/" 2>/dev/null) if [[ "$http_code" =~ ^2[0-9][0-9]$ ]] || [ "$http_code" = "401" ] || [ "$http_code" = "403" ]; then # 2xx 表示成功,401/403 表示需要认证但服务可达(也是可用的) return 0 fi return 1 } # ---------- 镜像加速:安装 jq ---------- install_jq() { print_info "尝试安装 jq..." case $OS in opensuse*|suse*|opensuse-tumbleweed|opensuse-slowroll|opensuse-leap) zypper install -y jq 2>/dev/null || true ;; ubuntu|debian|linuxmint) apt update -qq 2>/dev/null && apt install -y jq 2>/dev/null || true ;; centos|rhel|fedora|rocky|almalinux) yum install -y jq 2>/dev/null || true ;; alpine) apk add jq 2>/dev/null || true ;; arch|manjaro|endeavouros) pacman -S --needed --noconfirm jq 2>/dev/null || true ;; *) return 1 ;; esac command -v jq &> /dev/null && HAS_JQ=true || HAS_JQ=false if [ "$HAS_JQ" = true ]; then print_success "jq 安装成功" return 0 else print_warn "jq 安装失败,将使用 sed 模式(功能受限)" return 1 fi } # ---------- 镜像加速:检测阶段 ---------- check_docker_mirrors() { print_step "检测 Docker 镜像源配置..." # 1. 检测三个默认地址的连通性 print_info "检测默认镜像源连通性 (超时 ${MIRROR_CHECK_TIMEOUT}s)..." for mirror in "${DEFAULT_MIRRORS[@]}"; do if check_mirror_available "$mirror"; then AVAILABLE_MIRRORS+=("$mirror") print_info " ✅ $mirror 可用" else print_info " ❌ $mirror 不可用" fi done if [ ${#AVAILABLE_MIRRORS[@]} -eq 0 ]; then MIRROR_STATUS="default_unavailable" print_warn "所有默认镜像源均不可用,将跳过镜像加速配置" print_warn "如果 Docker 构建失败,请检查网络或手动配置镜像源" return 0 fi print_success "找到 ${#AVAILABLE_MIRRORS[@]} 个可用默认镜像源" # 2. 检查 daemon.json if [ ! -f "$DAEMON_JSON" ]; then MIRROR_STATUS="no_config" HAS_REGISTRY_MIRRORS_KEY=false print_info "daemon.json 不存在,将创建新配置" return 0 fi # 3. 检测 jq 是否可解析 if ! jq -e '.' "$DAEMON_JSON" >/dev/null 2>&1 2>/dev/null; then print_warn "daemon.json 格式不标准,将备份并重建" local backup_path="${DAEMON_JSON}.bak.$(date +%Y%m%d-%H%M%S)" cp "$DAEMON_JSON" "$backup_path" print_info "已备份至: $backup_path" echo '{}' > "$DAEMON_JSON" print_warn "已重建 daemon.json,请确认是否需要将其他自定义配置迁回" MIRROR_STATUS="no_config" HAS_REGISTRY_MIRRORS_KEY=false return 0 fi # 4. 检查是否存在 registry-mirrors 字段 local has_key has_key=$(jq -e 'has("registry-mirrors")' "$DAEMON_JSON" 2>/dev/null) if [ "$has_key" != "true" ]; then HAS_REGISTRY_MIRRORS_KEY=false MIRROR_STATUS="has_mirrors" print_info "daemon.json 中无 registry-mirrors 字段,将仅插入该字段(保留其他配置)" return 0 fi HAS_REGISTRY_MIRRORS_KEY=true # 5. 读取 registry-mirrors 数组 local mirror_list mirror_list=$(jq -r '."registry-mirrors" // [] | .[]' "$DAEMON_JSON" 2>/dev/null) if [ -z "$mirror_list" ]; then MIRROR_STATUS="has_mirrors" print_info "registry-mirrors 为空数组,将写入默认地址" return 0 fi # 6. 检测用户配置中每个地址的连通性 print_info "检测用户配置的镜像源连通性..." USER_MIRRORS_AVAILABLE=() USER_MIRRORS_UNAVAILABLE=() while IFS= read -r mirror; do if [ -n "$mirror" ]; then if check_mirror_available "$mirror"; then USER_MIRRORS_AVAILABLE+=("$mirror") print_info " ✅ $mirror (用户配置) 可用" else USER_MIRRORS_UNAVAILABLE+=("$mirror") print_info " ❌ $mirror (用户配置) 不可用" fi fi done <<< "$mirror_list" MIRROR_STATUS="has_mirrors" # 7. 判断是否需要重排 if [ ${#USER_MIRRORS_AVAILABLE[@]} -gt 0 ]; then # 检查可用地址是否已经全部排在不可用地址前面 NEED_REORDER=false local seen_unavailable=false for mirror in "${USER_MIRRORS_AVAILABLE[@]}" "${USER_MIRRORS_UNAVAILABLE[@]}"; do # 遍历原始顺序:在 USER_MIRRORS_AVAILABLE 和 USER_MIRRORS_UNAVAILABLE 的拼接中, # 如果先遇到不可用,再遇到可用,说明需要重排 # 但我们这里无法直接还原原始顺序,用另一个方法:检查原始列表中是否存在可用在不可用之后 # 更简单的方法:如果存在任何一个不可用在可用之前 # 用原始列表检测 local raw_list raw_list=$(jq -r '."registry-mirrors" // [] | .[]' "$DAEMON_JSON" 2>/dev/null) local raw_available=() local raw_unavailable=() while IFS= read -r m; do if [ -n "$m" ]; then local found=false for a in "${USER_MIRRORS_AVAILABLE[@]}"; do if [ "$a" = "$m" ]; then raw_available+=("$m") found=true break fi done if [ "$found" = false ]; then raw_unavailable+=("$m") fi fi done <<< "$raw_list" # 检查原始列表中,是否有不可用地址出现在可用地址之前 local seen_avail=false for m in "${raw_available[@]}" "${raw_unavailable[@]}"; do # 这里只检查原始顺序 true done # 更准确的方法:遍历原始列表,记录是否见过不可用 local seen_unavail=false local need_reorder=false while IFS= read -r m; do if [ -n "$m" ]; then local is_avail=false for a in "${USER_MIRRORS_AVAILABLE[@]}"; do if [ "$a" = "$m" ]; then is_avail=true break fi done if [ "$is_avail" = true ] && [ "$seen_unavail" = true ]; then need_reorder=true break fi if [ "$is_avail" = false ]; then seen_unavail=true fi fi done <<< "$raw_list" NEED_REORDER=$need_reorder done if [ "$NEED_REORDER" = true ]; then print_info "检测到可用镜像源排在不可用镜像源之后,需要重排" else print_info "用户镜像源配置正常,无需调整" fi elif [ ${#USER_MIRRORS_UNAVAILABLE[@]} -gt 0 ] && [ ${#AVAILABLE_MIRRORS[@]} -gt 0 ]; then # 用户配置全部不可用,但有可用的默认地址 NEED_REORDER=true print_info "用户配置全部不可用,将插入可用默认地址" fi print_success "镜像源检测完成" } # ---------- 镜像加速:配置执行阶段 ---------- configure_docker_mirrors() { print_step "配置 Docker 镜像源..." if [ "$MIRROR_STATUS" = "default_unavailable" ]; then print_warn "无可用默认镜像源,跳过配置" return 0 fi if [ ${#AVAILABLE_MIRRORS[@]} -eq 0 ]; then print_warn "无可用的默认镜像源,跳过配置" return 0 fi # 确保 jq 可用 if [ "$HAS_JQ" = false ]; then print_warn "jq 未安装,尝试安装..." if ! install_jq; then print_error "jq 安装失败,无法配置镜像源" print_error "请手动配置 /etc/docker/daemon.json 中的 registry-mirrors" return 1 fi fi local new_mirrors=() local need_write=false local need_restart=false # 情况判断 if [ "$HAS_REGISTRY_MIRRORS_KEY" = false ]; then # 不存在 registry-mirrors 字段,直接插入 new_mirrors=("${AVAILABLE_MIRRORS[@]}") need_write=true need_restart=true print_info "插入 registry-mirrors 字段: ${AVAILABLE_MIRRORS[*]}" elif [ ${#USER_MIRRORS_AVAILABLE[@]} -eq 0 ] && [ ${#USER_MIRRORS_UNAVAILABLE[@]} -eq 0 ]; then # 空数组,直接写入 new_mirrors=("${AVAILABLE_MIRRORS[@]}") need_write=true need_restart=true print_info "写入 registry-mirrors: ${AVAILABLE_MIRRORS[*]}" elif [ ${#USER_MIRRORS_AVAILABLE[@]} -gt 0 ]; then if [ "$NEED_REORDER" = true ]; then # 重排:可用地址在前 + 不可用地址在后(仅限用户配置中不可用的) new_mirrors=("${USER_MIRRORS_AVAILABLE[@]}" "${USER_MIRRORS_UNAVAILABLE[@]}") need_write=true need_restart=true print_info "重排镜像源: 可用前置" else print_info "镜像源配置正常,无需修改" return 0 fi elif [ ${#USER_MIRRORS_AVAILABLE[@]} -eq 0 ] && [ ${#USER_MIRRORS_UNAVAILABLE[@]} -gt 0 ]; then # 用户配置全部不可用,插入可用默认地址到最前面 new_mirrors=("${AVAILABLE_MIRRORS[@]}" "${USER_MIRRORS_UNAVAILABLE[@]}") need_write=true need_restart=true print_info "插入可用默认镜像源到最前面: ${AVAILABLE_MIRRORS[*]}" else print_info "无需修改镜像源配置" return 0 fi if [ "$need_write" = false ]; then return 0 fi # 检查是否需要重启 Docker(交互模式确认) if [ "$need_restart" = true ] && [ "$SKIP_CONFIRM" != true ]; then echo "" echo -e "${YELLOW}⚠ 修改 Docker 镜像源配置需要重启 Docker 服务。${NC}" echo -e "${YELLOW} 重启可能影响其他正在运行的容器。${NC}" read -p "确认继续? [y/N]: " confirm "${DAEMON_JSON}.tmp" 2>/dev/null; then print_error "写入 daemon.json 失败" return 1 fi mv "${DAEMON_JSON}.tmp" "$DAEMON_JSON" print_success "daemon.json 已更新" # 重启 Docker if [ "$need_restart" = true ]; then print_step "重启 Docker 服务..." if systemctl restart docker 2>/dev/null; then print_success "Docker 重启完成" sleep 2 else print_error "Docker 重启失败,请手动重启后重新运行脚本" print_info "手动重启命令: systemctl restart docker" exit 1 fi fi print_success "镜像源配置完成" } # ---------- 版本管理 ---------- read_version_from_remote() { local branch="$1" local version_url="https://git.whitetop.xyz/lxh2875931338/frpc-console/raw/${branch}/version.ini" local content content=$(curl -sSL "$version_url" 2>/dev/null) if [ -z "$content" ]; then print_error "无法从远程读取 version.ini: $version_url" return 1 fi SEMVER=$(echo "$content" | grep -v "^[;#]" | head -1 | awk '{print $1}') echo "$SEMVER" } generate_target_version() { local channel="$1" local branch="${2:-$BRANCH}" SEMVER=$(read_version_from_remote "$branch") if [ -z "$SEMVER" ]; then print_error "读取语义版本失败" exit 1 fi local minor="${SEMVER#*.}" if [ "$minor" = "0" ] || [ "$minor" = "5" ]; then CHANNEL_TYPE="lts" else CHANNEL_TYPE="preview" fi if [ "$channel" = "lts" ] || [ "$CHANNEL_TYPE" = "lts" ]; then TARGET_VERSION="${SEMVER}-lts-$(date +%Y%m%d)" IMAGE_TAG="${SEMVER}-lts-$(date +%Y%m%d)" else TIMESTAMP=$(date +%Y%m%d-%H%M%S) TARGET_VERSION="${SEMVER}-preview-${TIMESTAMP}" IMAGE_TAG="preview-${TIMESTAMP}" fi } get_current_version() { local version_file="${DEPLOY_DIR}/data/version.ini" if [ -f "$version_file" ]; then grep -v "^[;#]" "$version_file" | head -1 | tr -d '\n' else if [ "$CONTAINER_EXISTS" = true ]; then docker exec frpc-console cat /app/data/version.ini 2>/dev/null | grep -v "^[;#]" | head -1 | tr -d '\n' || echo "" else echo "" fi fi } # ---------- 通道选择 ---------- select_channel() { if [ -n "$DEPLOY_CHANNEL" ] && [ -n "$DEPLOY_BRANCH" ]; then CHANNEL="$DEPLOY_CHANNEL" BRANCH="$DEPLOY_BRANCH" print_info "使用环境变量: ${CHANNEL} 通道 (${BRANCH} 分支)" return fi if [ -n "$CHANNEL" ]; then if [ "$CHANNEL" != "lts" ] && [ "$CHANNEL" != "preview" ]; then print_error "无效通道: $CHANNEL" exit 1 fi BRANCH="$([ "$CHANNEL" = "preview" ] && echo "test" || echo "main")" return fi echo "" echo -e "${CYAN}请选择部署通道:${NC}" echo " 1. LTS (稳定版) [默认] → main 分支" echo " 2. Preview (技术预览版) → test 分支" echo "" read -p "请选择 [1]: " CHANNEL_INPUT /dev/null; then print_warn "检测到降级操作: $CURRENT_VERSION → $TARGET_VERSION" echo "" echo -e "${YELLOW}⚠ 降级可能导致数据不兼容${NC}" TIMESTAMP=$(date +%Y%m%d_%H%M%S) BACKUP_DIR="/opt/frpc-console-backups/${TIMESTAMP}_${CHANNEL}" echo -e "${CYAN}备份目录: ${BACKUP_DIR}${NC}" read -p "确认执行降级? [y/N]: " CONFIRM_DOWNGRADE /dev/null || true fi docker rm frpc-console 2>/dev/null || true print_success "旧容器已清理" fi # ----- 准备部署目录 ----- print_step "准备部署目录..." mkdir -p "$DEPLOY_DIR" mkdir -p "$DATA_DIR" # ----- 检测并迁移旧版本文件(从根目录迁移到 data/)----- print_step "检测旧版本文件..." OLD_DB="${DEPLOY_DIR}/frpc-console.db" OLD_TOML="${DEPLOY_DIR}/frpc.toml" OLD_LOG="${DEPLOY_DIR}/frpc.log" OLD_PID="${DEPLOY_DIR}/frpc.pid" OLD_VERSION="${DEPLOY_DIR}/version.ini" NEW_DB="${DATA_DIR}/frpc-console.db" if [ -f "$OLD_DB" ] && [ ! -f "$NEW_DB" ]; then print_warn "检测到旧版本数据库文件在根目录,将迁移到 data/ 子目录" BACKUP_OLD_DIR="${DEPLOY_DIR}/.old_backup_$(date +%Y%m%d_%H%M%S)" mkdir -p "$BACKUP_OLD_DIR" for file in frpc-console.db frpc.toml frpc.log frpc.pid version.ini; do if [ -f "${DEPLOY_DIR}/${file}" ]; then cp "${DEPLOY_DIR}/${file}" "${BACKUP_OLD_DIR}/" 2>/dev/null || true print_info " 已备份: ${file}" fi done for file in frpc-console.db frpc.toml frpc.log frpc.pid version.ini; do if [ -f "${DEPLOY_DIR}/${file}" ]; then cp "${DEPLOY_DIR}/${file}" "${DATA_DIR}/" 2>/dev/null || true print_info " 已迁移: ${file} → data/" fi done print_success "旧文件已迁移到 data/ 目录" print_info "旧文件备份位置: ${BACKUP_OLD_DIR}" print_info "确认服务正常后,可手动删除备份目录" else print_info "未检测到需要迁移的旧文件" fi # ----- 构建 Docker 镜像 ----- print_step "构建 Docker 镜像..." docker build --no-cache \ -t "${IMAGE_NAME}:${IMAGE_TAG}" \ . if [ $? -ne 0 ]; then print_error "Docker 构建失败" exit 1 fi print_success "Docker 镜像构建完成: ${IMAGE_NAME}:${IMAGE_TAG}" # 验证镜像内容 print_step "验证镜像内容..." if docker run --rm --entrypoint ls "${IMAGE_NAME}:${IMAGE_TAG}" -l /app/frpc-console 2>/dev/null; then print_success "二进制文件存在于镜像中" else print_error "镜像中缺少 /app/frpc-console" exit 1 fi # 标记通道标签 if [ "$CHANNEL" = "lts" ]; then docker tag "${IMAGE_NAME}:${IMAGE_TAG}" "${IMAGE_NAME}:lts" print_info "已标记 ${IMAGE_NAME}:lts" else docker tag "${IMAGE_NAME}:${IMAGE_TAG}" "${IMAGE_NAME}:preview" print_info "已标记 ${IMAGE_NAME}:preview" fi # ----- 启动新容器 ----- print_step "启动 frpc-console 容器..." docker run -d \ --name frpc-console \ --restart=always \ --network host \ -v ${DEPLOY_DIR}/data:/app/data \ -e PORT=${PORT} \ -e TZ=Asia/Shanghai \ ${IMAGE_NAME}:${IMAGE_TAG} if docker ps | grep -q frpc-console; then print_success "容器启动成功!" else print_error "容器启动失败,请检查日志: docker logs frpc-console" exit 1 fi # ----- 写入版本文件 ----- print_step "写入版本文件..." echo "$TARGET_VERSION" > "${DEPLOY_DIR}/data/version.ini" print_success "版本写入: ${TARGET_VERSION}" # ----- 检查 frpc 子进程(基于 PID 文件)----- print_step "检查 frpc 状态..." sleep 3 PID_FILE="${DEPLOY_DIR}/data/frpc.pid" if [ -f "$PID_FILE" ]; then PID=$(cat "$PID_FILE" 2>/dev/null | tr -d ' \n') if [ -n "$PID" ] && kill -0 "$PID" 2>/dev/null; then print_success "frpc 进程运行正常 (PID: $PID)" else print_warn "frpc 进程未运行(PID 文件存在但进程已退出,可能配置为空)" fi else print_warn "frpc 进程未运行(PID 文件不存在,请导入 TOML 后重试)" fi # ----- 事务后钩子:检测数据库 ----- print_step "验证数据库状态..." if docker exec frpc-console sqlite3 /app/data/frpc-console.db "SELECT COUNT(*) FROM users;" 2>/dev/null | grep -q "^[0-9]"; then print_success "数据库可用" else print_warn "数据库为空或不可用,请通过 WebUI 注册管理员账户" fi # ----- 验证版本 ----- print_step "验证版本..." sleep 1 CONTAINER_VERSION=$(docker exec frpc-console cat /app/data/version.ini 2>/dev/null | grep -v "^[;#]" | head -1 | tr -d '\n' || echo "unknown") echo -e " 容器版本: ${CONTAINER_VERSION}" echo -e " 目标版本: ${TARGET_VERSION}" if [ "$CONTAINER_VERSION" = "$TARGET_VERSION" ]; then print_success "版本写入成功" else print_warn "版本不匹配,请检查" fi # ----- 清理旧镜像 ----- print_step "清理旧镜像..." CURRENT_IMAGE_ID=$(docker images -q "${IMAGE_NAME}:${IMAGE_TAG}" 2>/dev/null) if [ -n "$CURRENT_IMAGE_ID" ]; then ALL_IMAGES=$(docker images "${IMAGE_NAME}:*" --format "{{.ID}} {{.CreatedAt}}" 2>/dev/null | sort -k2 -r | awk '{print $1}') COUNTER=0 TO_DELETE="" for IMG_ID in $ALL_IMAGES; do if [ "$IMG_ID" = "$CURRENT_IMAGE_ID" ]; then continue fi COUNTER=$((COUNTER + 1)) if [ $COUNTER -le $KEEP_IMAGES ]; then continue fi TO_DELETE="$TO_DELETE $IMG_ID" done if [ -n "$TO_DELETE" ]; then docker rmi -f $TO_DELETE 2>/dev/null || true print_info "已清理旧镜像" else print_info "无需清理,镜像数量未超过保留策略" fi fi # ----- 备份目录提示 ----- if [ -n "$BACKUP_DIR" ]; then echo "" print_warn "降级前数据已备份到: $BACKUP_DIR" echo " 如需回退: cp -r $BACKUP_DIR/frpc-console/* $DEPLOY_DIR/" fi # ----- 清理临时文件 ----- print_step "清理临时文件..." rm -rf "$WORK_DIR" print_success "临时文件已清理" # ----- 输出 ----- print_title print_success "frpc-console 部署完成!" print_title echo "" echo -e " ${CYAN}📍 访问地址:${NC} http://$(hostname -I | awk '{print $1}'):${PORT}" echo -e " ${CYAN}📂 数据目录:${NC} ${DEPLOY_DIR}" echo -e " ${CYAN}🐳 容器名称:${NC} frpc-console" echo -e " ${CYAN}🏷️ 版本:${NC} ${TARGET_VERSION}" echo -e " ${CYAN}🌿 分支:${NC} ${BRANCH}" echo "" echo -e " ${CYAN}常用命令:${NC}" echo " docker logs frpc-console # 查看日志" echo " docker restart frpc-console # 重启服务" echo " cat ${DEPLOY_DIR}/data/version.ini # 查看当前版本" echo "" echo -e " ${YELLOW}首次访问需要注册管理员账户${NC}" echo "" print_title } # ---------- 主流程 ---------- main() { clear 2>/dev/null || true parse_args "$@" check_root print_step "检测环境..." detect_os detect_arch check_tools check_container # ---------- 镜像源检测(拉取代码前,只检测不修改) ---------- check_docker_mirrors select_channel generate_target_version "$CHANNEL" "$BRANCH" CURRENT_VERSION="" if [ "$CONTAINER_EXISTS" = true ]; then CURRENT_VERSION=$(get_current_version) fi print_environment_summary if [ "$CHECK_ONLY" = true ]; then print_info "环境检测完成(--check 模式)" exit 0 fi if [ "$HAS_DOCKER" = false ]; then print_error "Docker 未安装" exit 1 fi generate_plan print_deployment_plan if ! confirm_deploy; then custom_config generate_plan print_deployment_plan if ! confirm_deploy; then print_info "已取消部署" exit 0 fi fi if [ "$DRY_RUN" = true ]; then print_info "演练模式(--dry-run)" exit 0 fi do_deploy } main "$@"