系统实践
Node.js 生产部署:从手工 git pull 到可回滚发布
把旧式 Node.js 服务器安装清单升级为一套可维护的生产部署方法:LTS 版本、构建产物、systemd、Nginx、密钥隔离、健康检查与回滚。

本文整合并重写了旧博客中关于 Node.js 生产环境(ID 480)、PHP Webhook 自动部署(ID 76)、自建 Git 服务器(ID 95)、Git Hook 发布(ID 97)和 Ubuntu LEMP 环境(ID 85)的实践。那些文章记录了从 FTP 到 Git 自动化的真实过程,但其中的 Node 12、CentOS 6/7、裸
git pull和 Webhook 执行 Shell 已不适合作为今天的生产方案。
部署 Node.js 的难点从来不是“怎样让进程跑起来”,而是以后怎样安全更新、发现故障和退回上一版。
一套适合个人项目和小型生产系统的基础结构可以很简单:
Git 仓库 → CI 测试与构建 → 不可变发布目录 → systemd → Nginx → HTTPS
↘ 上一版回滚
这套结构没有引入 Kubernetes,也不要求先容器化。它解决的是小型系统最常见的五个问题:版本漂移、进程守护、凭据泄露、发布中断和无法回滚。
生产环境只使用受支持的 Node.js
Node.js 官方建议生产应用使用 Active LTS 或 Maintenance LTS。到 2026 年 7 月,Node 24 和 22 仍处于 LTS,旧文使用的 Node 12 早已结束维护。
不要把“最新版”写死在长期部署脚本里。更可靠的规则是:
- 在项目中声明支持的 Node 主版本,例如
engines.node或版本管理文件。 - CI 与生产使用相同主版本。
- 升级主版本时重新安装依赖、运行测试并做预发布验证。
- 订阅 Node.js 安全发布,避免长期停留在已 EOL 的版本。
nvm 很适合个人开发环境,但 systemd 服务不应依赖交互式 Shell 才能找到 Node。生产机应使用明确、可重复的安装路径,并在单元文件中确认实际执行的二进制:
command -v node
node --version
npm --version
不要让应用以 root 身份运行
为应用创建独立系统用户,只让它读取发布文件和必要配置。应用监听本机高位端口,例如 127.0.0.1:3000;公网的 80/443 交给 Nginx。
目录可以这样组织:
/srv/example/
├── current -> releases/20260717-120000
├── releases/
│ ├── 20260716-180000/
│ └── 20260717-120000/
└── shared/
└── app.env
每次发布创建一个新目录,验证后再切换 current。不要在正在运行的目录中直接 git reset --hard、git clean 和覆盖依赖;旧文中的 PHP Webhook 正是这样做的,一旦构建或拉取中断,线上目录会停在半更新状态。
构建应在发布前完成
CI 的最小顺序是:
npm ci
npm test
npm run build
npm ci 依据锁文件进行干净安装,锁文件不一致时直接失败。对于 TypeScript 或需要打包的应用,发布的是测试过的构建产物,而不是让生产服务器临时决定依赖版本。
产物中应包含运行所需文件,但不能包含:
.env、私钥或云平台长期凭据。- 开发缓存和本机日志。
- 不需要在生产运行的测试数据。
- 来路不明的完整工作目录压缩包。
部署账户只获得写入 /srv/example/releases、切换软链接和重启指定服务所需的最小权限。GitHub Actions 的 production environment 可以限制部署分支、审批和环境 secrets;凭据应使用专用部署密钥或服务账号,而不是个人全权限 Token。
用 systemd 管理进程
一个基础单元文件可以从这里开始:
[Unit]
Description=Example Node.js application
After=network.target
[Service]
Type=simple
User=example
Group=example
WorkingDirectory=/srv/example/current
EnvironmentFile=/srv/example/shared/app.env
ExecStart=/usr/local/bin/node dist/server.js
Restart=on-failure
RestartSec=5s
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
[Install]
WantedBy=multi-user.target
路径要按实际 Node 安装位置和构建入口调整。首次启用前:
sudo systemd-analyze verify /etc/systemd/system/example.service
sudo systemctl daemon-reload
sudo systemctl enable --now example.service
sudo systemctl status example.service
不要为了省事把 Restart=always 当成健康检查。进程反复崩溃再重启只是在制造循环;应结合日志定位根因:
journalctl -u example.service -n 200 --no-pager
EnvironmentFile 也不是秘密保险箱。它仍是磁盘文件,必须限制所有者和权限,不能进入 Git。更高要求的系统应使用平台的 secrets 服务或短期凭据。
Nginx 只代理到本机应用
基础反向代理示例:
upstream example_node {
server 127.0.0.1:3000;
keepalive 32;
}
server {
listen 443 ssl;
server_name example.com;
location / {
proxy_pass http://example_node;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
}
}
如果应用使用 WebSocket,还需要按 Nginx 官方说明显式传递 Upgrade 和 Connection。不要无条件复制 WebSocket 配置到所有站点,也不要让应用盲目信任任意客户端传来的 X-Forwarded-*;信任边界应限定在自己的反向代理。
修改后先测试再重载:
sudo nginx -t
sudo systemctl reload nginx
HTTPS 的签发和续期流程见本站的 Nginx 配置 HTTPS 指南。
发布、健康检查与回滚
一次可回滚发布的顺序应固定:
- CI 测试并生成带提交 ID 的产物。
- 上传到新的 release 目录并校验文件。
- 安装生产依赖或使用已经封装完整的产物。
- 在临时端口执行启动和健康检查。
- 原子切换
current软链接。 - 重启或平滑切换应用进程。
- 从 Nginx 外部请求真实健康地址。
- 保留至少一个已验证的上一版本。
健康端点不应只返回进程“活着”。它至少要确认应用完成初始化;是否检查数据库等下游服务,要避免因为一个非关键依赖抖动造成整个集群雪崩。
回滚时切回上一目录,再重启服务:
sudo ln -sfn /srv/example/releases/20260716-180000 /srv/example/current
sudo systemctl restart example.service
这只能回滚代码。数据库迁移必须单独设计向后兼容和恢复策略:先做兼容性扩展,再发布新代码,最后清理旧字段。不可逆的数据库变更不能靠切软链接解决。
单实例重启仍会有短暂中断。真正的无中断需要至少两个应用实例、负载均衡、就绪探测和连接排空。对于个人项目,先把可靠发布和快速回滚做好,通常比过早搭建复杂集群更有价值。
从旧方案保留下来的真正经验
旧文从 FTP 走向 Git Hook,是自动化意识的一次进步;但“收到 Webhook 就在生产目录执行命令”把网络入口、代码拉取、构建和 root 权限绑在了一起。
今天更稳妥的边界是:CI 负责验证,发布产物不可变,systemd 负责进程,Nginx 负责入口,secrets 离开仓库,回滚不依赖重新拉代码。工具会继续变化,这些边界不会很快过时。
参考:Node.js 发布周期、Node.js EOL 说明、Nginx 反向代理模块、Nginx WebSocket 代理与 GitHub Actions 部署环境。

