#!/bin/bash # ============================================================ # Nginx Proxy Manager 一键管理脚本 v1 # 子命令: install(默认向导) / uninstall / status / update / backup # 用法: # bash npm.sh → 智能入口(已装→菜单, 未装→安装) # bash npm.sh install → 安装向导 # bash npm.sh status → 状态 # bash npm.sh update → 更新镜像并重启 # bash npm.sh backup → 备份数据 # bash npm.sh uninstall → 卸载 # ============================================================ RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m' info(){ echo -e "${GREEN}[✓]${NC} $1"; } warn(){ echo -e "${YELLOW}[!]${NC} $1"; } err(){ echo -e "${RED}[✗]${NC} $1"; } NPM_DIR="/opt/npm" COMPOSE="$NPM_DIR/docker-compose.yml" # ============================ 检测 ============================ need_docker(){ if ! command -v docker >/dev/null 2>&1; then warn "未安装 Docker, 开始安装..." curl -fsSL https://get.docker.com | bash 2>&1 | tail -2 systemctl enable --now docker 2>/dev/null fi # docker compose 插件 docker compose version >/dev/null 2>&1 && return warn "安装 docker compose 插件..." mkdir -p /usr/local/lib/docker/cli-plugins if [ -f /usr/lib/docker/cli-plugins/docker-compose ]; then ln -sf /usr/lib/docker/cli-plugins/docker-compose /usr/local/lib/docker/cli-plugins/docker-compose else curl -sL -m 120 -o /usr/local/lib/docker/cli-plugins/docker-compose \ "https://ghproxy.net/https://github.com/docker/compose/releases/latest/download/docker-compose-linux-x86_64" \ && chmod +x /usr/local/lib/docker/cli-plugins/docker-compose fi docker compose version >/dev/null 2>&1 || { err "docker compose 不可用"; exit 1; } } is_installed(){ [ -f "$COMPOSE" ] && docker ps --format '{{.Names}}' 2>/dev/null | grep -q npm } # ============================ 状态 ============================ do_status(){ echo -e "${BLUE}========== NPM 状态 ==========${NC}" if is_installed; then ST=$(docker inspect -f '{{.State.Status}}' npm 2>/dev/null) echo -e " ${GREEN}●${NC} 容器 npm: $ST" docker ps --format 'table {{.Names}}\t{{.Ports}}\t{{.Status}}' | head -3 UPTIME=$(docker inspect -f '{{.State.StartedAt}}' npm 2>/dev/null | cut -c1-19 | tr T ' ') echo " 启动时间: $UPTIME" else warn "NPM 未安装或未运行" fi exit 0 } # ============================ 备份 ============================ do_backup(){ [ -d "$NPM_DIR/data" ] || { err "无数据可备份"; exit 1; } TS=$(date +%Y%m%d%H%M%S) BK="/root/npm-backup-$TS.tar.gz" tar -czf "$BK" -C "$NPM_DIR" data letsencrypt 2>/dev/null info "备份完成: $BK ($(du -h "$BK" | cut -f1))" exit 0 } # ============================ update ============================ do_update(){ echo -e "${BLUE}========== NPM 升级 ==========${NC}" cd "$NPM_DIR" info "拉取最新镜像..." docker compose pull 2>&1 | tail -2 info "重建容器(数据保留)..." docker compose up -d 2>&1 | tail -2 info "升级完成, 数据/证书/配置全部保留" exit 0 } # ============================ 卸载 ============================ do_uninstall(){ echo -e "${BLUE}========== NPM 卸载 ==========${NC}" cd "$NPM_DIR" 2>/dev/null || { warn "未安装"; exit 0; } read -p "是否保留数据(证书/配置/数据库)? [Y/n]: " KEEP docker compose down 2>&1 | tail -1 info "容器已停止移除" if [[ "$KEEP" == "n" || "$KEEP" == "N" ]]; then rm -rf "$NPM_DIR" info "已删除 $NPM_DIR (数据一并清除)" else warn "数据保留在 $NPM_DIR (重装自动恢复)" fi docker images | grep -E "jc21/nginx-proxy-manager" | awk '{print $3}' | xargs -r docker rmi 2>/dev/null info "镜像已清理" exit 0 } # ============================ 安装 ============================ do_install(){ echo -e "${BLUE}=============================================${NC}" echo -e "${BLUE} Nginx Proxy Manager 一键部署${NC}" echo -e "${BLUE}=============================================${NC}" # 依赖 need_docker # ---------- 镜像来源 ---------- echo "" echo "镜像来源:" echo " 1) 在线拉取 (官方+镜像加速回退)" echo " 2) 离线安装 (本地/自定义地址的 docker 镜像包, docker load)" read -p "选择 [1/2, 默认1]: " IMG_SRC IMG_SRC=${IMG_SRC:-1} IMG_TAR="" if [ "$IMG_SRC" = "2" ]; then echo "离线包获取方式:" echo " a) 本地文件路径 (如 /root/npm.tar)" echo " b) 自定义下载地址 (http/https)" read -p "选择 [a/b]: " OFF_MODE case "$OFF_MODE" in b|B) read -p "输入下载地址: " OFF_URL [ -z "$OFF_URL" ] && { err "地址为空"; exit 1; } IMG_TAR="/tmp/npm-image.tar" info "下载镜像包..." DL_OK=0 curl -sL -m 600 --connect-timeout 15 -o "$IMG_TAR" "$OFF_URL" && [ -s "$IMG_TAR" ] && DL_OK=1 if [ $DL_OK -eq 0 ]; then warn "直连失败, 尝试镜像加速..." curl -sL -m 600 -o "$IMG_TAR" "https://ghproxy.net/${OFF_URL}" && [ -s "$IMG_TAR" ] && DL_OK=1 fi [ $DL_OK -eq 0 ] && { err "下载失败, 请检查地址"; exit 1; } ;; *) read -p "输入本地镜像包路径: " OFF_PATH [ ! -f "$OFF_PATH" ] && { err "文件不存在: $OFF_PATH"; exit 1; } IMG_TAR="$(readlink -f "$OFF_PATH")" ;; esac # 校验是 docker 镜像包 if ! docker load -i "$IMG_TAR" 2>&1 | tail -2; then err "docker load 失败: 文件不是有效的 docker 镜像包" exit 1 fi info "镜像已导入" fi # 端口输入(带冲突检测) read -p "管理界面端口 [默认 81]: " ADMIN_PORT ADMIN_PORT=${ADMIN_PORT:-81} read -p "HTTP 端口 [默认 80]: " HTTP_PORT HTTP_PORT=${HTTP_PORT:-80} read -p "HTTPS 端口 [默认 443]: " HTTPS_PORT HTTPS_PORT=${HTTPS_PORT:-443} for p in "$ADMIN_PORT" "$HTTP_PORT" "$HTTPS_PORT"; do if ss -tln | grep -q ":${p} "; then err "端口 $p 已被占用:" ss -tlnp | grep ":${p} " | head -1 read -p "换一个端口? [y/N]: " CH [[ "$CH" == "y" || "$CH" == "Y" ]] || exit 1 fi done # 确认本地导入的镜像名, 并打标准 tag if [ "$IMG_SRC" = "2" ]; then LOADED=$(docker images --format '{{.Repository}}:{{.Tag}}' | grep -iE "nginx-proxy-manager" | head -1) [ -z "$LOADED" ] && LOADED=$(docker images --format '{{.Repository}}:{{.Tag}}' | grep -iE "npm|proxy" | head -1) if [ -n "$LOADED" ] && [ "$LOADED" != "jc21/nginx-proxy-manager:latest" ]; then docker tag "$LOADED" "jc21/nginx-proxy-manager:latest" info "已打标: $LOADED → jc21/nginx-proxy-manager:latest" fi [ -z "$LOADED" ] && { err "导入后未找到 NPM 镜像, 请确认包内容"; exit 1; } fi mkdir -p "$NPM_DIR/data" "$NPM_DIR/letsencrypt" cat > "$COMPOSE" </dev/null 2>&1 && PULL_OK=1 if [ $PULL_OK -eq 0 ]; then warn "直连失败, 尝试国内镜像加速..." mkdir -p /etc/docker if [ ! -f /etc/docker/daemon.json ]; then echo '{"registry-mirrors":["https://dockerproxy.net","https://docker.m.daocloud.io"]}' > /etc/docker/daemon.json systemctl restart docker; sleep 3 fi docker compose pull >/dev/null 2>&1 && PULL_OK=1 if [ $PULL_OK -eq 0 ]; then # 手动逐源拉取再打标 for src in "dockerproxy.net/jc21/nginx-proxy-manager:latest" "docker.m.daocloud.io/jc21/nginx-proxy-manager:latest"; do docker pull "$src" && docker tag "$src" "jc21/nginx-proxy-manager:latest" && PULL_OK=1 && break done fi fi [ $PULL_OK -eq 0 ] && { err "镜像拉取失败, 检查网络后重跑本脚本(或选离线安装)"; exit 1; } else info "离线模式: 使用已导入镜像, 跳过拉取" fi cd "$NPM_DIR" docker compose up -d sleep 5 ST=$(docker inspect -f '{{.State.Status}}' npm 2>/dev/null) echo "" info "=========================================" info " NPM 部署完成! ($ST)" info " 管理界面: http://本机IP:${ADMIN_PORT}" info " 默认账号: admin@example.com / changeme" info " (首次登录强制修改, 记得改!)" info " HTTP:${HTTP_PORT} / HTTPS:${HTTPS_PORT}" info " 数据目录: $NPM_DIR (证书/配置持久化)" info " 管理: bash /usr/local/bin/npm.sh status | update | backup | uninstall" # 自落地: 把脚本保存到固定路径, 方便以后管理 if [[ "$0" == /dev/fd* || "$0" == *curl* ]] || [ ! -f /usr/local/bin/npm.sh ]; then cp "$0" /usr/local/bin/npm.sh 2>/dev/null && chmod +x /usr/local/bin/npm.sh \ && info "脚本已落地: /usr/local/bin/npm.sh (以后直接 bash npm.sh 管理)" fi info "=========================================" exit 0 } # ============================ 菜单 ============================ do_menu(){ while true; do echo "" echo -e "${BLUE}=============================================${NC}" echo -e "${BLUE} Nginx Proxy Manager 管理菜单${NC}" echo -e "${BLUE}=============================================${NC}" if is_installed; then ST=$(docker inspect -f '{{.State.Status}}' npm 2>/dev/null) echo -e " ${GREEN}●${NC} 容器: $ST" fi echo -e "${BLUE}---------------------------------------------${NC}" echo " 1) 安装" echo " 2) 启动" echo " 3) 停止" echo " 4) 重启" echo " 5) 状态详情" echo " 6) 升级 (拉最新镜像)" echo " 7) 备份数据" echo " 8) 查看日志 (最近30行)" echo " 9) 卸载" echo " 0) 退出" read -p "选择 [0-9]: " C case "$C" in 1) do_install ;; 2) cd "$NPM_DIR" && docker compose start 2>/dev/null && info "已启动" ;; 3) cd "$NPM_DIR" && docker compose stop 2>/dev/null && info "已停止" ;; 4) cd "$NPM_DIR" && docker compose restart 2>/dev/null && info "已重启" ;; 5) do_status ;; 6) do_update ;; 7) do_backup ;; 8) docker logs npm --tail 30 2>/dev/null | less -R || warn "未运行" ;; 9) do_uninstall ;; 0) exit 0 ;; *) warn "无效选择" ;; esac read -p "按回车返回菜单..." _ done } # ============================ 主入口 ============================ case "$1" in install) do_install ;; status) do_status ;; update) do_update ;; backup) do_backup ;; uninstall) do_uninstall ;; menu) do_menu ;; "") do_menu ;; -h|--help) echo "用法: bash npm.sh [install|menu|status|update|backup|uninstall]" exit 0 ;; *) err "未知命令: $1"; exit 1 ;; esac