系统实践

Node.js 生产部署:从手工 git pull 到可回滚发布

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

· 约 10 分钟阅读 · 更新于 2026年7月17日 · 近 30 天 少于 10 次浏览

Node.js 构建产物经过检查、反向代理和回滚路径部署到两台服务器的山海风格示意图

本文整合并重写了旧博客中关于 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 --hardgit 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 官方说明显式传递 UpgradeConnection。不要无条件复制 WebSocket 配置到所有站点,也不要让应用盲目信任任意客户端传来的 X-Forwarded-*;信任边界应限定在自己的反向代理。

修改后先测试再重载:

sudo nginx -t
sudo systemctl reload nginx

HTTPS 的签发和续期流程见本站的 Nginx 配置 HTTPS 指南

发布、健康检查与回滚

一次可回滚发布的顺序应固定:

  1. CI 测试并生成带提交 ID 的产物。
  2. 上传到新的 release 目录并校验文件。
  3. 安装生产依赖或使用已经封装完整的产物。
  4. 在临时端口执行启动和健康检查。
  5. 原子切换 current 软链接。
  6. 重启或平滑切换应用进程。
  7. 从 Nginx 外部请求真实健康地址。
  8. 保留至少一个已验证的上一版本。

健康端点不应只返回进程“活着”。它至少要确认应用完成初始化;是否检查数据库等下游服务,要避免因为一个非关键依赖抖动造成整个集群雪崩。

回滚时切回上一目录,再重启服务:

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 部署环境