#!/bin/bash # chmod +x 23-2-Sub2API.sh && ./23-2-Sub2API.sh # curl -sS -O https://gitea.tohub.top/Share/vps/raw/branch/main/install/23-2-Sub2API.sh && chmod +x 23-2-Sub2API.sh && ./23-2-Sub2API.sh set -euo pipefail port80=8231 # 安装目录与更新脚本路径 install_dir=/root/data/docker_data/Sub2API-2 update_script="$install_dir/auto-update.sh" env_file="$install_dir/.env" update_log="$install_dir/auto-update.log" # 检测 compose 命令,结果存入全局数组 compose_cmd;检测失败返回 1 detect_compose() { if docker compose version >/dev/null 2>&1; then compose_cmd=(docker compose) elif command -v docker-compose >/dev/null 2>&1; then compose_cmd=(docker-compose) else return 1 fi return 0 } # 卸载当前 Sub2API-2 实例。Docker 镜像可能被其他实例共用,因此不删除镜像。 uninstall_sub2api() { echo "即将卸载 Sub2API-2:" echo " - 停止并删除容器及 Compose 网络" echo " - 删除自动更新定时任务" echo " - 可选删除 ufw 端口规则 $port80" echo " - 永久删除 $install_dir(包括数据库、配置和日志)" read -r -p "此操作不可恢复。如确需卸载,请输入 UNINSTALL 确认:" uninstall_confirm if [ "$uninstall_confirm" != "UNINSTALL" ]; then echo "未确认卸载,已退出。原有安装未修改。" exit 0 fi if ! command -v docker >/dev/null 2>&1; then echo "未检测到 Docker,无法确认并停止现有容器,已取消卸载。" >&2 echo "请先恢复 Docker,或确认容器已不存在后手动清理 $install_dir。" >&2 exit 1 fi if ! detect_compose; then echo "未检测到 docker compose 或 docker-compose,无法安全停止现有容器,已取消卸载。" >&2 exit 1 fi if [ -f "$install_dir/docker-compose.yml" ]; then cd "$install_dir" || { echo "无法进入安装目录 $install_dir" >&2; exit 1; } "${compose_cmd[@]}" down --remove-orphans fi if command -v crontab >/dev/null 2>&1; then (crontab -l 2>/dev/null | grep -v -F -- "$update_script" || true) | crontab - echo "已删除自动更新定时任务。" fi if command -v ufw >/dev/null 2>&1; then read -r -p "是否同时删除 ufw 端口规则 $port80?[y/N]:" remove_ufw_rule if [ "$remove_ufw_rule" = "y" ] || [ "$remove_ufw_rule" = "Y" ]; then if ufw --force delete allow "$port80"; then echo "已删除 ufw 端口规则 $port80。" else echo "未找到或无法删除 ufw 端口规则 $port80,请手动检查。" >&2 fi else echo "已保留 ufw 端口规则 $port80。" fi fi cd / || { echo "无法切换到根目录,取消删除安装目录。" >&2; exit 1; } rm -rf -- "$install_dir" echo "Sub2API-2 已卸载,Docker 镜像已保留。" exit 0 } # 交互:已有安装时默认只更新,避免覆盖数据库账号、密码和连接配置 existing_install=0 if [ -f "$install_dir/docker-compose.yml" ]; then existing_install=1 fi echo "请选择操作:" if [ "$existing_install" -eq 1 ]; then echo " 1) 已安装,仅更新 Sub2API app 镜像(默认,安全,不覆盖配置/数据库)" echo " 2) 强制重装 Sub2API(重写 docker-compose.yml,保留 .env 与数据目录)" echo " 3) 卸载 Sub2API-2(删除容器、配置及全部数据)" read -r -p "请输入选项 [1/2/3](默认 1):" action_choice else echo " 1) 安装 Sub2API(默认)" echo " 2) 不安装,退出" read -r -p "请输入选项 [1/2](默认 1):" action_choice fi action_choice="${action_choice:-1}" # 严格校验输入,防止误输入落入重装或卸载流程 if [ "$existing_install" -eq 1 ]; then if [ "$action_choice" != "1" ] && [ "$action_choice" != "2" ] && [ "$action_choice" != "3" ]; then echo "无效选项:$action_choice,已退出。未做任何修改。" >&2 exit 1 fi elif [ "$action_choice" != "1" ] && [ "$action_choice" != "2" ]; then echo "无效选项:$action_choice,已退出。未做任何修改。" >&2 exit 1 fi if [ "$existing_install" -eq 0 ] && [ "$action_choice" = "2" ]; then echo "已选择不安装,退出。" exit 0 fi if [ "$existing_install" -eq 1 ] && [ "$action_choice" = "3" ]; then uninstall_sub2api fi # 安装、更新和重装都要求 Docker 与 Compose 正常可用;卸载在函数内单独检查。 if ! command -v docker >/dev/null 2>&1; then echo "未检测到 docker,请先安装 Docker 后再运行本脚本。" >&2 exit 1 fi if ! detect_compose; then echo "未检测到 docker compose 或 docker-compose,请先安装 Docker Compose。" >&2 exit 1 fi if [ "$existing_install" -eq 1 ] && [ "$action_choice" = "1" ]; then echo "已选择:立即执行 Docker 更新。" if [ -x "$update_script" ]; then # 优先用已生成的 auto-update.sh(同样只更新 app,不动 db / redis) "$update_script" else cd "$install_dir" || { echo "无法进入安装目录 $install_dir" >&2; exit 1; } # 回退方案:直接用 compose 只更新 app 服务(Sub2API 镜像),不动 db / redis "${compose_cmd[@]}" pull app "${compose_cmd[@]}" up -d app docker image prune -f fi echo "Docker 更新完成。" exit 0 fi if [ "$existing_install" -eq 1 ] && [ "$action_choice" = "2" ]; then echo "检测到已有 Sub2API 安装:$install_dir/docker-compose.yml" echo "强制重装会重写 docker-compose.yml(.env 与 data/ 数据目录会保留,原管理员账号密码继续有效)。" read -r -p "如确需重装,请输入 REINSTALL 确认:" reinstall_confirm if [ "$reinstall_confirm" != "REINSTALL" ]; then echo "未确认强制重装,已退出。原有配置未修改。" exit 0 fi fi # 1、更新包(更新失败不阻断后续安装;非 Debian 系无 apt 则跳过) if command -v apt >/dev/null 2>&1; then export DEBIAN_FRONTEND=noninteractive apt update -y || true apt upgrade -y || true #更新一下包 else echo "未检测到 apt,跳过系统包更新。" fi # 2、创建安装目录 mkdir -p "$install_dir" cd "$install_dir" || { echo "无法进入安装目录 $install_dir" >&2; exit 1; } # 3、生成 .env(仅首次生成,重装/更新不覆盖,避免密钥和数据库密码变化) gen_secret() { openssl rand -hex 32 2>/dev/null || head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n' } # Compose 对 .env 中未加引号和双引号的值执行变量插值。 # 不含单引号的值用单引号包裹,按字面处理($、#、空格、\ 均不被改写); # 含单引号的值改用双引号包裹,并转义 \、" 和 $。 compose_env_literal() { local value=$1 if [[ $value != *"'"* ]]; then printf "'%s'" "$value" return fi local escaped=${value//\\/\\\\} escaped=${escaped//\"/\\\"} escaped=${escaped//\$/\\\$} printf '"%s"' "$escaped" } if [ ! -f "$env_file" ]; then # 兼容旧版安装:尝试从旧 docker-compose.yml 中提取已在用的数据库密码, # 避免生成新密码后与已有 postgres 数据目录不一致导致连不上库 # 注意:这里回读的是旧版明文 docker-compose.yml(历史格式),与本脚本新生成的 # 引号化 .env 表示法不同。切勿改成从 .env 回读——.env 里的值可能带引号/转义, # 下面的 [^ #]* 会在引号或 # 处截断,取到错误密码。 old_pg_password="" old_pg_user="" if [ -f docker-compose.yml ]; then # grep -v '\$' 排除新版 compose 中的 ${VAR} 占位符,只取旧版明文值 old_pg_password=$(sed -n 's/.*POSTGRES_PASSWORD=\([^ #]*\).*/\1/p' docker-compose.yml | grep -v '\$' | head -n1 || true) old_pg_user=$(sed -n 's/.*POSTGRES_USER=\([^ #]*\).*/\1/p' docker-compose.yml | grep -v '\$' | head -n1 || true) fi pg_password="${old_pg_password:-$(gen_secret)}" pg_user="${old_pg_user:-postgres}" # 交互:设置管理员账号密码(仅数据库首次初始化时生效; # 如果数据库已有数据,登录仍用数据库里已存在的账号密码) default_admin_email="admin@sub2api-2.local" default_admin_password=$(gen_secret | cut -c1-16) echo "配置管理员账户(仅数据库首次初始化时创建):" read -r -p "管理员邮箱(默认 $default_admin_email):" admin_email admin_email="${admin_email:-$default_admin_email}" read -r -p "管理员密码(回车使用随机密码 $default_admin_password):" admin_password admin_password="${admin_password:-$default_admin_password}" # 单引号值无法在旧版 docker-compose (v1) 的 .env 解析中可靠转义,直接拒绝 while [[ $admin_email == *"'"* ]] || [[ $admin_password == *"'"* ]]; do echo "邮箱或密码不能包含单引号('),请重新输入。" read -r -p "管理员邮箱(默认 $default_admin_email):" admin_email admin_email="${admin_email:-$default_admin_email}" read -r -p "管理员密码(回车使用随机密码 $default_admin_password):" admin_password admin_password="${admin_password:-$default_admin_password}" done pg_user_env=$(compose_env_literal "$pg_user") pg_password_env=$(compose_env_literal "$pg_password") pg_db_env=$(compose_env_literal "sub2api") jwt_secret_env=$(compose_env_literal "$(gen_secret)") totp_key_env=$(compose_env_literal "$(gen_secret)") admin_email_env=$(compose_env_literal "$admin_email") admin_password_env=$(compose_env_literal "$admin_password") tz_env=$(compose_env_literal "Asia/Shanghai") cat < "$env_file" # Sub2API 环境配置(由安装脚本生成,请勿删除,否则需重新配置) # 数据库 POSTGRES_USER=$pg_user_env POSTGRES_PASSWORD=$pg_password_env POSTGRES_DB=$pg_db_env # 固定密钥:保证容器重建/更新后登录态和 2FA 不失效 JWT_SECRET=$jwt_secret_env TOTP_ENCRYPTION_KEY=$totp_key_env # 管理员账户(仅首次启动时创建,之后修改无效) ADMIN_EMAIL=$admin_email_env ADMIN_PASSWORD=$admin_password_env TZ=$tz_env EOF chmod 600 "$env_file" echo "已生成 $env_file(包含数据库密码、密钥和管理员初始密码,请妥善保管)。" else echo "检测到已有 $env_file,沿用原有管理员账号密码与数据库配置,不再询问。" fi # 4、填写docker-compose配置 # AUTO_SETUP=true + 环境变量传入数据库/Redis 配置,容器重建后无需再走网页安装向导 cat < docker-compose.yml services: db: image: postgres:15-alpine container_name: sub2api-2-db restart: unless-stopped volumes: - ./data/postgres:/var/lib/postgresql/data environment: - POSTGRES_USER=\${POSTGRES_USER:-postgres} - POSTGRES_PASSWORD=\${POSTGRES_PASSWORD:?POSTGRES_PASSWORD 未配置,请检查 .env} - POSTGRES_DB=\${POSTGRES_DB:-sub2api} - TZ=\${TZ:-Asia/Shanghai} healthcheck: test: ["CMD-SHELL", "pg_isready -U \${POSTGRES_USER:-postgres} -d \${POSTGRES_DB:-sub2api}"] interval: 10s timeout: 5s retries: 5 redis: image: redis:7-alpine container_name: sub2api-2-redis restart: unless-stopped volumes: - ./data/redis:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 5s retries: 5 app: image: weishaw/sub2api:latest container_name: sub2api-2-app restart: unless-stopped volumes: - ./data/app:/app/data # 持久化 app 配置/数据 ports: - "$port80:8080" # 左边的端口可以更换,右边不要动! depends_on: db: condition: service_healthy redis: condition: service_healthy environment: # 自动初始化:跳过网页安装向导,配置全部来自环境变量(.env) - AUTO_SETUP=true - SERVER_HOST=0.0.0.0 - SERVER_PORT=8080 # 必须与 ports 右边的容器端口一致 - SERVER_MODE=release # 主机用容器服务名 db / redis,不要写宿主机 IP! - DATABASE_HOST=db - DATABASE_PORT=5432 - DATABASE_USER=\${POSTGRES_USER:-postgres} - DATABASE_PASSWORD=\${POSTGRES_PASSWORD} - DATABASE_DBNAME=\${POSTGRES_DB:-sub2api} - DATABASE_SSLMODE=disable - REDIS_HOST=redis - REDIS_PORT=6379 # 固定密钥(来自 .env):更新/重启后登录态和 2FA 不失效 - JWT_SECRET=\${JWT_SECRET} - TOTP_ENCRYPTION_KEY=\${TOTP_ENCRYPTION_KEY} # 管理员账户仅首次启动时创建 - ADMIN_EMAIL=\${ADMIN_EMAIL:-admin@sub2api-2.local} - ADMIN_PASSWORD=\${ADMIN_PASSWORD:-} - TZ=\${TZ:-Asia/Shanghai} EOF # 5、安装(docker 与 compose 已在脚本开头检查过) "${compose_cmd[@]}" up -d # 6、打开防火墙的端口 if command -v ufw >/dev/null 2>&1; then ufw allow "$port80" ufw status else echo "未检测到 ufw,跳过防火墙端口放行。" fi # 7、配置每日自动更新 cat <<'EOF' > "$update_script" #!/bin/bash set -euo pipefail install_dir=/root/data/docker_data/Sub2API-2 cd "$install_dir" # 进入 docker-compose 所在的文件夹 if docker compose version >/dev/null 2>&1; then compose_cmd=(docker compose) elif command -v docker-compose >/dev/null 2>&1; then compose_cmd=(docker-compose) else echo "未检测到 docker compose 或 docker-compose,无法自动更新。" >&2 exit 1 fi # 只更新 app 服务(Sub2API 镜像),不动 db / redis "${compose_cmd[@]}" pull app "${compose_cmd[@]}" up -d app # 清理旧镜像,释放磁盘空间 docker image prune -f # 修剪日志避免无限增长(只保留最近 1000 行) # 用 cat 覆盖写回而非 mv,保留原 inode,避免 cron 持有的 append 文件描述符失效 log_file="$install_dir/auto-update.log" if [ -f "$log_file" ]; then tail -n 1000 "$log_file" > "$log_file.tmp" && cat "$log_file.tmp" > "$log_file" && rm -f "$log_file.tmp" fi EOF chmod +x "$update_script" # 写入每日 04:10 自动更新任务 cron_line="10 4 * * * $update_script >> $update_log 2>&1" if command -v crontab >/dev/null 2>&1; then # 使用脚本路径去重,避免重复写入 Sub2API 自动更新任务 ( crontab -l 2>/dev/null | grep -v -F -- "$update_script" || true ; echo "$cron_line" ) | crontab - echo "已配置定时自动更新任务:每天 04:10 自动更新 Sub2API app 镜像(不更新 db / redis)。" else echo "未检测到 crontab,跳过定时更新任务配置。可手动执行 $update_script 更新。" fi # 打印访问链接 echo "------------------------" echo "访问链接:" echo "http://服务器IP:$port80" echo "管理员账户(首次安装时自动创建,以下为容器实际收到的凭证):" if ! docker inspect sub2api-2-app --format '{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null \ | grep -E '^(ADMIN_EMAIL|ADMIN_PASSWORD)='; then echo "(容器尚未就绪,凭证可稍后执行 docker inspect sub2api-2-app 查看;如查看 $env_file,注意值外层的引号不属于密码本身)" fi echo "------------------------" echo "已启用 AUTO_SETUP 自动初始化:更新/重建容器后无需重新走网页安装向导。" echo "已开启定时自动更新:每天 04:10 仅拉取最新 app 镜像并重启(db / redis 不动)。" echo "手动更新可执行:$update_script" echo "------------------------"