📄 npm.sh 📝 编辑 ⬇ 下载 ← 返回列表
#!/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='\[;31m'; GREEN='\[;32m'; YELLOW='\[;33m'; BLUE='\[;34m'; NC='\[0m'
info(){ echo -e "${GREEN}[✓]${NC} $"; }
warn(){ echo -e "${YELLOW}[!]${NC} $"; }
err(){  echo -e "${RED}[✗]${NC} $"; }

NPM_DIR="/opt/npm"
COMPOSE="$NPM_DIR/docker-compose.yml"

# ============================ 检测 ============================
need_docker(){
   ! command -v docker >/dev/null >&; then
    warn "未安装 Docker, 开始安装..."
    curl -fsSL https://get.docker.com | bash >& | tail -
    systemctl enable --now docker >/dev/null
  fi
  # docker compose 插件
  docker compose version >/dev/null >& && 
  warn "安装 docker compose 插件..."
  mkdir -p /usr/local/lib/docker/cli-plugins
   [ -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
  
    curl -sL -m  -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 >& || { err "docker compose 不可用"; exit ; }
}

is_installed(){
  [ -f "$COMPOSE" ] && docker ps --format '{{.Names}}' >/dev/null | grep -q npm
}

# ============================ 状态 ============================
do_status(){
  echo -e "${BLUE}========== NPM 状态 ==========${NC}"
   is_installed; then
    ST=$(docker inspect -f '{{.State.Status}}' npm >/dev/null)
    echo -e "  ${GREEN}●${NC} 容器 npm: $ST"
    docker ps --format 'table {{.Names}}\t{{.Ports}}\t{{.Status}}' | head -
    UPTIME=$(docker inspect -f '{{.State.StartedAt}}' npm >/dev/null | cut -c1- | tr T ' ')
    echo "  启动时间: $UPTIME"
  
    warn "NPM 未安装或未运行"
  fi
  exit 
}

# ============================ 备份 ============================
do_backup(){
  [ -d "$NPM_DIR/data" ] || { err "无数据可备份"; exit ; }
  TS=$(date +%Y%m%d%H%M%S)
  BK="/root/npm-backup-$TS.tar.gz"
  tar -czf "$BK" -C "$NPM_DIR" data letsencrypt >/dev/null
  info "备份完成: $BK ($(du -h "$BK" | cut -f1))"
  exit 
}

# ============================ update ============================
do_update(){
  echo -e "${BLUE}========== NPM 升级 ==========${NC}"
  cd "$NPM_DIR"
  info "拉取最新镜像..."
  docker compose pull >& | tail -
  info "重建容器(数据保留)..."
  docker compose up -d >& | tail -
  info "升级完成, 数据/证书/配置全部保留"
  exit 
}

# ============================ 卸载 ============================
do_uninstall(){
  echo -e "${BLUE}========== NPM 卸载 ==========${NC}"
  cd "$NPM_DIR" >/dev/null || { warn "未安装"; exit ; }
  read -p "是否保留数据(证书/配置/数据库)? [Y/n]: " KEEP
  docker compose down >& | tail -
  info "容器已停止移除"
   [[ "$KEEP" == "n" || "$KEEP" == "N" ]]; then
    rm -rf "$NPM_DIR"
    info "已删除 $NPM_DIR (数据一并清除)"
  
    warn "数据保留在 $NPM_DIR (重装自动恢复)"
  fi
  docker images | grep -E "jc21/nginx-proxy-manager" | awk '{ $}' | xargs -r docker rmi >/dev/null
  info "镜像已清理"
  exit 
}

# ============================ 安装 ============================
do_install(){
  echo -e "${BLUE}=============================================${NC}"
  echo -e "${BLUE}   Nginx Proxy Manager 一键部署${NC}"
  echo -e "${BLUE}=============================================${NC}"

  # 依赖
  need_docker

  # ---------- 镜像来源 ----------
  echo ""
  echo "镜像来源:"
  echo "  ) 在线拉取 (官方+镜像加速回退)"
  echo "  ) 离线安装 (本地/自定义地址的 docker 镜像包, docker load)"
  read -p "选择 [/, 默认1]: " IMG_SRC
  IMG_SRC=${IMG_SRC:-}

  IMG_TAR=""
   [ "$IMG_SRC" = "" ]; then
    echo "离线包获取方式:"
    echo "  a) 本地文件路径 (如 /root/npm.tar)"
    echo "  b) 自定义下载地址 (http/https)"
    read -p "选择 [a/b]: " OFF_MODE
    case "$OFF_MODE" 
      b|B)
        read -p "输入下载地址: " OFF_URL
        [ -z "$OFF_URL" ] && { err "地址为空"; exit ; }
        IMG_TAR="/tmp/npm-image.tar"
        info "下载镜像包..."
        DL_OK=
        curl -sL -m  --connect-timeout  -o "$IMG_TAR" "$OFF_URL" && [ -s "$IMG_TAR" ] && DL_OK=
         [ $DL_OK -eq  ]; then
          warn "直连失败, 尝试镜像加速..."
          curl -sL -m  -o "$IMG_TAR" "https://ghproxy.net/${OFF_URL}" && [ -s "$IMG_TAR" ] && DL_OK=
        fi
        [ $DL_OK -eq  ] && { err "下载失败, 请检查地址"; exit ; }
        ;;
      *)
        read -p "输入本地镜像包路径: " OFF_PATH
        [ ! -f "$OFF_PATH" ] && { err "文件不存在: $OFF_PATH"; exit ; }
        IMG_TAR="$(readlink -f "$OFF_PATH")"
        ;;
    esac
    # 校验是 docker 镜像包
     ! docker load -i "$IMG_TAR" >& | tail -; then
      err "docker load 失败: 文件不是有效的 docker 镜像包"
      exit 
    fi
    info "镜像已导入"
  fi

  # 端口输入(带冲突检测)
  read -p "管理界面端口 [默认 ]: " ADMIN_PORT
  ADMIN_PORT=${ADMIN_PORT:-}
  read -p "HTTP 端口 [默认 ]: " HTTP_PORT
  HTTP_PORT=${HTTP_PORT:-}
  read -p "HTTPS 端口 [默认 ]: " HTTPS_PORT
  HTTPS_PORT=${HTTPS_PORT:-}

   p  "$ADMIN_PORT" "$HTTP_PORT" "$HTTPS_PORT"; do
     ss -tln | grep -q ":${p} "; then
      err "端口 $p 已被占用:"
      ss -tlnp | grep ":${p} " | head -
      read -p "换一个端口? [y/N]: " CH
      [[ "$CH" == "y" || "$CH" == "Y" ]] || exit 
    fi
  done

  # 确认本地导入的镜像名, 并打标准 tag
   [ "$IMG_SRC" = "" ]; then
    LOADED=$(docker images --format '{{.Repository}}:{{.Tag}}' | grep -iE "nginx-proxy-manager" | head -)
    [ -z "$LOADED" ] && LOADED=$(docker images --format '{{.Repository}}:{{.Tag}}' | grep -iE "npm|proxy" | head -)
     [ -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 ; }
  fi

  mkdir -p "$NPM_DIR/data" "$NPM_DIR/letsencrypt"

  cat > "$COMPOSE" <<EOF
