Skip to content

DevOps 新手 Node.js / Express BFF 生产部署指南

English · GitHub source English version: README.md

本指南在 Linux VM 上部署通用 Express BFF:PM2 cluster 以非特权账户运行多个 Node.js Worker;可选且独立运维的 Nginx 或 OpenResty 网关转发公网流量。所有 <placeholder> 必须通过批准的变更流程替换。本文是可复用基线,不证明任何生产服务使用这些路径、端口或 Worker 数。

目录

心智模型与边界

浏览器 -> 外部 Nginx/OpenResty -> Express BFF(PM2 cluster)
                                    -> 已批准的下游 HTTP 服务

Node.js 在浏览器外运行 JavaScript;Express 是 Web 框架;BFF 是面向浏览器的后端,可处理 Session/授权、请求适配和下游 HTTP 调用。它不等于 OpenResty、Nginx 或下游 API。

PM2 cluster Worker 共享一个私有监听端口,因此必须无状态:不能把 Session、上传或权威业务数据保存在某个 Worker 内存里,应使用已批准的共享服务。网关可能在另一台主机,不能从本文推断是否共置。

准备发布主机

选择并在变更单记录受支持 Node.js LTS 的精确补丁版本;生产不能使用 EOL 或仅 Current 的版本。通过组织批准的软件源安装 Node,随后确认实际二进制。

node --version
npm --version
command -v node
command -v npm

创建专用服务账户与发布目录。该账户可读取应用与受限环境文件,但没有 root 权限。

sudo groupadd --system <app-group>
sudo useradd --system --gid <app-group> --home-dir /srv/<app-name> --create-home \
  --shell /bin/bash <app-user>
sudo install -d -o <app-user> -g <app-group> -m 0750 \
  /srv/<app-name>/releases /srv/<app-name>/shared /srv/<app-name>/shared/logs \
  /etc/<app-name>
sudo install -o root -g <app-group> -m 0640 /dev/null /etc/<app-name>/production.env

系统账户密码保持锁定,且不得授权直接 SSH 登录。敏感值只进入批准的密钥交付机制或 /etc/<app-name>/production.env,绝不可提交。该文件使用 KEY=value 格式,不是 JavaScript;变更记录中只写属主、权限和所需键名。

创建可运维的应用

开发者通常提供已测试、含 package.jsonpackage-lock.json 的制品。npm ci 要求二者匹配且不会改写它们,因此是部署安装命令。不要在服务器运行 npm install 来“修复”发布包。

最小应用契约是私有 GET /healthz 和优雅退出。以下教学代码没有认证和业务路由;真实 BFF 授权必须由应用代码与评审负责。

// app.js
const express = require('express');
const app = express();
app.get('/healthz', (_req, res) => res.status(200).type('text').send('ok\n'));
const server = app.listen(process.env.PORT, '127.0.0.1');

function stop(signal) {
  server.close(() => process.exit(0));
  setTimeout(() => process.exit(1), Number(process.env.SHUTDOWN_TIMEOUT_MS || 30000));
}
process.on('SIGINT', () => stop('SIGINT'));
process.on('SIGTERM', () => stop('SIGTERM'));

每个应用使用一个 ecosystem 文件。以下是安全示例,不是容量建议;instances、内存阈值和超时必须由批准的压测与主机资源决定。

// ecosystem.config.cjs
module.exports = {
  apps: [{
    name: '<app-name>',
    script: './app.js',
    cwd: '/srv/<app-name>/current',
    instances: 2,
    exec_mode: 'cluster',
    env: { NODE_ENV: 'production', PORT: '3000' },
    node_args: '--env-file=/etc/<app-name>/production.env',
    min_uptime: '10s', max_restarts: 5, restart_delay: 5000,
    listen_timeout: 10000, kill_timeout: 30000,
    max_memory_restart: '512M', watch: false, time: true,
    out_file: '/srv/<app-name>/shared/logs/out.log',
    error_file: '/srv/<app-name>/shared/logs/error.log', merge_logs: true
  }]
};

NODE_ENV=production 避免 Express 返回开发式错误页面。PM2 正常 reload/stop 时先发送 SIGINT,应用必须在 kill_timeout 前关闭监听器和已批准依赖。生产 release 目录不能启用 PM2 watch

使用 PM2 部署与托管

