根分区爆满这种事,遇过一次就长记性了。我手里那台设备,根分区只有几个GB,日志、缓存一多就报警,偏偏业务必须用特定版本的 Node.js,还不能用系统包管理器随便装。后来我把整个 Node.js 运行环境搬到了挂载在 /ext 的大容量分区,才彻底解决问题。这个需求其实很常见:不是每个人都有权限动 / 分区,也不是所有环境都允许自定义安装路径。Node.js 本身不像很多软件那样强绑定 /usr,解压就能跑,官方 tar.gz 直接丢进 /ext 是完全可行的,难的是后续的 PATH、全局模块、动态库、迁移验证这一整套动作。这篇文章就把我实测过的几种安装方式、中间踩过的坑、以及迁移老版本 Node.js 的经验一次性讲清楚,适合做嵌入式、服务器瘦身、或者给 CI 环境定制运行环境的朋友参考。
1. 动手之前,先确认 /ext 分区能不能真的扛住 Node.js
很多人拿到设备第一反应就是“把 Node.js 安装到 /ext 目录”,然后开始找安装命令。但在这之前,还有一件更要紧的事:搞清楚 /ext 这个路径到底挂载在什么存储设备上、挂载参数是什么。我见过不少翻车现场,最后排查下来根本不是安装步骤的问题,而是分区本身就不允许执行程序,或者空间、性能根本不达标。
1.1 为什么不是改个安装路径就能完事
普通桌面软件装到自定义目录,安装完把可执行文件路径加进 PATH 就行,但 Node.js 不是单个可执行文件。一个完整的 Node.js 运行环境至少包含四类内容:bin 目录下的 node、npm、npx 可执行文件,lib 目录下的 Node.js 标准库模块(也就是 node_modules 里内置的那一票模块),include 目录下给 C/C++ 插件用的头文件,share 目录下的 man 手册文档。如果你直接从源码编译或解压官方二进制包,这些内容会集中放在一个前缀目录下,相对好管理。但如果之前是用系统包管理器(比如 apt、yum)装的,这些文件可能被拆散到 /usr/bin、/usr/lib/node_modules、/usr/include/node 好几个地方,迁移时漏掉任何一个都会出问题。
所以,真正要装进 /ext 的不只是一个 node 可执行文件,而是一整套目录树。后面我会分别讲解压式安装、源码编译和 nvm 三种方式,但无论哪种,都要先确认 /ext 本身可用。
1.2 判断 /ext 是否适合承载 Node.js 运行环境
用df -h /ext看剩余空间,用mount | grep /ext查看挂载参数。这里要特别留意两个词:noexec和nosuid。如果 /ext 挂载时带了noexec,那么即使你往 /ext 里面解压了 node,执行/ext/node/bin/node --version也会直接报Permission denied。如果是这种挂载方式,要么重新以 exec 方式挂载(可能需要改 /etc/fstab 后 reboot),要么用环境变量 LD_PRELOAD 那类绕行做法——但后者太折腾,我一般直接建议换路径。
还需要看挂载在什么介质上。如果 /ext 是一块机械硬盘或网络存储,Node.js 启动时要加载大量小文件,频繁读取会导致启动速度变慢,npm 安装依赖时更是痛苦。可以用一个简单命令感受一下:time /ext/node/bin/node -e "require('http')",多跑几次看看耗时。如果每次启动都在百毫秒以上,那就要认真考虑是不是把 npm 缓存放到 tmpfs,或者干脆换更快的存储。
我把主要检查项整理成了下面这张表,动手前逐项过一遍:
| 检查项 | 命令/方法 | 合格标准 |
|---|---|---|
| 可用空间 | df -h /ext | 大于 2GB,编译安装建议大于 5GB |
| 可执行权限 | `mount | grep /ext` |
| 写权限 | touch /ext/.write_test && rm /ext/.write_test | 成功执行 |
| 读取性能 | time dd if=/dev/zero of=/ext/test bs=1M count=100 | 不是异常耗时 |
| 持久挂载 | cat /etc/fstab | 重启后 /ext 仍会挂载 |
这里最后一项很容易被忽略。有的设备 /ext 是开机后由脚本临时挂载的,如果你把 Node.js 装上去,一旦挂载晚于服务启动,依赖 Node.js 的进程就会找不到解释器。所以我建议装完以后一定要重启一次验证,而不是只在当前会话里跑通。
2. 解压式安装:官方二进制包直接扔进 /ext,最简单也最不容易出错
如果不需要定制参数,我强烈建议优先使用官方编译好的二进制包。Node.js 官网提供 linux-x64 / linux-arm64 等预编译产物,解压以后就是一个完整的目录树,不需要编译,也不需要 root 权限(只要 /ext 有写权限)。这种“绿色软件”式的安装方式,特别适合给应用准备独立运行环境。
2.1 从二进制包开始,而不是用系统包管理器
为什么不用 apt install nodejs?因为系统包管理器会强制把文件放进 /usr,并且库里版本往往很旧。我们想要的是完全可控、放在 /ext 下面的一套运行时,用二进制包解压可以做到和系统互不干扰。下载时注意选择版本号对应的 tar.gz,不要下载源码包,源码包是 tar.gz 格式但里面是源码,需要自己编译。
具体做法是先创建一个目录,把包下进去:
mkdir -p /ext/apps cd /ext/apps wget https://nodejs.org/dist/v20.11.0/node-v20.11.0-linux-x64.tar.xz下载完成后,最好校验一下自带的 SHASUMS256.txt,我吃过没校验的亏,下载到损坏的包,解压时报错,白折腾了半天。校验命令:
grep 'node-v20.11.0-linux-x64.tar.xz' SHASUMS256.txt | sha256sum -c -通过后解压到 /ext 下:
mkdir -p /ext/node tar -xJf node-v20.11.0-linux-x64.tar.xz -C /ext/node --strip-components=1--strip-components=1是去掉顶层目录,这样 /ext/node 下面直接就是 bin、lib、include、share,而不是/ext/node/node-v20.11.0-linux-x64/。这一步做完,其实安装已经算完成了,核心工作只剩下让系统能找到它。
2.2 PATH 与软链接:让系统“看到” Node.js
解压出来的 /ext/node/bin 不会被系统自动搜索,需要手动把路径加进 PATH。我习惯在 /etc/profile.d/ 下面新建一个独立文件,这样所有用户登录时都会生效,不用污染 /root/.bashrc:
cat > /etc/profile.d/node-ext.sh <<'EOF' export PATH=/ext/node/bin:$PATH EOF然后source /etc/profile.d/node-ext.sh或者重新登录,再验证:
which node node -v npm -v有些脚本或 systemd 服务在启动时不会加载 profile,会直接去找 /usr/bin/node。这种情况下可以创建软链接给系统里已有的 bin 目录:
ln -s /ext/node/bin/node /usr/local/bin/node ln -s /ext/node/bin/npm /usr/local/bin/npm ln -s /ext/node/bin/npx /usr/local/bin/npx这里有个细节:软链接最好放在 /usr/local/bin,而不是直接改 /usr/bin。因为 /usr/local/bin 在 PATH 里的优先级通常更高,而且这也是系统留给管理员自定义命令的位置,不容易和包管理器管理的文件冲突。另外,不要用硬链接,硬链接在不同文件系统之间是创建不了的,/ext 和 /usr/local 很可能不在同一个分区。
2.3 npm 全局安装目录的二次转移
解压式安装下,npm 的全局包默认会放在 /ext/node/lib/node_modules,全局命令的软链接会放在 /ext/node/bin,这恰好和我们期望的目录一致,所以只要 PATH 包含 /ext/node/bin,npm install -g 后生成的命令就能直接用。这是解压式安装比源码安装舒服的地方。
但如果你的环境里之前设置过 NPM_CONFIG_PREFIX,需要注意这个变量可能会覆盖默认行为。可以通过下面两个命令确认最终生效路径:
npm config get prefix npm root -g如果显示的不是 /ext/node,可以手动指定:
npm config set prefix /ext/node我个人还会顺手把 npm 缓存目录也指到 /ext 下,避免长期占根分区空间:
npm config set cache /ext/node/.npm-cache这一步不是必须的,但既然我们就是为了省根分区,那就省到底。
3. 从源码编译:用 --prefix 让 configure 听你的话
如果你需要的 Node.js 版本没有对应的官方二进制包,或者要针对某个嵌入式架构做裁剪(比如只保留必要功能、去掉 intl),那就只能从源码自己编译。源码编译的精髓就是 configure 时的--prefix参数,它决定 make install 最终把文件装到哪里。
3.1 编译参数怎么定
先下载源码包并解压到临时目录,进入源码目录后执行:
./configure --prefix=/ext/node如果你的环境需要特殊字符集支持,可以加上--with-intl=none来禁用 ICU 数据,这样编译产物体积会小很多,但要注意某些国际化功能会失效。我一般标配是:
./configure --prefix=/ext/node --with-intl=none如果不加 --prefix,默认路径是 /usr/local,编译完 make install 会把文件装到 /usr/local/bin、/usr/local/lib 等地方,那离我们的目标就远了。
接下来编译:
make -j$(nproc) make install-j$(nproc)是并行编译,能大幅缩短时间。但如果你是在内存很小的设备上操作,并行任务太多容易直接 OOM,这时反而要限制并发,比如用make -j2,甚至make -j1。
3.2 编译期间常见的两个坑
第一个坑是编译依赖缺失。Node.js 从源码编译需要 Python、GCC/G++、Make 和 libc 开发头文件。在精简系统上很可能没有装齐全,configure 阶段会提示“Python not found”或者 “C++ compiler not found”。这时候不要硬着头皮去改环境变量,先把基础编译工具装好再说。具体用什么命令取决于系统包管理器,我就不展开了。
第二个坑是磁盘空间。编译产物和中间文件比最终安装体积大得多,/usr/tmp 或源码所在分区空间不足会在链接阶段报错。所以我一般把源码放在 /ext 下编译,让临时分区和安装目录在同一块空间比较大的存储上。曾经因为 /tmp 只有几百 MB,编译到一半报错,后面我学乖了,先在 /ext 下建一个 build 目录,再在源码目录里export TMPDIR=/ext/tmp把临时文件指过去。
编译安装完成后,多花一分钟检查一下 node 依赖的库:
ldd /ext/node/bin/node如果你的 Node.js 是动态链接了 libstdc++ 之类的库,在某个精简系统上运行时可能会提示找不到共享库。解决办法有两个:把对应库的路径加进/etc/ld.so.conf.d/然后执行ldconfig,或者启动前设LD_LIBRARY_PATH。不过大多数官方预编译包和常规编译配置下,node 对系统库的依赖很少,一般不用特别处理。
3.3 编译安装后还要手动补哪些环境变量
make install 不会帮你改 PATH,所以源码编译完成后的配置工作与解压式安装基本一致:写 /etc/profile.d 下的 PATH,按需做软链接。唯一多出来的步骤是确认 include 和 lib 路径有没有被正确安装到 /ext/node 下面:
ls /ext/node/include/node ls /ext/node/lib/node_modules这两个目录存在,说明带原生模块的 npm 包后续才能找到头文件。如果以后要写 C++ 插件,编译时通过node-gyp自动找 /ext/node/include/node,不需要额外设置。
4. nvm 方案:不想手动管理版本时的懒人路线
对于需要频繁切换 Node.js 版本的人来说,nvm 永远是绕不开的。nvm 本身是 shell 脚本,默认会装在用户主目录的 ~/.nvm 下,但完全可以把 NVM_DIR 指向 /ext。这样版本文件全部落在 /ext,既省根分区,又能保留多版本切换的能力。
4.1 设置 NVM_DIR 到 /ext
安装 nvm 之前先手动建好目录:
export NVM_DIR="/ext/nvm" mkdir -p "$NVM_DIR" curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash安装脚本会检测当前环境里的 NVM_DIR 变量,然后把脚本文件克隆到 /ext/nvm,并且自动往你的 shell 配置文件里写入两行。因为我上面已经导出了 NVM_DIR,所以写入的配置一般就是正确指向 /ext/nvm。但保险起见,装完以后打开 ~/.bashrc 或 ~/.profile 检查一下,确保不是旧默认路径。
然后重新 source 一下配置:
source ~/.bashrc nvm --version接下来正常使用 nvm install 20、nvm use 20 即可。安装的每个版本会放在 /ext/nvm/versions/node/ 下,nvm use实际上是通过修改 PATH 指向对应版本目录。
4.2 nvm 安装的 Node 其实也依赖 /ext 的空间
使用 nvm 时,npm 全局包的位置和直接解压安装不太一样。默认情况下,npm 的全局根目录仍然是当前激活的那套 Node 目录,也就是 /ext/nvm/versions/node/v20.11.0/。所以它会随着版本变化。如果你给每个版本都全局安装了一大堆东西,这些包就会重复占空间。我自己习惯少用全局包,需要哪些依赖就放在项目里,这样换版本时不用重复安装。
还有 npm 缓存。nvm 安装的 node 会把 npm 缓存默认放到 ~/.npm。如果家目录也位于根分区,缓存多了还是会长回去。所以同样建议转移到 /ext:
npm config set cache /ext/nvm/.npm-cache4.3 卸载迁移时容易漏掉的隐藏文件
如果你是把自己的 ~/.nvm 整体迁移到 /ext/nvm,有一个很容易漏掉的点:nvm 在~/.nvm之外还会维护一个~/.nvmrc或 shell 配置里的自动切换源码。如果你仅复制走了目录,但 shell 配置没有改全,新终端打开后还是会去找旧路径。
另外,迁移前最好执行nvm alias default看一下当前默认版本,迁移后要在新目录下确认默认 alias 还在。我遇到过迁移之后nvm use能切换成功,但新开的终端 node 命令找不到,原因就是默认 alias 丢了。
5. 迁移老版本 Node.js:一个现成的目录搬家实例
如果你的系统里已经装好了 Node.js,但不是装在 /ext,而是散落在 /usr/local 甚至 /usr,根分区又快满了,这时候再重新下载未必方便。还有一种思路,就是直接把现有环境整体搬运到 /ext。第一次做的时候我踩了不少坑,这里把一条比较稳妥的路径整理出来。
5.1 搬家前先看 node_modules 与全局包的存放位置
先用一组命令把当前 Node.js 相关的内容大致摸清。千万不要只拷 /usr/bin/node,那只是冰山一角。
| 路径 | 常见内容 | 是否需要搬家 |
|---|---|---|
| /usr/bin/node | node 可执行文件 | 是 |
| /usr/bin/npm / npx | npm 主程序 | 是 |
| /usr/lib/node_modules | npm 全局包目录 | 是 |
| /usr/include/node | 头文件 | 建议搬运 |
| /usr/share/man/man1/node.1 | 手册 | 可选 |
| ~/.npm | 本地缓存 | 可以不搬,重装即可 |
一个最省心的方式,是用 npm 导出当前全局包列表,然后在新的 /ext 环境里重装这些全局包:
npm list -g --depth=0 > global-packages.txt这样迁移 Node 二进制本身之后,不必担心复制 lib/node_modules 时文件权限或链接损坏的问题。如果全局包不多,重装反而比无脑拷贝更干净。
5.2 迁移步骤和验证命令
第一步,先准备目录结构。我习惯把目标环境放在/ext/node,然后手动把三种来源的文件汇进去:
mkdir -p /ext/node/bin /ext/node/lib /ext/node/include /ext/node/share第二步,把当前 node、npm、npx 复制到 /ext/node/bin 下:
cp -a /usr/bin/node /ext/node/bin/ cp -a /usr/bin/npm /ext/node/bin/ cp -a /usr/bin/npx /ext/node/bin/第三步,把全局包和头文件都复制过去:
cp -a /usr/lib/node_modules /ext/node/lib/ cp -a /usr/include/node /ext/node/include/这一步有个大坑:/usr/lib/node_modules里的包通常带有指向/usr/bin/node的 shebang,比如#!/usr/bin/env node通常是没问题的,但如果有包内部硬编码了/usr/bin/node路径,就需要用 grep 全面检查:
grep -R "/usr/bin/node" /ext/node/lib/node_modules 2>/dev/null | head对于命中硬编码路径的文件,要么手动 sed 替换成/ext/node/bin/node,要么用软链接兜底。我的习惯是在 /usr/local/bin 下做软链接,这样即使有脚本写的是#!/usr/bin/env node,也不影响查找。
最后,把 PATH 切换过去验证:
export PATH=/ext/node/bin:$PATH node -v npm -v npm root -g which node如果npm root -g显示/ext/node/lib/node_modules,说明全局包已经正确识别。此时再运行一下项目里的核心接口做 smoke test,确认没有启动级别的缺失。
5.3 路径写死在脚本里的历史遗留问题
迁移最容易翻车的不是文件复制,而是那些写着绝对路径的脚本和 service 文件。systemd 服务文件里常写ExecStart=/usr/bin/node app.js,这种如果没改成新路径,重启后就会静默失败。建议迁移后全局搜索一次:
grep -R "/usr/bin/node\|/usr/local/bin/node" /etc/systemd /opt /srv /home 2>/dev/null有命中的就逐个替换成/ext/node/bin/node。如果你已经做了/usr/local/bin/node -> /ext/node/bin/node的软链接,那部分脚本可以暂时不替换,但依赖/usr/bin/node的还是要改,因为 /usr/bin 下通常不存在指向 /ext 的软链接。
还有环境变量。有些人会在配置文件里写NODE_HOME=/usr/local/node之类,迁移后也要同步更新。总之,路径问题宁可多搜几遍,也不要图省事。
6. 装完之后的体检:验证、性能考量与日常维护
安装完并不代表结束。我从实际使用中总结了几件事,每次装完或更新完 Node.js 都会例行做一遍,能省掉很多后续的排查时间。
6.1 验证安装位置真的生效
有时候你以为用的是 /ext/node,实际上操作系统里还残留着更早的 Node.js 版本在 PATH 前面。用下面三行快速确认:
which node readlink -f "$(which node)" node -p "process.execPath"process.execPath是最可信的,它输出的是当前进程实际的二进制路径。如果输出不是 /ext/node/bin/node,说明 PATH 顺序有问题,检查 /etc/profile.d 和 /root/.bashrc 里的 export 是不是被后加载的配置覆盖了。
6.2 在 /ext 上运行时需要留意写盘延迟
/ext 分区如果是普通磁盘或 SD 卡,Node.js 运行时的日志写入和 npm 依赖写入可能会比根分区内置固态慢不少。对于性能敏感的场景,有几个取舍:
- 把 Node.js 应用本身的日志目录放到 tmpfs(如 /dev/shm),重启丢失但速度快
- npm 缓存放在 /ext 下,虽然慢一点,但能避免根分区爆满
- 如果内存足够,可以把整个 /ext/node 做成一个只读挂载,然后只把可写部分重定向到 tmpfs
具体怎么做取决于业务。我遇到过的稳妥方案是:Node.js 安装在 /ext/node 保持只读,项目代码在 /srv 下,日志写到 /var/log 或内存盘,并配合 logrotate 限制大小,这样根分区压力最小。
6.3 开机自启与更新策略
很多设备是通过 systemd 启动 node 服务的,如果服务文件里写的是相对 PATH 的node,那么开机早期可能拿不到 /ext 下的 PATH。最好在 ExecStart 里写完整绝对路径:
ExecStart=/ext/node/bin/node /srv/app/app.js更新 Node.js 版本时,解压式安装和源码编译安装都比较简单:下载新版本,确认 SHASUMS,解压到旧目录上。但不要只是覆盖,最好先把旧目录留一份备份,或者确认新版完整解压后再切换软链接。用 nvm 更新更简单,直接nvm install new,然后nvm alias default new,再删掉旧版本即可。
我最后再分享一个小技巧:在 /ext/node 下放一个 VERSION 文件,每次安装或更新后写入node -v的输出,比如:
node -v > /ext/node/VERSION这样过几个月回来,一眼就能看出设备上跑的是哪个版本,不用再执行命令猜。维护环境的细水长流,往往就是靠这些小习惯积累起来的。