Red Hat 系发行版上装 Node.js,乍看就是三条命令的事,真到了生产机上,版本流、模块流、SELinux、systemd、npm 全局权限,随便哪一项都能把人卡上半小时。我经手过几台 RHEL 8 和 RHEL 9 的机器,也帮人收拾过“装完 node 之后 npm 报 EACCES”“dnf 装完版本莫名其妙变回 16”“服务重启就挂”这类摊子,回过头看,装本身不难,难的是把版本来源、权限模型和开机自启这三件事一次性理顺。这篇内容就围绕“在 Red Hat 系系统上安装 Node.js 并进入正常编程开发”这个主线,把选型逻辑、完整操作步骤、参数计算和踩坑记录摊开讲。
不管你是刚接手一台 RHEL 服务器的后端同学,还是要给前端项目搭构建环境的运维,或者只是想在本地 CentOS Stream 上跑个脚本,下面这套路径都能照着走。我会先讲清楚 Red Hat 的模块流机制和 Node.js 版本节奏这对关系,再对比四条安装路线各自适合谁,然后给三套能直接复制的实操流程,最后把 systemd 托管、SELinux 拦截、原生模块编译失败这些高频问题整理成速查表。全程按我实际动手的顺序来,命令都是验证过的。
1. 先搞清楚要装的是什么:Red Hat 的软件源机制与 Node.js 的版本账
装 Node.js 之前不先确认系统底子,后面八成要返工。Red Hat 系和 Ubuntu 系最大的差别不是命令长短,而是它有一套 Application Streams(应用流)机制,同一个软件名在仓库里可能挂着好几个版本流,默认启用哪个、装完是哪个,全看模块状态。我见过有人执行完dnf install nodejs,node -v出来是 v16,而项目 package.json 里写着"node": ">=20",然后开始怀疑人生。这不是命令错了,是模块流默认选了个老版本。
1.1 先摸清发行版版本和仓库状态
第一件事永远是确认自己在哪台机器上。cat /etc/redhat-release和cat /etc/os-release两条都跑一下,前者给的是发行版标识,后者给的是版本号和代号,两者对照着看更靠谱。RHEL 7、RHEL 8、RHEL 9 在这件事上的处理方式完全不同:RHEL 7 走的是 yum 加额外仓库的老路,而且这个版本早已停止维护,新项目不该再从它起步;RHEL 8 和 RHEL 9 才是 dnf 加模块流的玩法,RHEL 9 的模块里可选版本比 RHEL 8 更新一截。
紧接着看模块状态:dnf module list nodejs。这条命令的输出会告诉你三件事——有哪些流可用、哪个是默认流(标着[d])、当前装没装(标着[i]或[e])。如果输出里某一行前面带[e],说明这个流被显式启用过,哪怕你没装包,dnf 也会优先按这个流解析依赖。这一步很多人跳过,结果后面装第三方仓库的时候和模块流打架,报一堆module filtering相关的错误。
除了版本,还要确认三件外部条件:机器能不能出网、内网有没有可用的软件仓库镜像、有没有装第三方仓库的授权。这三条直接决定你走哪条安装路线。有些企业的机器只能访问内网镜像源,那第三方 RPM 仓库这条路就得先让人同步到内网,否则 curl 一出去就超时。还有磁盘空间,Node.js 本体不大,但node_modules和 npm 缓存能把一个 20G 的根分区吃满,提前df -h看一眼/var和/home的余量,比事后清理省事。
1.2 Node.js 的版本节奏与“够用”的判断标准
Node.js 的发布节奏是固定的:每年 4 月和 10 月各出一个大版本,偶数版本进入长期支持(LTS),奇数版本只维护很短一段时间就下线。选版本的时候只认偶数版,别碰奇数版,这是行内默认的规矩。LTS 版本一般有两到三年的维护窗口,前一段是活跃维护,后一段只修安全问题,具体时间点去官方 release schedule 页面查,别凭记忆。
判断“我这个项目到底要哪个版本”,别猜,看两处。第一处是项目依赖里各个包的engines字段,npm ls或者直接翻 package-lock.json 都能看到,很多构建工具会明确写>=18。第二处是构建工具链本身的要求,近两年的前端构建工具普遍把最低版本抬到了 18 甚至 20,用 16 去跑经常报语法错误,因为新版本才支持的一些语法特性老版本解析不了。
还有一条经验:生产环境和开发环境的主版本号必须对齐,小版本可以差。开发机用 20.11、服务器用 20.9 一般没事,但开发机 20、服务器 18,就等着上线当天排查“本地能跑线上跑不了”吧。这条约束后面讲 nvm 和 .nvmrc 的时候还会用到,先把结论放这儿。
2. 四条安装路线的取舍:为什么我最终选了能长期维护的那条
同一个目标有四条路能到,选哪条不看哪个快,看你要在这台机器上待多久。我把这四条路的适用场景、优缺点和隐藏代价整理在下面这张表里,先看结论再往下读细节。
| 路线 | 版本新鲜度 | 多版本切换 | 与系统集成度 | 典型适用场景 |
|---|---|---|---|---|
| 系统模块流(dnf module) | 偏低,滞后数个版本 | 不支持 | 最高,dnf 统一管理 | 生产服务器、长期维护、内网镜像 |
| 第三方 RPM 仓库 | 高,紧跟官方 | 不支持 | 较高,仍走 dnf | 需要较新版本的生产机 |
| nvm 版本管理器 | 最高,任意版本 | 支持 | 低,纯用户态 | 开发机、多项目并行 |
| 官方二进制包 / 容器镜像 | 高 | 手动软链可切换 | 中,需手工维护 | 离线环境、容器化部署 |
2.1 系统模块流:最稳但版本偏旧
系统自带的模块流是我在生产机上默认的第一选择,理由很实在:它和 dnf 是一套体系,安全补丁跟着系统更新一起走,内网镜像源能覆盖,审计的时候也有据可查。运维同事做漏洞扫描,看到包来自官方仓库,直接过;看到来自某个第三方源,还得额外走一轮审批。这种“流程成本”在真实工作里往往比技术优劣更影响决策。
它的代价是版本滞后。系统仓库里的 Node.js 通常比官方最新 LTS 落后一到两个大版本,因为发行版要考虑兼容性和测试周期。如果你的项目只是跑个接口服务、用 express 或 fastify 这类稳定框架,滞后一两个版本完全不影响。但如果项目用到新版本才有的 API,比如某些内置的测试运行器特性、新的 fetch 实现细节,那就得掂量了。
模块流还有个细节要注意:装的时候要指定 profile。dnf module install nodejs:20/common里的common是配置档,装的是常用组件;还有minimal和development两个档,development会多装一些编译工具链相关的东西,适合要在服务器上直接编译原生模块的场景。不指定 profile 的话默认走common,多数情况够用。
2.2 第三方 RPM 仓库:版本新、仍然可管
当项目确实需要较新版本,又不想放弃 dnf 的统一管理,那就上第三方 RPM 仓库。社区里维护得比较勤的是 NodeSource 那个仓库,RHEL、CentOS Stream、Rocky、Alma 这些下游发行版都覆盖。它的工作方式很简单:往/etc/yum.repos.d/里放一个仓库配置文件,导入签名公钥,之后dnf install nodejs就从它那儿取包。
这条路最大的坑是和系统模块流冲突。系统仓库里存在 nodejs 模块,第三方仓库里是普通 RPM 包,dnf 解析的时候会犯迷糊,经常出现“明明加了仓库,装完还是老版本”或者直接报模块过滤错误。解决办法是装之前先把系统模块流关掉:dnf module disable nodejs -y。这一条我吃过亏,第一次配的时候折腾了四十分钟才反应过来是模块在捣乱。
提示:第三方仓库的安装脚本常见写法是把远程脚本直接管道给 shell 执行。生产环境我建议别这么干,把仓库配置文件和公钥地址手工写进
.repo文件,内容看得见、diff 得出来,出了问题也好回滚。
2.3 nvm 版本管理器:开发机的正确答案
nvm 是纯用户态的东西,装在当前用户的家目录下,不碰系统目录,也不需要 root。它的价值在于多版本共存和秒级切换:一个终端跑 Node 18 维护老项目,另一个终端nvm use 20跑新项目,互不干扰。前端同学本地开发、需要同时对接几个不同技术栈的老项目,这个几乎是刚需。
它的短板也很明显,两条。第一条,nvm 靠 shell 函数注入环境变量,systemd 启动的服务读不到这些变量,所以服务器上用它托管常驻服务会很别扭,ExecStart里得写 node 二进制的绝对路径,而且 nvm 目录一换路径就失效。第二条,它绕过了系统的包管理,安全更新得自己nvm install重装,长期没人管的开发机很容易攒下一堆过期版本。
我的习惯是:开发机用 nvm,服务器用 dnf 装。两边主版本号靠.nvmrc和文档对齐,谁也别怪谁。
2.4 二进制包与容器镜像:离线场景的兜底
有些机器压根不出网,内网仓库也没同步 Node.js,那就只能走官方二进制包。官网下载.tar.xz,解压到/usr/local/lib/nodejs/下面,再把bin目录软链到/usr/local/bin。这条路的优点是干净、可控、不需要任何仓库,缺点是升级要手工替换、多版本切换要自己维护软链、安全更新完全靠人盯。
容器化部署则是另一个维度的事。基础镜像里自带 Node.js,版本由镜像标签决定,构建和运行环境天然一致,这是它最大的优势。但要注意镜像体积和构建缓存,node_modules不要在宿主机和容器之间来回拷贝,容易出权限和平台不一致的问题。这个话题展开能写一整篇,这里只提一句:容器里跑 Node.js,本地开发该有的调试能力一样要有,别为了镜像小而把构建工具全删了导致线上排障没法下手。
3. 从零到能写代码:三条路线的完整实操
下面三条流程都是我实际跑过的,命令按顺序贴,中间穿插说明每一步为什么这么写。你可以根据自己的场景挑一条,别混着来,混着来最容易出模块冲突。
3.1 通用前置:系统更新与基础依赖
不管走哪条路,前四步是一样的。先更新系统:dnf update -y。这一步看着废话,但真的重要——老版本的系统仓库元数据可能已经过期,直接装包容易遇到依赖解析失败,更新完再操作顺畅很多。生产机如果要严格控制变更,至少把ca-certificates、curl、gnupg这几个包装上,HTTPS 握手的证书链问题和签名校验问题都靠它们。
时间同步也顺手确认一下:timedatectl status看System clock synchronized是不是 yes。服务器时间漂移超过几分钟,npm 装包时的 TLS 校验会直接失败,报的错还特别绕,说是证书不受信任,实际是时钟跑偏了。没同步的话装个 chrony 起起来,systemctl enable --now chronyd,两分钟的事。
然后建一个专用的运行用户,别用 root 跑 Node 服务。useradd -r -m -s /sbin/nologin nodeapp这条命令里,-r表示系统用户,-m建家目录,-s把登录 shell 设成不可登录,降低被拿来当跳板的风险。应用目录放/opt/app,属主改成它:mkdir -p /opt/app && chown -R nodeapp:nodeapp /opt/app。这个用户后面写 systemd 单元的时候还要用。
3.2 路线一:用系统模块流装(生产首选)
第一步查可用流:dnf module list nodejs。输出里会列出所有流,比如 16、18、20 几行,默认流后面标着[d]。选一个满足项目要求的,比如 20。如果之前误启用了别的流,先重置:dnf module reset nodejs -y,这条会把模块状态清干净,装过的包不删,但流的选择回到初始状态。
第二步装:dnf module install nodejs:20/common -y。注意冒号后面是流版本,斜杠后面是 profile。装的过程中它会提示要安装的包列表,确认一下版本号对不对再按 y。
第三步验证:node -v && npm -v && which node。which那条别省,输出应该是/usr/bin/node。如果输出了/usr/local/bin/node或者某个 nvm 路径,说明 PATH 里有别的优先级更高的东西,得先清理,否则你以后每次开新终端都可能用错版本。
第四步切换版本。比如项目要从 18 升到 20,dnf module reset nodejs然后重新dnf module install nodejs:20/common。注意 reset 之后要重装,不是自动升级的。这一步在维护窗口做,别在业务高峰切。
注意:模块流之间不能同时启用两个。有人想“我 18 和 20 都装着,用的时候切”,这条路在系统模块流上行不通,想要多版本共存只能用 nvm 或者手工软链。
3.3 路线二:第三方 RPM 仓库装(要新版本)
先关掉系统模块流,这一步是前提:dnf module disable nodejs -y。不关的话后面装包会被模块过滤掉。然后写仓库配置文件,我用vi或者cat重定向都行,路径/etc/yum.repos.d/nodesource.repo,内容大概是下面这样,baseurl具体地址以仓库官方页面给出的为准,不同大版本路径不一样,别照抄错:
[nodejs] name=Node.js RPM Repository baseurl=https://rpm.nodesource.com/pub_20.x/nodistro/nodever/x86_64/ enabled=1 gpgcheck=1 gpgkey=https://rpm.nodesource.com/gpgkey/ns-operations-public.key repo_gpgcheck=1gpgcheck=1和repo_gpgcheck=1两个都打开,包签名和仓库元数据签名都验,多花几秒钟换来的是一层保障。公钥地址同样以官方页面为准,写错了 dnf 会报Public key not installed,那时候别急着关 gpgcheck,先回头核对地址。
写完清缓存:dnf clean all && dnf makecache。然后装:dnf install nodejs -y。装完同样验证node -v、npm -v、which node。
这条路线升级很简单,dnf update nodejs就行,仓库里出新版本 dnf 会跟着走。但要注意大版本升级不要用 update 直接跨,比如从 18 仓库换成 20 仓库,得先改 repo 文件里的 baseurl,再dnf distro-sync或者干脆卸载重装。跨大版本 update 偶尔会留下旧版本的残留文件,node -v显示的版本和你预期对不上,那时候rpm -qa | grep nodejs看一眼装了哪些包,再决定是卸还是补。
3.4 路线三:nvm 装(开发机、多版本)
先装依赖:dnf install -y git curl。然后拉 nvm,官方推荐用安装脚本,我更喜欢 git clone 到固定目录,可控性强一些:
export NVM_DIR="$HOME/.nvm" git clone https://github.com/nvm-sh/nvm.git "$NVM_DIR" cd "$NVM_DIR" && git checkout v0.39.7版本号别用最新,用一个稳定的 tag,写进文档里,团队统一。clone 完之后在~/.bashrc末尾追加三行加载逻辑,这个不能省,省了下次开终端nvm命令就没了:
export NVM_DIR="$HOME/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"source ~/.bashrc之后nvm -v能出版本号就通了。然后装 LTS:nvm install --lts,装完设默认:nvm alias default 'lts/*'。再验证nvm ls看列表里有没有标default的那一行。
多版本切换就是nvm install 18装完再nvm use 18,nvm current看当前是哪个。项目目录里放一个.nvmrc,内容写版本号比如20,进目录执行nvm use会自动读这个文件切过去。这个约定对多人协作特别有用,能省掉“你用的什么版本”这种来回问。
注意:装了 nvm 之后,
sudo npm install -g这种命令会失效或者装到奇怪的地方去。因为 sudo 会重置环境变量,找不到 nvm 的路径。要全局装包就老老实实不带 sudo:npm install -g 包名,它装到当前 nvm 版本对应的目录下,本来就是用户可写的,不需要提权。
3.5 装完之后的验证与 npm 基础配置
三条路线装完,验证要做得比node -v更彻底一点。跑一段node -e "console.log(process.versions)",把 v8、openssl、modules 几个版本都打出来,确认 ABI 版本号和后续要装的原生模块对得上。再跑node -e "console.log(process.execPath)"确认二进制路径是你期望的那个。
npm 那边先看三件事。npm config get prefix看全局安装位置,系统包管理器装的 npm 一般指向/usr,意味着普通用户npm i -g会撞权限错误。npm config get cache看缓存目录,默认在~/.npm,长期不清理能堆到几个 G,定期npm cache verify或者npm cache clean --force收一下。npm config get registry看拉包源,公司有内网私有仓库(Nexus、Artifactory 这类)的话把它指过去,拉包速度和可控性都会好很多。
配置的优先级要清楚:项目根目录的.npmrc优先级最高,其次是用户级的~/.npmrc,然后是全局的,内置的最后。这意味着项目里写死了一个源,你在用户级怎么改都不生效。排查拉包慢的问题,第一件事就是cat .npmrc看有没有被人埋了东西。切换源用npm config set registry <地址>,想只对当前项目生效就加--location=project参数。
最后跑个能访问的东西验证整条链路。写个最小的server.js,起个 HTTP 服务监听 3000,然后本地curl -I http://127.0.0.1:3000看返回 200。这一步能同时验证 Node 本身、端口能力、防火墙策略三件事,比单纯看版本号有意义得多。
4. 让 Node.js 服务在服务器上“住下来”:systemd 与被忽略的系统配置
自己开个终端跑node server.js只能算调试,要让它断电重启后还在,必须交给 systemd。这一步看着只是写个配置文件,实际上涉及用户权限、环境变量、资源上限、安全策略四层东西,每一层都能让服务起不来或者跑一会儿就死。
4.1 单元文件怎么写才不踩坑
单元文件放/etc/systemd/system/nodeapp.service,内容大致如下。几个关键点我逐条说。
[Unit] Description=Node.js Application Service After=network-online.target Wants=network-online.target [Service] Type=simple User=nodeapp Group=nodeapp WorkingDirectory=/opt/app ExecStart=/usr/bin/node /opt/app/server.js Restart=always RestartSec=5 Environment=NODE_ENV=production Environment=PORT=3000 EnvironmentFile=-/etc/sysconfig/nodeapp LimitNOFILE=65535 StandardOutput=journal StandardError=journal SyslogIdentifier=nodeapp [Install] WantedBy=multi-user.targetExecStart必须写二进制绝对路径。这是 nvm 用户最容易栽的地方,写node靠 PATH 解析,systemd 的环境里根本没有你 shell 里的 PATH,起来就报status=203/EXEC。前面which node那条命令就是为这里准备的,直接抄过来。
EnvironmentFile前面加个减号表示文件不存在也不报错,方便分环境放配置。数据库密码、密钥这类东西别写在单元文件里,单元文件权限宽,放在/etc/sysconfig/nodeapp里把权限设成 600 更合适。
Restart=always配合RestartSec=5是防止崩溃后无限重启刷日志,5 秒间隔比较温和。如果服务是常驻型的,always合适;如果是一次性任务型的,得改成on-failure,不然跑完就重启,变成死循环。
写完之后systemctl daemon-reload、systemctl enable --now nodeapp、systemctl status nodeapp。改过单元文件必须重新 daemon-reload,否则 systemd 用的还是旧配置,这个坑我踩过不止一次,改了十行代码没生效,最后发现根本没重载。
日志用journalctl -u nodeapp -f跟,-n 200看最近两百行,--since "10 min ago"按时间过滤。比让应用自己往文件里写方便,也不用操心日志轮转,journald 有内置的容量控制。
4.2 SELinux 和防火墙:真正拦你的往往是这一层
getenforce看一眼 SELinux 状态。默认是Enforcing,这时候 Node 进程本身监听 3000 这种高位端口一般不受影响,因为服务跑在宽松的域里。真正被拦的是两个场景。
第一个场景是用 nginx 或 httpd 做反向代理。代理进程想连接到后端的 3000 端口,SELinux 默认不允许它发起网络连接,结果就是用户看到 502,日志里写Permission denied,systemctl status nginx还一片正常。解决办法是开布尔值:setsebool -P httpd_can_network_connect 1。这是最常见的一条,几乎每个前后端分离部署的项目都要开。
第二个场景是静态文件目录的上下文不对。nginx 要读的目录如果放在/opt下面,默认上下文是usr_t之类的,httpd 读不了。用ls -Z看上下文,用semanage fcontext -a -t httpd_sys_content_t "/opt/app/public(/.*)?"加上规则,再restorecon -Rv /opt/app/public生效。改完别忘 restorecon,只改规则不重打标签是不生效的,这点和防火墙不一样。
真被拦了怎么确认?ausearch -m avc -ts recent能看到最近的拒绝记录,里面有进程名、目标类型、被拒的操作,照着记录去查该开哪个布尔值或者加哪条规则。临时用setenforce 0验证是不是 SELinux 的问题可以,但验证完立刻setenforce 1切回来,别图省事一直关着,这台机器以后所有的安全检查都会亮红灯。
防火墙那边简单得多,firewall-cmd --permanent --add-port=3000/tcp然后firewall-cmd --reload。如果是给外部访问的正式服务,通常不直接开业务端口,而是只放 80 和 443,由反向代理转发到本机的 3000,3000 压根不用对外开。这个架构更干净,改端口也不影响用户。
4.3 文件句柄、内存与内核参数
Node 服务跑久了出现EMFILE: too many open files,问题不在代码里,在系统限制。先看ulimit -n,普通用户默认可能是 1024,对高并发服务明显不够。改法有两处,/etc/security/limits.conf或者/etc/security/limits.d/下面加一个配置文件,对应用户设nofile65535。但注意,systemd 启动的服务不读 limits.conf,得在单元文件里写LimitNOFILE=65535,这就是前面那份配置里那行的由来。两处都写一遍才保险,因为手工登录调试的时候走的是 limits.conf,服务启动走的是单元文件。
内存方面,Node 的堆内存有默认上限,机器内存大不代表 Node 能用满。老版本默认大概 1.5G 到 2G,新版本会自动按可用内存算,但在容器或者有内存限制的环境里算出来的值可能偏小。要手动调就用NODE_OPTIONS=--max-old-space-size=4096,写在EnvironmentFile里,单位是 MB。这个值不是越大越好,设得比物理内存还大,进程会先被系统 OOM killer 杀掉,而不是抛 JS 层面的内存错误。判断有没有被杀过,dmesg -T | grep -i "killed process"一看便知。
内核层面还能调一个net.core.somaxconn,控制监听队列长度,高并发下默认 128 或 4096,太小会出现连接被拒的情况。用sysctl -w net.core.somaxconn=65535临时生效,写进/etc/sysctl.d/才能持久。这个参数不是必调项,压测时发现连接堆积再动它,别一上来就乱改。
5. 常见问题与排查技巧实录
下面这些问题都是我在真实机器上遇到的,按现象、原因、处理三列整理成速查表,遇到同类情况直接对号入座。
| 现象 | 大概率原因 | 处理方式 |
|---|---|---|
node: command not found | PATH 未包含安装目录,或 nvm 未加载 | 检查which node,nvm 用户检查.bashrc加载段 |
npm i -g报 EACCES | 全局目录属于 root,普通用户无写权限 | 改 prefix 到~/.npm-global并加入 PATH |
| dnf 装完版本还是旧的 | 系统模块流默认流覆盖了第三方仓库 | dnf module disable nodejs或reset后重装 |
服务启动报203/EXEC | ExecStart用了相对路径 | 改成 node 二进制绝对路径 |
| nginx 反代 502,后端日志无请求 | SELinux 拦截了代理进程的网络连接 | setsebool -P httpd_can_network_connect 1 |
| 装原生模块时报 gyp 错误 | 缺少编译工具链 | 装gcc-c++ make python3 |
| 拉包卡住或超时 | registry 不可达或 DNS 异常 | 检查npm config get registry,配置内网源 |
| 运行一段时间被强制终止 | 内存超限被系统 OOM killer 干掉 | 查dmesg,调整堆上限或加内存 |
| TLS 证书校验失败 | 系统时间漂移或缺少根证书 | 同步时间,重装ca-certificates |
5.1 权限与全局包的那些事
npm install -g报权限错误是新装机器最高频的问题,根子在系统包管理器把 npm 的全局目录放在了/usr/lib/node_modules,普通用户写不进去。网上流传最广的解决办法是sudo npm install -g,我强烈不建议,理由有两个:一是 sudo 下 npm 生成的文件属主是 root,后续普通用户操作会连锁出各种权限问题;二是某些包的安装脚本会在提权状态下执行,风险不好控制。
正解是换全局目录。npm config set prefix ~/.npm-global,然后在~/.bashrc里把~/.npm-global/bin加到 PATH 前面,重开终端之后npm i -g全部落在用户目录下,干净且不需要提权。用 nvm 的话这步都不用做,nvm 天然就是用户态隔离的,每个 Node 版本有自己独立的全局目录。
还有一种情况是项目目录的属主搞错了。有人用 root 跑过一次npm install,node_modules里全是 root 属主的文件,之后普通用户再装就各种 EACCES。处理方式是chown -R 用户名:组名 项目目录,然后删掉node_modules重装,别在上面修修补补浪费时间。
5.2 版本冲突与依赖解析
模块流和第三方仓库打架这件事前面提过,这里补一个排查手法。装完之后版本不对,先跑dnf module list nodejs看被启用的是哪个流,再看rpm -qa | grep -i nodejs列出实际装的包,两者对不上基本就是模块在干扰。清场顺序是:dnf module reset nodejs、dnf remove nodejs npm、dnf clean all,然后按你选定的路线重来一遍。别嫌麻烦,混装状态下修修补补的时间比推倒重来长得多。
另一类是原生模块编译失败。带 C++ 扩展的包(各种图像处理、数据库驱动、加密库)安装时要现场编译,缺 gcc、缺 make、缺 python3 都会报 gyp 错误。装齐三件套:dnf install -y gcc-c++ make python3。编译完还可以顺手清一下缓存,因为编译产物跟 Node 的 ABI 版本绑定,升级 Node 大版本之后旧的编译缓存会失效甚至报错,npm rebuild或者直接删node_modules重装。
6. 一套可复用的落地清单
讲了这么多细节,最后收口到可以照着执行的东西上。下面这份清单我一般会整理成一个脚本或者一份内部文档,新机器上手的时候照着做,比每次凭记忆敲省心。
6.1 新机器初始化检查与安装
#!/usr/bin/env bash set -euo pipefail # 1. 基础信息确认 echo "=== 系统信息 ===" cat /etc/redhat-release uname -m # 2. 系统更新与基础依赖 dnf update -y dnf install -y curl ca-certificates git tar xz # 3. 时间同步 systemctl enable --now chronyd timedatectl status | grep -i synchronized # 4. 查看可用 Node.js 模块流 echo "=== 可用 nodejs 模块流 ===" dnf module list nodejs || true # 5. 安装指定版本(按需改成 18 或 20) dnf module reset nodejs -y || true dnf module install nodejs:20/common -y # 6. 验证 node -v npm -v which node node -e "console.log(process.versions)"脚本里set -euo pipefail这三件套建议都加上,出错立刻停,避免在一个已经失败的状态下继续往下装,最后搞出一台状态不明的机器。|| true是给那些“不存在也不算错”的命令留的余地,比如模块还没启用的时候 reset 会返回非零。
6.2 团队约定与升级窗口
技术方案定下来之后,真正决定它稳不稳的是约定。我们内部现在固定几条:主版本号写进.nvmrc和项目文档,任何人本地起环境先nvm use;package-lock.json必须进版本库,不许 gitignore;生产环境只走 dnf 装的那套,不用 nvm,避免服务启动路径依赖用户家目录;升级 Node 大版本统一走变更窗口,先在一台预发机跑满一周再全量。
升级前的验证清单也不复杂:npm ci能不能干净装完、构建能不能过、核心接口的冒烟测试能不能过、有没有原生模块需要重新编译。这四关过了,大版本升级的风险就控住了大半。剩下的是观察期,重点看内存曲线和错误日志里有没有新出现的不兼容报错。
我个人在实际操作中的体会是,Red Hat 上装 Node.js 这件事真正的难点从来不在装,而在“装完之后它归谁管”。系统仓库装的包跟着系统补丁走,第三方仓库装的要自己盯上游,nvm 装的基本没人管。把版本来源和升级责任人先定下来,比研究哪条命令更快有价值得多。还有个小技巧分享:每次动完系统级的 Node 环境,顺手把node -v、npm -v、which node三条的输出记进变更单,下次出问题回头一比对,能立刻判断是不是环境被动过,比翻半天的操作日志快得多。