# node部署
30 秒速记
- 部署 = 环境可复现 + 进程有人管 + 发布能回滚,不是
git pull完node app.js - 版本锁死:
.nvmrc/engines固定Node版本,yarn.lock/package-lock.json配npm ci装依赖 - 进程交给
PM2/systemd/ 容器平台托管:崩溃自动拉起,开机自启,日志统一收 - 配置和密钥走环境变量在运行时注入,不打进镜像也不进仓库
- 应用自己要做三件事:
/health健康检查、收到SIGTERM优雅退出、启动失败返回非0退出码
Node 部署说白了要解决三个问题:线上跑的环境和本地一致、进程挂了有人拉起来、发布出问题能马上退回去。 环境一致靠锁版本,Node 版本写进 .nvmrc 或 Docker 基础镜像,依赖用 npm ci 严格按锁文件装。进程托管交给 PM2、systemd 或者 K8s,别用 nohup 凑合。应用这边我一般会补一个 /health 接口、处理 SIGTERM 做优雅退出,配置缺失时直接 process.exit(1),让发布脚本能发现失败并回滚,而不是带着坏配置继续跑。
理解「node部署」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。
工程中还要验证退出码、资源清理和异常输入。属于 module 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。
一次靠谱的 Node 服务发布,大致是下面这条线,每一步都能回答「失败了怎么办」:
- 构建阶段:在固定版本的镜像里
npm ci、跑测试、打包,产物只带运行要用的文件(dist、生产依赖),devDependencies不进生产镜像。 - 启动阶段:从环境变量读配置,启动时就校验,缺了直接退出。
// config.js:启动时校验,缺什么一眼看出来
const required = ['DATABASE_URL', 'JWT_SECRET']
const missing = required.filter((k) => !process.env[k])
if (missing.length) {
console.error('缺少环境变量:', missing.join(', '))
process.exit(1) // 非 0 退出码,发布脚本 / PM2 / K8s 都能识别为失败
}
- 托管阶段:交给
PM2、systemd或容器平台,负责崩溃重启、开机自启、日志收集。 - 切流阶段:新实例
/health返回200之后再把流量切过去,旧实例收到SIGTERM后停止接新请求、处理完手头请求再退出。 - 回滚阶段:保留上一版镜像或上一版代码目录,健康检查不过就切回去,回滚要能一条命令搞定。
单机小项目 PM2 足够;多机或者已经用上容器的,直接用 Docker + 编排平台,PM2 在容器里的意义就不大了,因为重启和多副本平台都管了。
面试官追问
追问 1配置文件缺了一个数据库地址,服务照样启动成功了,有什么问题?
它会在第一个请求进来时才炸,发布流程却已经判定成功、旧版本也下线了。启动阶段就校验必需配置,缺了直接 process.exit(1),让 PM2 / K8s / 发布脚本拿到非零退出码,自动停住发布。
追问 2本地 node app.js 能跑,服务器上一启动就报语法错误,先查什么?
先看两边 node -v。最常见的是服务器 Node 版本太老,不认识可选链 ?.、顶层 await 这类语法。在 package.json 写 "engines": { "node": ">=18" },镜像也固定成同一个大版本。
追问 3为什么服务器上装依赖要用 npm ci 而不是 npm install?
npm ci 严格按 package-lock.json 装,锁文件和 package.json 对不上会直接报错,而且会先删掉 node_modules。npm install 可能顺手升级小版本,今天和昨天装出来的东西不一样,线上问题就很难复现。
追问 4服务一直重启,日志里只有一行启动成功,怎么判断是谁杀的?
先看托管方的记录:pm2 describe 里的重启次数和退出码,K8s 看 kubectl describe pod 里是不是 OOMKilled 或健康检查失败。退出码 137 一般是被 SIGKILL,多半是内存超限,应用日志里当然什么都没有。
追问 5日志写到容器里的 logs/app.log 有什么坏处?
容器一重建日志就没了,多实例时还得挨个进容器翻。容器里直接写 stdout / stderr,交给 docker logs 或日志采集组件统一收;非要写文件也要挂卷加轮转。
# 一、forever
30 秒速记
forever是最早一批Node进程守护工具:终端 /SSH断开进程不死,崩溃了自动重启- 常用命令:
forever start app.js/forever stop app.js/forever list,-w监听文件变动重启 - 和
nohup的区别:nohup只管断开终端不死,进程崩了就真没了 - 现在基本不维护了,新项目选
PM2或systemd/ 容器平台;它没有集群模式和日志聚合
forever 就是一个保活工具,让 Node 进程脱离终端在后台跑,挂了自动拉起来。 以前在服务器上直接 node app.js,SSH 一断进程就跟着退了,forever start app.js 解决的就是这个问题,外加崩溃重启。它的能力也就到这儿了,多进程、日志聚合、监控面板都没有,而且项目已经很久没怎么更新。现在我新项目不会再选它,单机用 PM2,有容器就交给平台。
forever则可以在cmd或ssh连接断开时,让项目一直运行,而且可以在项目崩溃时自动重启
- 安装
npm install -g forever forever的帮助手册forever --help- 使用
forever启动项目forever start app.js - 使用
forever停止项目forever stop app.js - 列出所有通过
forever管理的项目forever list - 监视项目中的文件,当文件有变动时重启项目
forever -w start app.js
面试官追问
追问 1nohup node app.js & 不也能让进程在后台一直跑吗?
能在后台跑,但进程一旦因为未捕获异常退出,就没人管了,第二天用户告诉你网站打不开。forever / PM2 多做的就是「监视进程,挂了重新拉起」这一步。
追问 2forever 显示服务一直在重启,用户请求总断,怎么查?
先在前台直接 node app.js 跑一下,多半一启动就抛错,比如端口被占、配置缺失,forever 只是在机械地反复拉起。再看重启是不是和文件改动同步,开了 -w 又往项目目录写日志,就会自己触发自己。
追问 3生产环境能开 forever -w 吗?
一般不开。线上代码靠发布流程更新,不需要监听文件;而且日志、上传目录如果在监听范围内,写一次文件就重启一次,请求会被莫名其妙地掐断。
追问 4一台机器上跑了好几个项目,入口都叫 app.js,forever stop app.js 会出什么事?
很容易停错。按 forever list 里的序号或者启动时用 --uid 起个名字,再按名字停,比按文件名可靠得多。
追问 5老项目一直用 forever,要不要迁到 PM2?
需要多实例、日志切割、内存超限重启的话就迁,成本不高,就是换个启动命令加一份 ecosystem.config.js。只是单进程保活、跑得好好的内部工具,没必要为换而换。
# 二、pm2
30 秒速记
PM2= 进程守护 + 集群模式 + 日志 + 监控,单机部署Node的事实标准- 日常命令:
pm2 start/list/logs/monit/restart/reload/delete -i max(或-i 0)按CPU核数开cluster进程;reload逐个重启做到不停机,restart是一起重启- 配置写进
ecosystem.config.js:watch/ignore_watch、max_memory_restart、env、日志路径 - 坑:改了配置文件再
start不会生效,要delete后重新start,或者reload ... --update-env
PM2 是 Node 的进程管理器,负责把应用跑起来、挂了拉起来、多核机器上开多个进程,再顺便把日志和监控管了。 一般我会把配置写进 ecosystem.config.js,pm2 start ecosystem.config.js 一把启动。线上发版用 pm2 reload,在 cluster 模式下它会一个个进程轮着重启,始终有进程在接请求;restart 是全部杀掉再起,会有一小段不可用。还有个常见坑,改了配置文件后直接 pm2 start 进程会沿用旧配置,要先 pm2 delete 再启动。
