08 · 部署与运维
文档定位:给出从零到可运行环境的完整搭建步骤,以及进程编排、环境变量隔离、健康检查、备份与故障排查的运维约定。
关联文档:
docs/adr/0007-node-toolchain-nvm-pnpm-pm2.md、docs/01-系统总体方案.md、docs/04-MCP安全网关设计.md、docs/09-原型验证方案.md
1. 工具链:NVM → PNPM → PM2
NVM ──► PNPM ──► PM2
管理 Node 版本 安装依赖 守护 Node 进程三层各自有唯一权威配置,环境因此可复现:
| 层 | 权威文件 | 作用 |
|---|---|---|
| NVM | .nvmrc | 固定 Node 版本 |
| PNPM | pnpm-workspace.yaml + pnpm-lock.yaml | workspace 定义与依赖锁定 |
| PM2 | deploy/ecosystem.config.cjs | 进程拓扑、重启策略、环境变量、日志路径 |
1.1 Node 版本选择(关键)
OpenClaw 官方要求:Node 24.15+(推荐),或 Node 22 LTS ≥ 22.22.3,或 Node 25.9+。
本机现状核对:
| 本机 Node | 版本 | 是否满足 |
|---|---|---|
| 托管版本 | 22.22.2 | ❌ 低于 22.22.3 下限 |
| 系统版本 | 22.23.1 | ✅ |
决策:统一使用 Node 24.x(≥24.15),理由见 ADR-0007。
# .nvmrc 内容
24.15.0# 安装并切换
nvm install 24.15.0
nvm use # 自动读 .nvmrc
node -v # 应输出 v24.15.0⚠ 坑位记录:
nvm use只影响当前 shell。PM2 守护的进程继承的是 PM2 守护进程启动时的 Node 版本。切换 Node 版本后必须pm2 kill再重新pm2 start,否则守护进程仍用旧版本 Node 运行——而旧版本可能不满足 OpenClaw 下限,表现为启动即报错。
1.2 PNPM
corepack enable
corepack prepare pnpm@latest --activate
pnpm -v仓库为 monorepo:
# pnpm-workspace.yaml
packages:
- 'packages/*'
- 'apps/*'pnpm install --frozen-lockfile # CI / 部署环境用
pnpm -r build # 递归构建约定:只用 pnpm-lock.yaml,不引入 npm/yarn 的 lockfile。
1.3 PM2
pnpm add -g pm2
pm2 -v2. 进程拓扑与编排
2.1 进程清单
| PM2 进程名 | 脚本 | 作用 | 启动顺序 |
|---|---|---|---|
mcp-gateway | packages/mcp-gateway/dist/index.js | MCP 安全网关(安全边界) | 1 |
business-api | packages/business-api/dist/index.js | 业务接口服务 | 2 |
openclaw | openclaw gateway | Agent 运行时 + 安全管控插件层 | 3 |
web(可选) | Nginx 或静态服务 | Web 控制台 | 任意 |
顺序理由:插件层的 plugin:init 会做网关健康检查;网关不可达则插件拒绝启用(fail-closed)。因此网关必须先起。
2.2 deploy/ecosystem.config.cjs
// deploy/ecosystem.config.cjs
// 注意:本文件入库,但 env_file 指向的文件不入库(见 .gitignore)
const path = require('path');
const ROOT = path.resolve(__dirname, '..');
module.exports = {
apps: [
{
name: 'mcp-gateway',
cwd: ROOT,
script: 'packages/mcp-gateway/dist/index.js',
instances: 1,
exec_mode: 'fork',
env_file: 'deploy/env/mcp-gateway.env',
env: { NODE_ENV: 'production' },
max_memory_restart: '512M',
autorestart: true,
max_restarts: 10,
restart_delay: 3000,
kill_timeout: 10000,
error_file: 'logs/mcp-gateway-error.log',
out_file: 'logs/mcp-gateway-out.log',
merge_logs: true,
time: true,
},
{
name: 'business-api',
cwd: ROOT,
script: 'packages/business-api/dist/index.js',
instances: 1,
exec_mode: 'fork',
env_file: 'deploy/env/business-api.env',
env: { NODE_ENV: 'production' },
max_memory_restart: '512M',
error_file: 'logs/business-api-error.log',
out_file: 'logs/business-api-out.log',
merge_logs: true,
time: true,
},
{
name: 'openclaw',
cwd: ROOT,
script: 'openclaw',
args: 'gateway',
instances: 1,
exec_mode: 'fork',
env_file: 'deploy/env/openclaw.env',
env: { NODE_ENV: 'production' },
max_memory_restart: '1G',
autorestart: true,
error_file: 'logs/openclaw-error.log',
out_file: 'logs/openclaw-out.log',
merge_logs: true,
time: true,
},
],
};注意:PM2 的
env_file会加载文件中的变量。若某进程需要"显式不继承某变量",可在该 app 的env中把它设为空串——但更可靠的做法是从不在任何 env 文件中写这些变量。见 §3。
2.3 常用命令
pm2 start deploy/ecosystem.config.cjs # 启动全部
pm2 start deploy/ecosystem.config.cjs --only mcp-gateway
pm2 restart openclaw # 重启单个(插件改动后必须)
pm2 list # 查看全部进程状态
pm2 logs mcp-gateway --lines 200 # 查看日志
pm2 monit # 实时监控
pm2 save # 保存当前进程列表
pm2 startup # 生成开机自启脚本(Linux)Windows 开机自启:pm2 startup 在 Windows 不适用,需用 pm2-windows-startup 或任务计划程序配置开机运行 pm2 resurrect。
3. 环境变量隔离(安全核心)
这是本系统最重要的运维约定。 凭证按进程最小化分配,deploy/env/ 下每个文件只包含该进程需要的变量。
3.1 凭证分配矩阵
| 变量 | mcp-gateway | business-api | openclaw(含插件层) |
|---|---|---|---|
BUSINESS_DB_URL | ✅ | ✅ | ❌ 禁止 |
AI_DB_URL | ✅ | ❌ | ❌ 禁止 |
MQTT_URL / MQTT_USER / MQTT_PASS | ✅ | ❌ | ❌ 禁止 |
AGENT_TOKEN_SECRET(签发用) | ✅ | ❌ | ❌ 禁止 |
JWT_PUBLIC_KEY | ✅ | ❌ | ❌ |
JWT_PRIVATE_KEY | ❌ | ✅ | ❌ |
GATEWAY_MCP_ENDPOINT | ❌ | ✅ | ✅ |
AGENT_TOKEN(运行时令牌) | ❌ | ❌ | ✅ |
AGENT_ID | ❌ | ❌ | ✅ |
SECURITY_POLICY_PATH | ❌ | ❌ | ✅ |
OPENCLAW_GATEWAY_TOKEN | ❌ | ❌ | ✅ |
这张表是 docs/09-原型验证方案.md 中"凭证隔离验证"用例的判定依据。
3.2 文件示例
# deploy/env/mcp-gateway.env
NODE_ENV=production
PORT=7801
BUSINESS_DB_URL=file:./data/business.sqlite
AI_DB_URL=http://127.0.0.1:8000
MQTT_URL=mqtt://127.0.0.1:1883
MQTT_USER=gw-svc
MQTT_PASS=<从密钥管理注入>
AGENT_TOKEN_SECRET=<从密钥管理注入>
JWT_PUBLIC_KEY_PATH=./keys/jwt.pub.pem
LOG_LEVEL=info# deploy/env/business-api.env
NODE_ENV=production
PORT=7802
BUSINESS_DB_URL=file:./data/business.sqlite
JWT_PRIVATE_KEY_PATH=./keys/jwt.pem
JWT_PUBLIC_KEY_PATH=./keys/jwt.pub.pem# deploy/env/openclaw.env
NODE_ENV=production
GATEWAY_MCP_ENDPOINT=http://127.0.0.1:7801
AGENT_ID=ops-analyst
AGENT_TOKEN=<由网关签发,或由启动脚本拉取>
SECURITY_POLICY_PATH=./packages/security-plugin/policy.json
OPENCLAW_GATEWAY_TOKEN=<从密钥管理注入>
# ── 以下变量在此文件中永远不得出现 ──
# BUSINESS_DB_URL / AI_DB_URL / MQTT_* / AGENT_TOKEN_SECRET3.3 校验脚本
提供 deploy/scripts/check-env-isolation.sh,在部署后自动核对:
#!/usr/bin/env bash
# 校验 openclaw 进程环境中不含业务凭证
set -euo pipefail
FORBIDDEN='BUSINESS_DB_URL|AI_DB_URL|MQTT_URL|MQTT_USER|MQTT_PASS|AGENT_TOKEN_SECRET'
if grep -Eq "$FORBIDDEN" deploy/env/openclaw.env 2>/dev/null; then
echo "❌ openclaw.env 含禁止的凭证变量:"
grep -En "$FORBIDDEN" deploy/env/openclaw.env
exit 1
fi
echo "✅ 凭证隔离校验通过"同时提供运行期核对(用 PM2 的进程环境做实测,比查文件更有说服力):
# 查看 PM2 中 openclaw 进程的实际环境变量(用于答辩截图)
pm2 env <pm2-id-of-openclaw>4. 从零搭建步骤
4.1 前置组件
| 组件 | 版本 | 安装方式 |
|---|---|---|
| NVM | — | 见官方安装说明 |
| Node | 24.15+ | nvm install 24.15.0 |
| PNPM | latest | corepack enable |
| PM2 | latest | pnpm add -g pm2 |
| Mosquitto | 2.x | 系统包管理器 / 官方安装包 |
| OpenClaw | 锁定版本 | pnpm add -g openclaw@<锁定版本>(不用 @latest) |
| Chroma 或 Qdrant | — | 原型期用 Chroma 本地持久化 |
| STM32 工具链 | — | 见 docs/07-STM32边缘终端设计.md |
4.2 步骤清单
# ① 环境
nvm install 24.15.0 && nvm use
node -v # 期望 v24.15.x
corepack enable && pnpm -v
# ② 依赖
pnpm install --frozen-lockfile
pnpm -r build
# ③ 数据库初始化(双库,两套独立文件)
pnpm --filter mcp-gateway run db:migrate:business # → data/business.sqlite
pnpm --filter mcp-gateway run db:migrate:ai # → data/ai.sqlite / chroma/
pnpm --filter mcp-gateway run db:seed # 内置管理员、示例设备
# ④ 知识库入库
pnpm --filter mcp-gateway run kb:ingest -- ./fixtures/manuals
# ⑤ 密钥与令牌
openssl genrsa -out keys/jwt.pem 2048
openssl rsa -in keys/jwt.pem -pubout -out keys/jwt.pub.pem
# 写入 deploy/env/*.env(这些文件不入库)
# ⑥ Broker
mosquitto -c deploy/mosquitto/mosquitto.conf -d
mosquitto_passwd -c deploy/mosquitto/passwd gw-svc
mosquitto_passwd deploy/mosquitto/passwd stm32-node-001
# ⑦ 启动服务(注意顺序)
pm2 start deploy/ecosystem.config.cjs --only mcp-gateway
curl -s http://127.0.0.1:7801/health | jq # 期望 ok:true, auditWritable:true
pm2 start deploy/ecosystem.config.cjs --only business-api
pm2 start deploy/ecosystem.config.cjs --only openclaw
# ⑧ 安装并启用安全管控插件层
openclaw plugins install --link ./packages/security-plugin
openclaw plugins enable security-plugin
openclaw gateway restart
openclaw plugins inspect security-plugin --runtime --json # 关键验证
# ⑨ 安全基线自检
openclaw security audit
openclaw security audit --deep
# ⑩ 持久化
pm2 save4.3 部署后必做的三项验证
# 1) 凭证隔离
bash deploy/scripts/check-env-isolation.sh
pm2 env <openclaw-pm2-id> | grep -E 'BUSINESS_DB|AI_DB|MQTT' || echo "✅ 未发现业务凭证"
# 2) 插件与钩子已注册
openclaw plugins inspect security-plugin --runtime --json
# 3) 审计可写
curl -s http://127.0.0.1:7801/health | jq '.checks.auditWritable'5. 健康检查与告警
| 对象 | 检查方式 | 判定 |
|---|---|---|
| 网关 | GET /health | ok:true 且 checks.auditWritable:true |
| OpenClaw Gateway | openclaw gateway status --deep --require-rpc | 返回 RPC 正常 |
| 业务接口服务 | GET /health | ok:true |
| Broker | mosquitto_sub -t '$SYS/broker/uptime' -C 1 | 有输出 |
| 进程存活 | pm2 list | 无 errored / 重启次数异常增长 |
关键告警项:auditWritable: false。此时网关应主动进入拒绝模式,运维需立即处理(通常是 business-db 文件权限或磁盘满)。
6. 备份与数据管理
| 对象 | 备份方式 | 频率 | 说明 |
|---|---|---|---|
business-db | sqlite3 data/business.sqlite ".backup data/backup/business-$(date +%F).sqlite" | 每日 | 用 .backup 命令而非直接复制文件(WAL 模式下复制可能不一致) |
ai-db 向量数据 | 复制 Chroma 持久化目录 | 每周 | 可从原始手册重建,优先级低 |
| 审计日志 | 随 business-db 一起 | 每日 | 不得删除,是答辩证据 |
| 配置与 env | 手工归档到密钥管理 | 变更时 | deploy/env/* 不入库,需单独备份 |
| 日志 | logs/ 保留 30 天 | — | 由 PM2 的 pm2-logrotate 模块轮转 |
pm2 install pm2-logrotate
pm2 set pm2-logrotate:max_size 50M
pm2 set pm2-logrotate:retain 307. 故障排查
| 现象 | 可能原因 | 排查步骤 |
|---|---|---|
| OpenClaw 启动即报 Node 版本错误 | PM2 守护进程用了旧 Node | pm2 kill → nvm use → pm2 start |
| 插件加载失败 | openclaw.compat.pluginApi / minHostVersion 与实际版本不符 | 查实际版本,改正清单字段;openclaw plugins inspect <id> |
--runtime --json 看不到钩子 | 钩子事件名与版本不符,或插件未重启 | openclaw gateway restart;核对事件名 |
所有工具调用返回 AUDIT_UNAVAILABLE | business-db 不可写(权限/磁盘满/文件锁) | curl /health 看 auditWritable;检查 data/ 权限与磁盘 |
| Agent 调不到 MCP 工具 | 工具名带前缀,tools.allow 写错 | 查运行时工具清单;tools.deny:["*"] + 精确 allow |
| 遥测不落库 | Broker 认证失败 / 主题不匹配 / deviceId 未登记 | mosquitto_sub -t 'iot/+/telemetry' -v 观察;查网关日志 |
| 网关端口被占用 | 7801 冲突 | netstat -ano | findstr 7801 |
pnpm install 报 engine 不兼容 | Node 版本不符 | 核对 .nvmrc |
| Agent 上下文膨胀、推理变慢 | 遥测返回行数过多 | 检查 limit 与插件层 R12 体积控制是否生效 |
8. 落地期演进方向
原型期刻意保持简单,落地期需要补齐的部分记录在此,作为答辩"后续工作"章节的素材:
| 项 | 原型期 | 落地期 |
|---|---|---|
| 进程编排 | PM2(单机) | Docker Compose / K8s |
| 数据库 | SQLite 双文件(逻辑隔离) | PostgreSQL + Qdrant 两套独立实例(物理隔离) |
| 网关实例数 | 1(fork) | 多实例 + 负载均衡(注意审计写入的并发) |
| 审计存储 | 同库同表 | 独立实例 + 按月分区 |
| 令牌 | 静态短时效令牌 | 网关按会话动态签发 + 刷新 |
| 密钥管理 | .env 文件 | Vault / KMS |
| 终端安全 | 用户名密码 + 可选 TLS | 双向 TLS + 一机一密 + 固件签名与安全启动 |
| 可观测性 | PM2 日志 + 轻量指标 | OpenTelemetry + 集中式日志 |
| 多租户 | 不支持(框架限制,见 ADR-0003) | 按信任边界拆分独立 Gateway |