在服务器上跑 npm ci 四十分钟没装完,我打包把 node_modules 扔了过去

本文首发于 note.sichenai.cc,原文链接:note.sichenai.cc/articles/li... ,作者:斯晨

先说结论。把一个 Node 应用搬到国内小内存服务器上,有三条保命经验:

  1. **服务器只收成品。**npm install、npm ci 这类依赖解析的活,在开发机干完,让服务器直接收整包。
  2. **node_modules 可以整机搬,但原生包要按清单补。**纯 JavaScript 的包随便搬,带平台二进制的包要分「构建期用」和「运行期用」两批另补。
  3. **SSH 会话里启动的后台任务,默认活不过你的断开连接。**要它长命,用 systemd-run。

这三条是我用一个晚上换来的。下面按实际发生的顺序讲,每件事都拆成现象、真因、处置三段,报错原文一律保留,不为「完美教程」修剪。

实测环境:腾讯云轻量应用服务器 2核4G(2026-09 购入,188 元/年档),Ubuntu 26.04,npm 9.2,Docker 29.8.1,大陆家用网络。实测日期 2026-09-20(服务器初始化)与 2026-09-24(应用上机)。

开工前先撞两堵墙

机器到手,第一件事是装 Docker 跑两个自用的中间层容器,结果两堵墙挨个撞上。

**墙一:Docker Hub 拉不动。**国内服务器直连 Docker Hub,镜像拉取长时间无响应最后超时。解法是个梯度,从省事到费事依次试:装 Docker 本体走云厂商内网源;拉镜像配厂商的镜像加速地址;个别加速也拿不到的镜像,在本机下载再传上去。

**墙二:镜像名是错的。**我照着教程写的名字拉镜像,报 not found。到 Docker Hub 一搜才发现教程把名字写错了(rsshub/rsshub 这个仓根本不存在,真名是 diygod/rsshub)。教程会过时,转述会走样,用镜像之前先去 registry 查一眼全名,十秒钟的事,能省半小时。

顺手立了安全基线的规矩:SSH 只留密钥登录、禁 root,公网防火墙只开 22;后来起的每个服务都只绑 127.0.0.1,管理界面走 SSH 隧道看。这一段没有坑,但值得抄。

第一幕:npm ci 跑了四十分钟,我投降了

代码上服务器,按肌肉记忆敲了 npm ci,起身倒水。回来一看,CPU 顶着 100% 已经四十分钟,没跑完。

翻 debug 日志:25 万行,最后停在这一句:

text 复制代码
silly placeDep ROOT next@16.3.6 REPLACE for: @opennextjs/aws

翻译一下。npm 在解析依赖时需要在 next 15 和 next 16 之间反复横跳:我的 package.json 声明 next ^15,而另一个依赖的 peerDependencies 又放行了 next 16。npm 处理这种冲突的方式,是把版本组合挨个试过去,组合数是指数增长的,术语叫版本回溯。开发机上这个爆炸可能几秒就过去了,2 核弱机上每步慢十倍,直接原地去世。

坦白说,我没回头试 --legacy-peer-deps 这类缓解参数,当时的心思是让服务赶紧跑起来,没心思给 npm 会诊。我直接换了条路线,事后看也是更对的路线:让服务器只收成品,不做任何解析。

具体三步:

  1. 代码用 git ls-files 列出受管文件清单,rsync 按清单同步,不碰 node_modules 也不带本地杂物;
  2. node_modules 在开发机打成 tar 包,ssh 一条命令灌过去(791M);
  3. 从此服务器上不出现任何 npm install。

第二幕:依赖搬过去了,缺了几块骨头

整包搬运的隐患马上显形。node_modules 里的包其实分两种。一种是纯 JavaScript 写的,放到哪台机器都能干活。另一种捆着编译好的二进制文件,这些二进制跟操作系统和 CPU 架构绑定:你在 Mac 上装的包,里面的二进制说 macOS 话,搬到 Linux 服务器上就成了哑巴。

这次连炸两回,位置还不重样。

**第一回炸在类型检查。**next build 走到一半报错:

text 复制代码
error: Cannot find module '@tarojs/cli'