从批准的流水线取得制品,按要求验证发布标识/校验和,创建新的不可变 release 目录。新版本未通过本地检查前不能覆盖 current

sudo -u <app-user> install -d -m 0750 /srv/<app-name>/releases/<release-id>
sudo -u <app-user> tar -xzf <approved-artifact>.tar.gz -C /srv/<app-name>/releases/<release-id>
sudo -u <app-user> sh -c 'cd /srv/<app-name>/releases/<release-id> && npm ci --omit=dev'
sudo -u <app-user> sh -c 'cd /srv/<app-name>/releases/<release-id> && npm run test --if-present'
sudo -u <app-user> sh -c 'cd /srv/<app-name>/releases/<release-id> && node --check app.js'

替换 app.js 前先确认批准制品的真实启动文件;不能假定 package.jsonmain 就是生产入口。

<app-user> 安装批准版本 PM2,并仅首次创建 OS 启动集成。以服务账户运行 pm2 startup,审查后仅执行它输出的那条特定特权命令;最后保存已知进程清单。Node 二进制路径变更后必须重新设置启动脚本。

sudo -iu <app-user> npm install --global pm2@<approved-pm2-version>
sudo -iu <app-user> pm2 startup
# 审查后执行上一条命令打印出的精确 sudo 命令。
sudo -iu <app-user> pm2 save

发布、验证与回滚

先采集只读基线:

sudo -iu <app-user> pm2 status
sudo -iu <app-user> pm2 describe <app-name>
readlink -f /srv/<app-name>/current
curl --fail http://127.0.0.1:3000/healthz

本地制品检查成功后,原子切换 current 到新 release,再启动或 reload ecosystem。reload 用于 cluster 应用;若 Worker 无法 ready,PM2 可能退化为 restart,因此必须观察状态、日志和真实请求。

sudo -u <app-user> ln -sfn /srv/<app-name>/releases/<release-id> /srv/<app-name>/current
# 仅首次部署:
sudo -iu <app-user> pm2 start /srv/<app-name>/current/ecosystem.config.cjs --only <app-name>
# 已存在的 cluster 应用:
sudo -iu <app-user> pm2 reload /srv/<app-name>/current/ecosystem.config.cjs --only <app-name>
sudo -iu <app-user> pm2 save
sudo -iu <app-user> pm2 status
sudo -iu <app-user> pm2 logs <app-name> --lines 100 --nostream
curl --fail http://127.0.0.1:3000/healthz

只有私有健康检查、网关请求、已批准代表性用户流程、错误率和 release 标识都满足变更标准后,才能继续发布。失败时停止扩大范围、保留日志、将 current 指回 <known-good-release-id>、reload,并重复相同验证。观察期内不得删除已知正常 release。

网关集成与日常运维

本单机例子将 BFF 仅绑定 127.0.0.1:3000。外部网关架构应替换为获批准的私有接口与网络策略;不得把未认证的开发监听器直接暴露到 Internet。

配置网关请使用现有 Nginx 指南OpenResty 指南。网关 upstream 必须匹配 BFF listener;保留 HostX-Forwarded-ForX-Forwarded-Proto,且仅在流量来自受批准网关时信任这些 header。

日常只读检查:

sudo -iu <app-user> pm2 status
sudo -iu <app-user> pm2 logs <app-name> --lines 100 --nostream
curl --fail http://127.0.0.1:3000/healthz
df -h /srv/<app-name> /var/log
df -i /srv/<app-name> /var/log
free -h

监控请求成功率/延迟、网关 5xx、BFF 5xx、Worker 重启、内存、CPU、应用可提供的 event-loop delay、下游失败、磁盘/inode、日志增长和 release 版本。SLO、内存阈值不能从本文照抄,应由服务 owner 与实测决定。

验收清单

  • [ ] Node 是受支持 LTS 的精确补丁版本;PM2 是 <app-user> 所有的批准版本。
  • [ ] release 使用 npm ci、已提交 lockfile、非 root 属主和受限环境文件。
  • [ ] PM2 cluster Worker 无状态、watch 已关闭,且启动恢复已在受控重启后验证。
  • [ ] 已验证私有 /healthz、批准网关路径、代表性流程、日志、错误和发布版本。
  • [ ] 已知正常 release 和获批准的回滚负责人仍可用。
  • [ ] 未公开凭据、真实内部地址、客户数据或完整进程参数。

官方资料