fix(deploy): install.sh 不再依赖 sudo

用户实测报错:deploy/install.sh: line 173: sudo: command not found

本脚本已经强制要求 root 权限,用 sudo 降权到服务用户本就多余,
且部分服务器(尤其精简系统/容器)根本没有安装 sudo。

- 新增 as_service_user():runuser → setpriv → su 依次回退,
  并在三者都不可用时给出明确错误
- 替换全部 sudo -u 调用(npm ci / npm install / npm run build / 迁移)
- 新增 http_get() / http_ok():curl → wget → node fetch 依次回退,
  健康检查不再强依赖 curl
- 启动前检查 systemctl(本部署依赖 systemd),缺失时给出可操作提示
- 修正 install.sh / uninstall.sh 中所有「用 sudo 执行」的提示文案

实测(桩替换 runuser 验证分派与参数传递):
  有 runuser:runuser -u opendance -- npm ci --omit=dev --loglevel=error
  仅 su:正确回退到 su
  HTTP:curl 路径与 node fetch 回退路径返回一致
This commit is contained in:
2026-09-15 00:27:27 +08:00
parent 7cd607ecde
commit 1ffe3059af
2 changed files with 54 additions and 13 deletions
+49 -9
View File
@@ -37,8 +37,46 @@ log() { printf '\033[1;34m[install]\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m[warn]\033[0m %s\n' "$*"; } warn() { printf '\033[1;33m[warn]\033[0m %s\n' "$*"; }
die() { printf '\033[1;31m[error]\033[0m %s\n' "$*" >&2; exit 1; } die() { printf '\033[1;31m[error]\033[0m %s\n' "$*" >&2; exit 1; }
# 以服务用户身份执行命令。
# 本脚本已要求 root,因此**不使用 sudo**(部分服务器根本没装 sudo,
# 且 root 降权本来也不需要它)。优先 runuser,其次 setpriv,最后 su。
as_service_user() {
if command -v runuser >/dev/null 2>&1; then
runuser -u "$SERVICE_USER" -- "$@"
elif command -v setpriv >/dev/null 2>&1; then
setpriv --reuid="$SERVICE_USER" --regid="$SERVICE_USER" --init-groups -- "$@"
elif command -v su >/dev/null 2>&1; then
su -s /bin/bash "$SERVICE_USER" -c "$(printf '%q ' "$@")"
else
die "无法以 $SERVICE_USER 身份执行命令:runuser / setpriv / su 均不可用"
fi
}
# HTTP GET,输出响应体。curl → wget → node(node 必有,且 22+ 自带 fetch)
http_get() {
if command -v curl >/dev/null 2>&1; then
curl -fsS -m 5 "$1"
elif command -v wget >/dev/null 2>&1; then
wget -q -O - -T 5 "$1"
else
"$NODE_BIN" -e 'fetch(process.argv[1]).then(r=>r.text()).then(t=>process.stdout.write(t)).catch(()=>process.exit(1))' "$1"
fi
}
# 健康检查(只看成功与否)
http_ok() {
if command -v curl >/dev/null 2>&1; then
curl -fsS -m 3 "$1" >/dev/null 2>&1
elif command -v wget >/dev/null 2>&1; then
wget -q -O /dev/null -T 3 "$1"
else
"$NODE_BIN" -e 'fetch(process.argv[1]).then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))' "$1"
fi
}
# ---------- 0. 前置检查 ---------- # ---------- 0. 前置检查 ----------
[ "$(id -u)" -eq 0 ] || die "需要 root 权限(安装 systemd 服务与 nginx 站点):sudo bash deploy/install.sh" [ "$(id -u)" -eq 0 ] || die "需要 root 权限(安装 systemd 服务与 nginx 站点)。
请以 root 身份执行,例如: su -c 'bash deploy/install.sh' 或 sudo bash deploy/install.sh"
[ -f deploy/.env ] || die "缺少 deploy/.env,请先: [ -f deploy/.env ] || die "缺少 deploy/.env,请先:
cp deploy/.env.example deploy/.env && vi deploy/.env" cp deploy/.env.example deploy/.env && vi deploy/.env"
@@ -54,6 +92,8 @@ API_PORT="${PORT:-3000}"
[ -n "${DB_PASS:-}" ] || die "deploy/.env 中 DB_PASS 未设置" [ -n "${DB_PASS:-}" ] || die "deploy/.env 中 DB_PASS 未设置"
[ -n "${DB_NAME:-}" ] || die "deploy/.env 中 DB_NAME 未设置" [ -n "${DB_NAME:-}" ] || die "deploy/.env 中 DB_NAME 未设置"
command -v systemctl >/dev/null 2>&1 || die "未找到 systemctl。本部署依赖 systemd 托管 Node 进程;
若目标系统没有 systemd(如容器/WSL1),请改用 SKIP_NGINX=1 手动以进程方式运行,或换一台服务器。"
command -v node >/dev/null 2>&1 || die "未找到 node。请安装 Node.js 22 或更高版本。" command -v node >/dev/null 2>&1 || die "未找到 node。请安装 Node.js 22 或更高版本。"
NODE_BIN="$(command -v node)" NODE_BIN="$(command -v node)"
NODE_MAJOR="$(node -p 'process.versions.node.split(".")[0]')" NODE_MAJOR="$(node -p 'process.versions.node.split(".")[0]')"
@@ -83,7 +123,7 @@ if [ "$SRC_REAL" = "$APP_REAL" ]; then
die "安装目录与源目录相同($APP_REAL)。 die "安装目录与源目录相同($APP_REAL)。
本脚本会先清空安装目录下的 web-server/ admin/ 再复制,直接在这里执行会删掉源文件。 本脚本会先清空安装目录下的 web-server/ admin/ 再复制,直接在这里执行会删掉源文件。
请把发布包解压到别处,或用 APP_DIR=... 指定另一个安装目录: 请把发布包解压到别处,或用 APP_DIR=... 指定另一个安装目录:
sudo APP_DIR=/opt/opendance bash $0" APP_DIR=/opt/opendance bash $0 (root 身份下执行)"
fi fi
case "$APP_REAL/" in case "$APP_REAL/" in
"$SRC_REAL"/*) "$SRC_REAL"/*)
@@ -170,15 +210,15 @@ cd "$APP_DIR/web-server"
log "安装运行期依赖(仅生产依赖,可能需要 1-3 分钟…)" log "安装运行期依赖(仅生产依赖,可能需要 1-3 分钟…)"
if [ -f package-lock.json ]; then if [ -f package-lock.json ]; then
sudo -u "$SERVICE_USER" npm ci --omit=dev --no-audit --no-fund --loglevel=error as_service_user npm ci --omit=dev --no-audit --no-fund --loglevel=error
else else
sudo -u "$SERVICE_USER" npm install --omit=dev --no-audit --no-fund --loglevel=error as_service_user npm install --omit=dev --no-audit --no-fund --loglevel=error
fi fi
if [ ! -f dist/main.js ]; then if [ ! -f dist/main.js ]; then
warn "未找到构建产物 dist/main.js,改为在服务器上构建(需要 devDependencies)" warn "未找到构建产物 dist/main.js,改为在服务器上构建(需要 devDependencies)"
sudo -u "$SERVICE_USER" npm install --no-audit --no-fund as_service_user npm install --no-audit --no-fund
sudo -u "$SERVICE_USER" npm run build as_service_user npm run build
fi fi
[ -f dist/main.js ] || die "构建产物缺失:$APP_DIR/web-server/dist/main.js" [ -f dist/main.js ] || die "构建产物缺失:$APP_DIR/web-server/dist/main.js"
[ -f "$APP_DIR/admin/index.html" ] || die "管理端构建产物缺失:$APP_DIR/admin/index.html" [ -f "$APP_DIR/admin/index.html" ] || die "管理端构建产物缺失:$APP_DIR/admin/index.html"
@@ -187,7 +227,7 @@ fi
log "执行 TypeORM 迁移(不使用 DB_SYNCHRONIZE)" log "执行 TypeORM 迁移(不使用 DB_SYNCHRONIZE)"
set -a; . "$APP_DIR/deploy/.env"; set +a set -a; . "$APP_DIR/deploy/.env"; set +a
cd "$APP_DIR/web-server" cd "$APP_DIR/web-server"
sudo -u "$SERVICE_USER" env \ as_service_user env \
"PATH=$(dirname "$NODE_BIN"):/usr/local/bin:/usr/bin:/bin" \ "PATH=$(dirname "$NODE_BIN"):/usr/local/bin:/usr/bin:/bin" \
"DB_HOST=${DB_HOST}" "DB_PORT=${DB_PORT:-3306}" "DB_USER=${DB_USER}" \ "DB_HOST=${DB_HOST}" "DB_PORT=${DB_PORT:-3306}" "DB_USER=${DB_USER}" \
"DB_PASS=${DB_PASS}" "DB_NAME=${DB_NAME}" "DB_LOGGING=${DB_LOGGING:-false}" \ "DB_PASS=${DB_PASS}" "DB_NAME=${DB_NAME}" "DB_LOGGING=${DB_LOGGING:-false}" \
@@ -210,7 +250,7 @@ systemctl restart opendance-api
log "等待 API 就绪…" log "等待 API 就绪…"
READY=0 READY=0
for _ in $(seq 1 30); do for _ in $(seq 1 30); do
if curl -fsS -m 3 "http://${API_HOST}:${API_PORT}/health" >/dev/null 2>&1; then READY=1; break; fi if http_ok "http://${API_HOST}:${API_PORT}/health"; then READY=1; break; fi
sleep 1 sleep 1
done done
if [ "$READY" -ne 1 ]; then if [ "$READY" -ne 1 ]; then
@@ -218,7 +258,7 @@ if [ "$READY" -ne 1 ]; then
journalctl -u opendance-api -n 40 --no-pager || true journalctl -u opendance-api -n 40 --no-pager || true
die "API 启动失败" die "API 启动失败"
fi fi
log "API 已就绪:$(curl -fsS -m 5 "http://${API_HOST}:${API_PORT}/health" | head -c 200)" log "API 已就绪:$(http_get "http://${API_HOST}:${API_PORT}/health" | head -c 200)"
# ---------- 5. nginx ---------- # ---------- 5. nginx ----------
if [ "${SKIP_NGINX:-0}" = "1" ]; then if [ "${SKIP_NGINX:-0}" = "1" ]; then
+5 -4
View File
@@ -6,8 +6,8 @@
# 数据库从来不由本脚本管理,永不删除。 # 数据库从来不由本脚本管理,永不删除。
# #
# 用法: # 用法:
# sudo bash deploy/uninstall.sh # 停服务、禁用站点,保留数据 # bash deploy/uninstall.sh # 停服务、禁用站点,保留数据(需 root)
# sudo bash deploy/uninstall.sh --purge # 同时删除程序目录与上传文件 # bash deploy/uninstall.sh --purge # 同时删除程序目录与上传文件
# ============================================================ # ============================================================
set -euo pipefail set -euo pipefail
@@ -21,7 +21,8 @@ log() { printf '\033[1;34m[uninstall]\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m[warn]\033[0m %s\n' "$*"; } warn() { printf '\033[1;33m[warn]\033[0m %s\n' "$*"; }
die() { printf '\033[1;31m[error]\033[0m %s\n' "$*" >&2; exit 1; } die() { printf '\033[1;31m[error]\033[0m %s\n' "$*" >&2; exit 1; }
[ "$(id -u)" -eq 0 ] || die "需要 root 权限:sudo bash deploy/uninstall.sh" [ "$(id -u)" -eq 0 ] || die "需要 root 权限。请以 root 身份执行,例如:
su -c 'bash deploy/uninstall.sh' 或 sudo bash deploy/uninstall.sh"
log "停止并禁用 opendance-api 服务" log "停止并禁用 opendance-api 服务"
systemctl stop opendance-api 2>/dev/null || true systemctl stop opendance-api 2>/dev/null || true
@@ -47,6 +48,6 @@ else
log "完成。已保留:" log "完成。已保留:"
echo " 程序目录:$APP_DIR" echo " 程序目录:$APP_DIR"
echo " 上传目录:$STORAGE_DIR" echo " 上传目录:$STORAGE_DIR"
echo " 如需一并删除:sudo bash deploy/uninstall.sh --purge" echo " 如需一并删除(root 身份):bash deploy/uninstall.sh --purge"
fi fi
echo " 数据库:始终由部署方自行管理,本脚本不做任何改动" echo " 数据库:始终由部署方自行管理,本脚本不做任何改动"