这个有意思。我的仓里除了 Next 主应用,还有个小程序子工程(Taro 框架),根目录 tsconfig 的 include 写的是 **/*.ts,把子工程也扫了进来。类型检查要去找子工程的依赖,而 Taro 的包装在 Mac 上,服务器上没有。为什么开发时从没炸过?因为那台 Mac 恰好装过。这种「单机从不暴露」的配置耦合,换台机器准炸。修法是把子工程加进根 tsconfig 的 exclude。

**第二回炸在运行期。**build 过了,定时任务首跑又挂:

text 复制代码
generateBinPath (.../esbuild/lib/main.js)

这回是 esbuild。跑脚本的工具链要靠它,安装时 npm 会按当前平台下对应的二进制,Mac 上装的是 darwin 版,服务器要的是 linux 版。

两回合修法统一:原生包按绑定时机分两批补。

  • 构建期绑定(build 时才用到):@tailwindcss/oxide-linux-x64-gnu、lightningcss-linux-x64-gnu
  • 运行期绑定(服务跑起来每次都要用):@esbuild/linux-x64、better-sqlite3

补法:从 registry 的元数据里查到准确版本的 tarball 直链(用 npmmirror 镜像记得加 -L 跟随 302 跳转),下载解压到 node_modules 里对应位置,版本跟你已有的一模一样。

有个运气成分要交代:better-sqlite3 我原样直传过去居然能用,

bash 复制代码
node -e "require('better-sqlite3')"

实测通过。不同版本的打包策略不一样,别默认它能活,搬完务必跑一句这个验证。

第三幕:后台任务两次无声死亡

依赖齐了,构建也过了,起服务、挂定时任务,然后开始见鬼:SSH 里跑的长任务,过一阵去看,进程没了,日志停在半截,没有报错。

第一反应是内存爆了被系统干掉。dmesg 翻了,没有 OOM 记录;journalctl 翻了,没有任何击杀日志。它就是消失了,连句遗言都没有。

把进程死亡时间和我的操作时间对了一遍,规律出来了:**进程死的时间,跟我断开 SSH 的时间吻合。**我的推断是这样:会话断开时,系统把会话里启动的进程连带清理了。这段机制我没有日志层面的直接证据,算推断;但下面这个解法是实打实验证过的。setsid、nohup 都试过,躲不过。

正解是 systemd-run:

bash 复制代码
sudo systemd-run --uid=ubuntu --unit=migrate-job bash -c 'long-task.sh && touch /tmp/migrate-job.done'

它把任务放进系统作用域,跟你的 SSH 会话从此没关系。任务完没完成,轮询那个 done 标记文件就行。

**花絮一枚,值回票价。**有天晚上想杀掉卡住的 npm ci,我敲了 pkill -f "npm ci",结果自己的 SSH 会话当场断成 exit 255。pkill 的 -f 是对整条命令行做匹配,而我这条 pkill 命令自己身上就写着「npm ci」几个字,成功自杀。一晚上干了两次。正确姿势:先 ps 查 PID,再按 PID 杀。

最后装好的样子

应用本体由 systemd 常驻,定时任务三件套(数据抓取、备份、巡检)挂进 crontab,部署当天首轮抓取全部成功。合上 MacBook,服务照常跑,这台 2 核 4G 的小机器从此就是它的家。

复盘一张表:

坑 症状 真因 处置
服务器跑 npm ci CPU 满载 40 分钟不收敛,debug log 25 万行 peer 依赖版本回溯指数爆炸,弱机放大 依赖解析回开发机,node_modules 打包直传
类型检查炸 Cannot find module '@tarojs/cli' 根 tsconfig include 扫进子工程,其依赖只在 Mac 上有 exclude 加子工程
运行期炸 generateBinPath 报错(esbuild) 平台二进制,Mac 装的是 darwin 版 按构建期/运行期清单补 linux 包
后台任务蒸发 进程消失、日志半截、无 OOM 记录 推断为 SSH 会话断开连带清理(推断,已标注) systemd-run 脱离会话
pkill 自杀 SSH 会话 exit 255 -f 匹配到自身命令行 先查 PID 再杀
Docker Hub 拉不动 拉取超时 国内直连不通 内网源、镜像加速、本机中转,三级梯度
镜像名讹传 pull 报 not found 教程转述的名字是错的 用前去 registry 查全名

我跑通的路径,和你能照抄的路径

三件事说清楚,免得你照着踩坑:

  • **我的环境有特殊性。**镜像加速地址是腾讯云机器专用的,别家云有各自的地址;「构建期/运行期」的原生包清单要按你自己的项目列,我的清单(oxide、lightningcss、esbuild、better-sqlite3)只是长这样的一个样例。
  • **两处没走到底,如实交代。**npm 回溯我没试 --legacy-peer-deps 之类的缓解参数,是直接换的路,没验证过「撑下去」是否可行;better-sqlite3 直传能用有运气成分,你的版本未必。
  • **一处没记录。**791M 打包直传的耗时我没记,家用宽带上行各不相同,你那边多快我给不了参考数。

数据来源与视角

  • 本篇为本人部署实录(2026-09-20 服务器初始化、2026-09-24 应用上机),报错原文未润色
  • 「SSH 会话清理」机制一节为推断,已就地标注
  • 文中价格与版本号为 2026-09 时点
  • 2核4G 足够跑这类单体小应用,这个结论本身也来自本次实测
相关推荐
曦樂~2 小时前
Symmetric Ciphers对称加密
运维·服务器
刚入门的大一新生2 小时前
Linux-Ext系列文件系统
linux·运维·服务器
bmxy小明同学2 小时前
2026-10-11-幻觉治理
服务器·前端·javascript
cpolar技术支持3 小时前
第三方平台回调总说收不到?把本机 Webhook 收单页跑通,用 cpolar 固定二级子域名给对面看实时请求
node.js·内网穿透·webhook·cpolar·回调对接
.Hypocritical.4 小时前
SpringBoot+Vue 前后端分离项目部署到云服务器
服务器·vue.js·spring boot
此生决int5 小时前
Linux系列(06)——进程概念(上)
linux·运维·服务器
房开民5 小时前
通俗总结 Octree(八叉树)
java·服务器·数据库
云飞云共享云桌面6 小时前
智能装备数字化转型:云飞云云桌面助力苏州工厂共享 SolidWorks 算力
运维·服务器·3d·自动化·制造
程序员-Benothing6 小时前
Linux进程优先级与调度:nice、renice、CPU亲和性实战
linux·运维·服务器