services:
  app:
    image: jc21/nginx-proxy-manager:latest
    container_name: npm
    restart: unless-stopped
    ports:
      - "${HTTP_PORT}:"
      - "${HTTPS_PORT}:"
      - "${ADMIN_PORT}:"
    environment:
      DISABLE_IPV6: "true"
    volumes:
      - ${NPM_DIR}/data:/data
      - ${NPM_DIR}/letsencrypt:/etc/letsencrypt
EOF

   [ "$IMG_SRC" = "" ]; then
    echo ""
    info "拉取镜像 (约 500MB, 视网络 - 分钟)..."
    cd "$NPM_DIR"
    PULL_OK=
    # 尝试直连, 失败换国内镜像
    docker compose pull >/dev/null >& && PULL_OK=
     [ $PULL_OK -eq  ]; then
      warn "直连失败, 尝试国内镜像加速..."
      mkdir -p /etc/docker
       [ ! -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 
      fi
      docker compose pull >/dev/null >& && PULL_OK=
       [ $PULL_OK -eq  ]; then
        # 手动逐源拉取再打标
         src  "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= && 
        done
      fi
    fi
    [ $PULL_OK -eq  ] && { err "镜像拉取失败, 检查网络后重跑本脚本(或选离线安装)"; exit ; }
  
    info "离线模式: 使用已导入镜像, 跳过拉取"
  fi

  cd "$NPM_DIR"
  docker compose up -d
  sleep 
  ST=$(docker inspect -f '{{.State.Status}}' npm >/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"
  # 自落地: 把脚本保存到固定路径, 方便以后管理
   [[ "$" == /dev/fd* || "$" == *curl* ]] || [ ! -f /usr/local/bin/npm.sh ]; then
    cp "$" /usr/local/bin/npm.sh >/dev/null && chmod +x /usr/local/bin/npm.sh \
      && info "脚本已落地: /usr/local/bin/npm.sh (以后直接 bash npm.sh 管理)"
  fi
  info "========================================="
  exit 
}

# ============================ 菜单 ============================
do_menu(){
   true; do
    echo ""
    echo -e "${BLUE}=============================================${NC}"
    echo -e "${BLUE}   Nginx Proxy Manager 管理菜单${NC}"
    echo -e "${BLUE}=============================================${NC}"
     is_installed; then
      ST=$(docker inspect -f '{{.State.Status}}' npm >/dev/null)
      echo -e "  ${GREEN}●${NC} 容器: $ST"
    fi
    echo -e "${BLUE}---------------------------------------------${NC}"
    echo "  ) 安装"
    echo "  ) 启动"
    echo "  ) 停止"
    echo "  ) 重启"
    echo "  ) 状态详情"
    echo "  ) 升级 (拉最新镜像)"
    echo "  ) 备份数据"
    echo "  ) 查看日志 (最近30行)"
    echo "  ) 卸载"
    echo "  ) 退出"
    read -p "选择 [-]: " C
    case "$C" 
      ) do_install ;;
      ) cd "$NPM_DIR" && docker compose start >/dev/null && info "已启动" ;;
      ) cd "$NPM_DIR" && docker compose stop >/dev/null && info "已停止" ;;
      ) cd "$NPM_DIR" && docker compose restart >/dev/null && info "已重启" ;;
      ) do_status ;;
      ) do_update ;;
      ) do_backup ;;
      ) docker logs npm --tail  >/dev/null | less -R || warn "未运行" ;;
      ) do_uninstall ;;
      ) exit  ;;
      *) warn "无效选择" ;;
    esac
    read -p "按回车返回菜单..." _
  done
}

# ============================ 主入口 ============================
case "$" 
  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  ;;
  *) err "未知命令: $"; exit  ;;
esac