在 Linux 服务器上部署 Node.js,常见路径是:先手动解压一个官方 tar 包,后来版本多了改用 NVM 管理,最后还要让 systemd 服务稳定地跑在某个版本上。本文按这四个阶段整理完整流程:
- 手动安装 Node.js
- 使用 NVM 管理 Node.js 版本
- Node.js 版本升级后的迁移工作
- Node.js 的 systemd 文件 PATH 设置规范
1. 手动安装 Node.js
最直接的安装方式是从 nodejs.org 下载官方编译好的 tar 包,解压到 /usr/local:
cd /usr/local
curl -fLO https://nodejs.org/dist/v24.14.0/node-v24.14.0-linux-x64.tar.xz
tar -xJf node-v24.14.0-linux-x64.tar.xz
然后把 bin 目录加入 PATH,例如在 /etc/profile 或 /etc/zshenv 中:
export PATH="/usr/local/node-v24.14.0-linux-x64/bin:$PATH"
确认安装:
command -v node
command -v npm
node --version
npm --version
这种方式简单透明,适合只需要单一版本的服务器。但升级时会出现一个典型问题:新的 node 和 npm 已经生效,原来通过 npm install -g 安装的命令却“消失”了。
这不是软件被删除,而是 npm 的全局安装目录跟随 Node.js 的安装前缀。Linux 下全局包通常位于:
${prefix}/lib/node_modules
对应的命令入口位于:
${prefix}/bin
从 node-v24.14.0-linux-x64 切换到 node-v24.15.0-linux-x64 后,全局包目录也跟着切换。旧软件还在旧目录,只是新 PATH 找不到它们。
也不建议把所有旧版本的 bin 都追加到 PATH。每个目录里都有自己的 node、npm 和全局命令,目录顺序稍有变化就可能混用不同版本,问题会变得更加隐蔽。
可以用下面的命令确认当前状态:
npm config get prefix
npm root --global
npm list --global --depth=0
type -a node npm
echo "$PATH"
当服务器需要维护多个版本、或者需要频繁升级时,就该交给 NVM 管理了。
2. 使用 NVM 管理 Node.js 版本
安装 NVM(从 GitHub API 读取最新版本)
NVM 官方安装脚本需要指定版本号。为了避免写死版本,可以先从 GitHub API 查询 nvm-sh/nvm 的最新 release:
nvm_version="$(curl -fsSL https://api.github.com/repos/nvm-sh/nvm/releases/latest \
| grep -m1 '"tag_name"' \
| cut -d '"' -f 4)"
echo "$nvm_version"
curl -o- "https://raw.githubusercontent.com/nvm-sh/nvm/${nvm_version}/install.sh" | bash
重新登录 SSH,或者手工加载:
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
确认安装:
command -v nvm
nvm --version
nvm 是 Shell 函数,不是普通二进制文件,所以应使用 command -v nvm 检查,不要依赖 which nvm。
配置 default-packages
NVM 支持 $NVM_DIR/default-packages,文件中每行写一个包,之后通过 NVM 安装新的 Node.js 版本时会自动安装这些包。在安装第一个版本前就配置好,可以省去后续手动安装:
cat > "$NVM_DIR/default-packages" <<'EOF'
openclaw
@openai/codex
pnpm
prettier
playwright
agent-browser
@zed-industries/codex-acp
EOF
业务项目依赖不应放在这里,而应继续由项目自己的 package.json 管理。
安装并设置默认版本
不必写死完整版本号,可以用 nvm version-remote 查询某个大版本的最新 release:
new="$(nvm version-remote 24)"
nvm install "$new"
nvm alias default "$new"
nvm use default
node --version
npm --version
nvm current
关于 nvm alias default:首次安装时 NVM 会自动把第一个安装的版本设为 default,此时这行看似多余。但之后安装新版本并不会自动更新 default 别名,所以显式设置一次更稳妥,后续升级时也必须执行它才能让新登录的 Shell 默认使用新版本。
NVM 的职责划分建议:
| 场景 | 管理方式 |
|---|---|
SSH 登录后使用 node、npm 等 |
NVM 的默认版本 |
| systemd 长期运行的 Node.js 服务 | NVM 版本目录中的绝对路径 |
| 项目依赖 | 项目内 package.json、lock 文件和 node_modules |
| 常用全局 CLI | NVM 的 default-packages 和包迁移命令 |
核心原则是:交互式 Shell 可以灵活切换,后台服务必须固定版本。
3. Node.js 版本升级后的迁移工作
第一次从手动安装的nodejs迁移到 NVM
第一次使用 NVM 时,原来的 Node.js 位于 /usr/local/node-*,不属于 NVM 管理范围,不能直接让 NVM 迁移。需要先记录旧版本中的全局包。
为了避免旧 npm 的 shebang 又解析到当前 PATH 中的新 node,可以显式指定旧 Node.js、旧 npm CLI 和旧 prefix:
old_prefix=/usr/local/node-v24.14.0-linux-x64
"$old_prefix/bin/node" \
"$old_prefix/lib/node_modules/npm/bin/npm-cli.js" \
--prefix="$old_prefix" \
list --global --depth=0
然后在 NVM 管理的新版本中重新安装需要的工具:
nvm use 24.15.0
npm install --global \
openclaw \
@openai/codex \
pnpm \
prettier \
playwright \
agent-browser \
@zed-industries/codex-acp
不要直接复制旧 node_modules。有些包包含原生模块或按平台下载的二进制文件,重新安装更可靠。
后续版本升级:用 NVM 重装全局包
后续版本都由 NVM 管理后,可以在安装新版本时直接重装旧版本中的全局包:
old="$(node -v)"
new="$(nvm version-remote 24)"
nvm install "$new" --reinstall-packages-from="$old"
nvm use "$new"
nvm alias default "$new"
node --version
npm list --global --depth=0
openclaw --version
node -v 输出带 v 前缀(如 v24.15.0),--reinstall-packages-from 和 nvm uninstall 都能识别这种格式,无需手工去掉前缀。
安装新版本不会自动更新 default 别名,必须显式执行 nvm alias default,否则新登录的 Shell 仍会使用旧版本。
NVM 官方支持 --reinstall-packages-from 和 nvm reinstall-packages,但这里的“迁移”实际是按清单重新安装,不是直接复制旧目录。
升级后不要立刻卸载旧版本
确认新版本工作正常之前,保留旧版本以便回滚。观察一段时间后再清理:
nvm uninstall "$old"
不要给 NVM 配置自定义 npm prefix
单独使用手动安装的 Node.js 时,可以把 npm prefix 配置成稳定目录,例如 /usr/local/npm-global。但采用 NVM 后不要这样做:
npm config set prefix /usr/local/npm-global
NVM 官方明确说明,它与 npm 的 prefix 配置不兼容。使用 NVM 时应让全局包保留在:
$NVM_DIR/versions/node/<version>/lib/node_modules
可以检查并清理以前遗留的 prefix 配置:
npm config get userconfig
grep -n '^[[:space:]]*prefix[[:space:]]*=' ~/.npmrc
确认无误后再编辑 ~/.npmrc,不要直接删除其中的 registry、proxy 等其他配置。
4. Node.js 的 systemd 文件 PATH 设置规范
不要依赖登录 Shell
systemd 启动服务时不会自动读取 .bashrc,因此下面这种写法不够稳妥:
ExecStart=/bin/bash -lc 'nvm use default && openclaw gateway'
它把服务启动绑定到 Shell 初始化脚本和 NVM alias。某次修改 .bashrc 或切换 default 就可能在下次重启服务时引入未经验证的新版本。
规范一:ExecStart 使用绝对路径
固定 Node.js 和应用入口的绝对路径。以 root 的 systemd user service 为例:
[Unit]
Description=OpenClaw Gateway
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=/root
ExecStart=/root/.nvm/versions/node/v24.15.0/bin/node /root/.nvm/versions/node/v24.15.0/lib/node_modules/openclaw/dist/index.js gateway --port 18789
Environment="PATH=/root/.nvm/versions/node/v24.15.0/bin:/usr/local/bin:/usr/bin:/bin"
EnvironmentFile=-/root/.config/systemd/user/openclaw-gateway.env
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=default.target
规范二:Environment 中的 PATH 与 ExecStart 保持同一版本
这里的 Environment=PATH 不是为了让 systemd 找到 ExecStart,因为 ExecStart 已经是绝对路径;它用于确保服务创建的子进程也使用同一个 Node.js 版本。两者必须指向同一个版本目录,否则主进程和子进程可能混用不同版本。
保存为:
/root/.config/systemd/user/openclaw-gateway.service
然后加载并启动:
systemctl --user daemon-reload
systemctl --user enable --now openclaw-gateway.service
systemctl --user status openclaw-gateway.service
journalctl --user -u openclaw-gateway.service -f
如果用户退出登录后服务也需要继续运行,应当启用 linger:
loginctl enable-linger root
loginctl show-user root -p Linger
如果服务运行在普通用户下,就把示例中的 /root 替换为该用户的 home,并使用对应用户执行 systemctl --user。
升级时同步修改 systemd unit
Node.js 升级并验证通过后,修改 unit 中的两个位置:
/root/.nvm/versions/node/v24.15.0/bin/node
/root/.nvm/versions/node/v24.15.0/lib/node_modules/openclaw/...
替换为新版本路径,同时更新 Environment=PATH,再重启:
systemctl --user daemon-reload
systemctl --user restart openclaw-gateway.service
systemctl --user status openclaw-gateway.service
journalctl --user -u openclaw-gateway.service -n 100 --no-pager
回滚同样直接:把 ExecStart 和 Environment=PATH 改回旧版本,daemon-reload 后重启服务即可;SSH 侧如需回退则执行 nvm alias default "$old"。这也是不让 systemd 直接使用 nvm alias default 的原因:服务版本切换应当是一次显式、可审查、可回退的部署操作。
规范三:项目型服务使用本地依赖
如果 systemd 运行的是自己的 Node.js 项目,而不是 openclaw 这类全局 CLI,最好不要依赖全局 npm 包。部署时在项目目录中安装:
cd /srv/my-app
npm ci --omit=dev
service 只固定 Node.js 和项目入口:
[Service]
WorkingDirectory=/srv/my-app
ExecStart=/root/.nvm/versions/node/v24.15.0/bin/node /srv/my-app/server.js
Environment="NODE_ENV=production"
Environment="PATH=/root/.nvm/versions/node/v24.15.0/bin:/usr/local/bin:/usr/bin:/bin"
Restart=always
这样 Node.js 版本、应用版本和依赖版本各自有清晰边界。
排查命令
SSH 当前使用的版本:
command -v node
command -v npm
nvm current
npm config get prefix
npm root --global
npm list --global --depth=0
systemd 实际启动了什么:
systemctl --user show openclaw-gateway.service \
-p ExecStart \
-p Environment
systemctl --user status openclaw-gateway.service
journalctl --user -u openclaw-gateway.service -n 100 --no-pager
进程正在使用哪个 Node.js:
pid="$(systemctl --user show openclaw-gateway.service -p MainPID --value)"
readlink -f "/proc/$pid/exe"
tr '\0' '\n' <"/proc/$pid/environ" | grep -E '^(HOME|PATH|NVM_DIR)='
总结
- 手动安装适合单一版本场景,升级时注意全局包跟随安装前缀。
- NVM 负责下载、保留和切换多个 Node.js 版本,安装时可从 GitHub API 读取最新 release。
- 升级后通过
--reinstall-packages-from或default-packages重装全局 CLI,不复制旧node_modules,不配置自定义 npm prefix。 - systemd 使用 NVM 版本目录中的绝对路径,
Environment=PATH与ExecStart保持同一版本;版本切换是显式、可回滚的部署操作。 - 新版本验证完成前保留旧版本,以便快速回滚。
这样既保留了 NVM 的便利,也保留了长期服务最重要的可预测性。