项目矩阵 平台基建 Omvp-deployer
OOttor 主维护 · 裸金属部署控制平面 · 平台基建

mvp-deployer

把"SSH 一条龙上线"包成一个 HTTP API · 13 个 MVP 的统一发布控制平面

AI agent 造出来的 MVP 要上线,过去是手动 SSH 一条龙:scp 代码、pnpm i && build、手写 PM2 ecosystem、改 Caddyfile reload — 一个项目 20 分钟,改完一个域名另一个 reload 后 404,忘填 host 还会把别人的线上路由静默改没。mvp-deployer 把这条龙收成一个 zip-only 的异步 API:业务方(含 agent)POST 一个 zip + manifest,拿 202 + taskId 轮询,13 phase 管线 snapshot → … → smoke → done 全自动跑完,Caddy 动态注入路由 + 自动 TLS,三层密钥合并注入,失败自动回滚。从"能部署"升级到"部署失败能自愈"。

控制台 deploy.mvp.restry.cn skill bundle skill.mvp.restry.cn Restry/mvp-deployer · private
13
apps 在管
13
phase 自愈管线
34
commits · 41 天
36
node:test 全绿

核心能力 · Five Capabilities

5 件事,业务方再也不用自己上 SSH 做

每个能力先回答"业务方原本要付什么代价",再补一行技术细节。所有能力共享一条铁律:业务方不碰服务器 — 不 SSH、不写 PM2 ecosystem、不改 Caddyfile、不手抄 .env,一切走 POST /api/deploy 一个 zip,网关统一兜底 build、路由、密钥、回滚。

zip 一发即上线
业务方零 SSH
上传一个 zip + manifest,立刻拿 202 + taskId,异步跑完整管线再轮询。
查看详情
业务方不再 scp 代码 + 手跑 pnpm build。一个 multipart POST 上传 zip + manifest,立刻拿 202 + taskId,异步跑完整管线再轮询。
POST /api/deploy · 202 taskId · 500MB 上限
Caddy 动态路由 + TLS
不碰 Caddyfile
网关调 Caddy Admin API 注入反代 127.0.0.1:<port>,证书自动签。
查看详情
业务方不再手改 Caddyfile reload 把别的域名打成 404。网关调 Caddy Admin API 注入 mvp-route-<p> 反代 127.0.0.1:<port>,证书自动签。
caddy.js · :2019 admin · 自动 HTTPS
三层密钥合并注入
不手抄 .env
按优先级合并 manifest.env > vault > ~/.credentials,喂给每个子进程。
查看详情
业务方不再各自维护一份线上 .env。部署时按优先级合并:manifest.env > vault > ~/.credentials/.env 前缀剥离,sanitize 成单一 finalEnv 喂给每个子进程。
mergeEnv · MVP_DEPLOYER__<KEY> · vault export
失败自动回滚
部署炸了能自愈
swap 前 cp -a 快照旧树,smoke 失败 / 丢 host 立即原子换回。
查看详情
业务方不再守着半坏的线上手动救。swap 前 cp -a 快照旧树;smoke 失败 / Caddy 丢 host 立即原子换回 + 改回上游 + pm2 restart
rollback.js · caddyDiff.js · 保留 3 份
GitHub push 即部署
推代码自动上线
webhook 为主、轮询兜底,只动声明 autoDeploy 的项目,白天才跑。
查看详情
业务方不再每次手动触发。webhook(HMAC 验签)为主、git ls-remote 轮询兜底,只对 manifest 声明 autoDeploy 的项目动手,白天 09–23 才跑。
/api/webhooks/github · poll fallback · daytime

架构 · Trigger → Pipeline → 13 managed apps

一张图看清:触发源 → 13 phase 管线 + 安全网 → 在管 app

两条触发路径(agent 手动 POST / GitHub push)汇进同一条 enqueue,异步管线串行跑 13 个 phase,左列是主链路、右列是安全网(snapshot / caddyDiff / smoke / rollback / vault),跑通后由 Caddy 把每个 host 反代到对应 PM2 进程。

Triggers (two paths, one enqueue)
AI agent · GitHub push POST zip+manifest · webhook HMAC · poll
enqueue · 202 taskId
Control Plane · mvp-deployer :9800 (Express + PM2 + Caddy)
主链路 · 13 phase pipeline
lib/builder.js
snapshot → … → smoke → done
extract envLoad install prisma build swap pm2 caddy
异步 · 最多 2 并发 build · phase 逐行写入 task log
安全网 · self-heal
rollback · smoke · vault
任何 swap 后失败 → restore
snapshot caddyDiff smoke rollback vault
host 必填 · 丢 host 即回滚 · vault 缺失仅 WARN
Caddy reverse-proxy · <host> → 127.0.0.1:<port>
Managed apps (13 in production · PM2 procs behind Caddy)
wx
wx-gateway
wxmsg
莆阳网关
design
image-studio
echo
Echo·5 分钟
menshen
门神面板
packhorizon
PackHorizon
shutiao
薯条 world · static
recap
本盘点站 · static
node app · PM2 常驻进程(install + build) static · 纯静态托管(skip build)
这张图说的是:触发到上线的扇出链路 — 两条触发(agent 手动 POST / GitHub push)汇成一条 enqueue,异步管线串行跑 13 phase + 安全网,跑通后 Caddy 把每个 host 反代扇出到 13 个在管 app。

关键里程碑 · Timeline

41 天 · 34 commits · 从"能部署"到"部署能自愈"

4/23 一天把同步老脚本重写成异步管线,4 月底补自助 skill + prefix 密钥注入,5/9 加审计 + IP 白名单两层防御,5/12 v2 灵魂版本一次性补齐 host 必填 + smoke + 回滚 + vault,5/17 接 GitHub 自动部署收尾。★ 标记的是结构性里程碑。

2026-04-23 ★ 里程碑异步构建管线奠基 · 同步老脚本重写成 task store + lib/builder.js + deleteAndStart + wire routes,POST /api/deploy 改返 202 + taskId(02458ec · e93f2a1 · e37a6a7)。
2026-04-23 自助接入闭环:/skill.zip bundle + 安装说明页;pm2 interpreter=none 跑 shell 命令;caddy addRoute 支持自定义 host。
2026-04-23 ~/.credentials/.env 按项目前缀 <PROJECT>__ 自动注入密钥;ecosystem 改 systemd EnvironmentFile 去硬编码;新增 GET /api/inventory 一次拿全 ports/hosts/envKeys 做部署前预检。
2026-04-27 env 值是 object/array 时序列化成 JSON 而非 [object Object](53fa967);上传上限 100MB → 500MB,容纳带 .next/standalone 的大包。
2026-05-09 ★ 里程碑两层防御 + 审计上线 · NDJSON 审计层 + /api/audit;IP allowlist L2 防御(token 前先过白名单);audit-read token 与部署 token 隔离、默认不信 XFF(d09175e · 31358cf · b522231)。
2026-05-12 ★ 里程碑v2 灵魂版本:部署自愈(71abf42manifest.host 必填、删自动 migrate、smoke + 自动回滚caddyDiff 路由守卫、vault 三层密钥合并 — 从"能部署"到"失败能自愈"的质变。
2026-05-12 install-instruction 加 v2 breaking changes 横幅 + IP 白名单说明,业务方 redeploy 前先读破坏性变更。
2026-05-17 ★ 里程碑GitHub 自动部署上线(6c24291)· webhook HMAC 为主 + ls-remote 轮询兜底;auto-deploy env 透传 PM2;轮询改白天 09–23 CST 调度,避免凌晨半坏部署。
2026-05-18 dashboard 加微信登录鉴权(session cookie HMAC + finalize 验签)+ sync-skill 安装指引,控制台不再裸奔。
2026-06-02 修微信登录 ms-epoch 时间戳兼容(秒/毫秒都认)+ sync-skill install guide 收尾(7619663)。

名场面 · 1 个最值钱的坑(深度)

忘填 manifest.host 一个字段,就把别人的线上路由静默改没

这条坑没有报错、没有日志告警 — deploy 显示成功,域名却悄悄 404。修法看似就是"加个必填校验",但它背后是"路由 host 由 deployer 隐式计算 + agent 必然偶尔漏字段"两条约束叠加 — 任何把 host 当"约定"而非"声明"的方案,都注定某次忘填就把生产 host 替换成没人用的默认值。

HALL OF FAME · Caddy Route Gotcha #1

路由 host 不能靠隐式默认:为什么要 host 必填 + caddyDiff 守卫

一句话:忘填 host → deployer 用隐式默认重写 Caddy route → 真实生产 host 被静默踢下线,deploy 却显示成功。修法不是简单加校验,而是把 host 从"约定"升级成"必填声明" + PATCH 后比对守卫,丢 host 即自动回滚。
展开根因 / 修法 / commit / 代码 / 沉淀
症状
某次 deploy 忘传 manifest.host,deployer 用隐式默认 <project>.mvp.restry.cn 重写了 Caddy route,把真实生产 host(如 wx.mvp.restry.cn)静默踢下线。deploy 任务全程 succeeded,但公网访问 404。wx-gateway / wx-gateway-pucs 每次 agent 漏字段就中招一次。
根因
路由 host 的单一真相源是"约定"而非"声明" — deployer 在调用点用 <project>.<baseDomain> 兜底。而触发方是 AI agent,非确定性:它一定会偶尔漏掉 host 字段。在"漏了就用默认值覆盖"的世界里,这不是概率问题,是必然问题 — 只是不知道哪次。
修法
(1) manifest.host 必填,空字符串直接 400 Missing required field: host,删掉隐式默认。(2) caddyDiff 守卫:PATCH 前快照 route 的 host 集合,PATCH 后比对,deploy 前存在、deploy 后消失的 host = 回归 → 自动 rollback。(3) 快照 .caddy-routes.json 存项目目录内、随原子 swap 复制前进;首次部署无快照则 fallback 到 live caddy 查询。
commit
71abf42 v2 灵魂版本一次性落地 host 必填 + caddyDiff + smoke + rollback;配套 caddyDiff.test.js 7 case + manifestHost.test.js 5 case 锁回归。
// lib/caddyDiff.js — PATCH 后比对,丢 host 即让调用方回滚
const before = await snapshotHosts(project)   // 部署前 host 集合
await caddy.upsertRoute(project, manifest.host, port)
const after = await liveHosts(project)         // PATCH 后实测
const lost = before.filter(h => !after.includes(h))
if (lost.length) throw new Error(`caddy regression: lost ${lost}`) // → rollback.restore
沉淀:这条坑和"--preserve-env 经 SSH 抓 .env 失败只 stderr-warn → 生产 .env 被清空"、"自动 prisma migrate 用 mvpadmin 凭据撞 P1000"两个亲戚,统一写进 v2 CHANGELOG 的 breaking + risks 清单 — 凡是把"约定 / 隐式默认 / 静默降级"当安全网的地方,对 agent 触发方都是定时炸弹。沉淀成一条铁律:面向 agent 的字段宁可必填报错,也不要隐式兜底;每条破坏性变更配一个 node:test 锁住,改管线时测试 + skill 文档必须同改。

当前现状 · Status

五个维度,逐条带具体数字 / 文件 / commit / domain

不写"一切正常 / 运行良好"这类空话。下面五个分组每条都对应一个可在 git log / lib / inventory 里 grep 到的真实事实:生产部署 / 在管资产 / 安全态 / 最近变动 / 运维状态。

生产部署

控制台 deploy.mvp.restry.cn :9800,裸金属 Express 单进程 + PM2 + Caddy Admin API,密钥走 systemd EnvironmentFile 注入。
查看 5 条细节
  • 控制台 · deploy.mvp.restry.cn :9800 · Express 单进程,PM2 名 mvp-deployer
  • 密钥注入:systemd EnvironmentFileMVP_DEPLOYER__*,ecosystem 只声明非密配置
  • 运行栈:裸金属 + PM2(interpreter:none 跑 shell)+ Caddy Admin API 动态路由
  • 任务持久化:tasks.json 存最近 50 条 task,重启把 running 标记为 crashed
  • 自助接入:skill.mvp.restry.cn/skill.zip + 安装说明,业务方拉了就能发

在管资产规模

13 个 app 在管,node 常驻 + static 纯托管两类;GET /api/inventory 一把拿全 ports/hosts/envKeys 做部署前预检。
查看 4 条细节
  • 13 个 app 在管,Caddy 反代真实 hosts:wx · wxmsg · design · echo · menshen · packhorizon · cspy · shutiao-world · recap · skill
  • 类型:node 常驻进程(install+build)+ static 纯托管(skip build)两类
  • 预检:GET /api/inventory 一次返回 per-project manifest / PM2 / Caddy route / envKeys / disk / 已用端口 + next_suggested
  • 密文安全:inventory 从不返回 secret 值,仅白名单非敏感 key(如 DATABASE_URL)

安全态(token = host root,层层收口)

token 等价 host root,所以层层收口:L2 IP 白名单先于 token + 审计读写隔离 token + webhook/dashboard 双 HMAC,默认不信 XFF
查看 4 条细节
  • L2 IP 白名单:IP_ALLOWLIST CIDR/IP 在 token 校验之前过滤,离网泄露的 token 直接无效;命中拦截回显 yourIp 便于排查
  • 审计自保:NDJSON 审计层 + /api/audit独立 AUDIT_READ_TOKEN,部署 token 泄露读不到自己的取证日志;默认不信 X-Forwarded-For 防伪造
  • RCE 收口:/api/exec/<project> 等价 host root,正是 IP 白名单 + token 隔离的直接动机
  • 双 token:webhook 走 HMAC-SHA256(与 Bearer 解耦)· dashboard 走 session cookie HMAC + 微信 finalize 验签

最近主要变动(git log)

最近一路收口:v2 自愈(71abf42)→ GitHub 自动部署(6c24291)→ 轮询限白天 → dashboard 微信鉴权 → ms-epoch 时间戳修复
查看 8 条 commit
  • 7619663 fix: handle ms-epoch timestamp in wx login — 微信登录时间戳秒/毫秒都兼容
  • 15cad71 Add dashboard auth and sync-skill install guide — 控制台加微信鉴权
  • 7224b8f Make auto deploy polling daytime scheduled — 轮询改白天 09–23 CST
  • 6c24291 feat: add GitHub auto deploy — ★ webhook + 轮询双触发上线
  • 71abf42 feat(deployer): host-required, no auto-migrate, smoke+rollback, vault — ★ v2 灵魂
  • 31358cf feat(security): IP allowlist (L2 defense) with yourIp echo
  • d09175e feat(audit): NDJSON audit log layer + /api/audit endpoint
  • 17787b1 feat: add GET /api/inventory for full deployment context

运维状态

自愈链 snapshot → caddyDiff → smoke → rollback 已就位,node:test 36 case 本地全绿,回滚保留 3 份,vault 缺失仅 WARN 降级。
查看 5 条细节
  • node:test 36 case 本地全绿:host 校验 5 + caddyDiff 7 + smoke 11 + mergeEnv 4 + vault 6 + credentials,改管线必跑
  • 自愈链已就位:snapshot → caddyDiff → smoke → rollback,smoke 探 https://<host>/ + smokePaths,接受 {200,204,301,302,307,308}
  • 回滚保留 3 份:/opt/mvp-apps/.rollback/<p>-<ts>/,每次新快照剪旧的;失败时换回 + 改回 Caddy 上游 + pm2 restart --update-env
  • vault 容错:探 vault / menshen-vault on PATH,缺失或失败仅 WARN 不阻断,降级用前缀 env
  • auto-deploy 调度:webhook 即时 + 轮询兜底(默认 10 分钟),仅白天窗口跑,避免凌晨半坏部署

未结清债务 · Open Debt

7 条具体待办 · 每条带优先级 / 工作量 / 现状

不写"P1 待办若干"这种模糊罗列。每条 4 列:优先级(P0 立刻 / P1 本周 / P2 排期) · 工作量(S < 0.5d · M 1–2d · L 3d+) · 待办 · 现状是什么(看了就知道离修好还差什么)。来源:CHANGELOG.md v2 risks 清单 / README 已知问题 / 近 34 commits / 实际运维。

P0Lv2 自身尚未 redeploy 上线
现状(还差什么)
host 必填 / smoke / 回滚 / vault 全是 本地 main 上的 commit,服务器仍跑旧版。需人工 review → rsync → 重启 deployer;且每个项目下次 redeploy 才生效,在管 app 仍是旧管线。
P0M13 apps 补齐 manifest.host
现状(还差什么)
v2 上线后没填 host 的项目下次 redeploy 直接 400。需先 jq 'select(.host==null)' /opt/mvp-apps/*/.deploy-manifest.json 全量体检,逐个补 host 再放行。
P1M部署 task log 屏蔽 secret
现状(还差什么)
phase log 逐行写子进程输出,可能带明文 secret/token;/api/audit 也落盘。需在写入前对 *_SECRET|*_TOKEN|*_PASSWORD|*KEY 做 mask,再补一条 selftest 锁住。
P1S纯 API 项目 smoke 适配
现状(还差什么)
smoke 默认探 https://<host>/,只服务 /api/* 的项目首次 v2 redeploy 会 fail。需为这类项目配 smokePaths:["/api/health"]smokeDisabled:true,默认值不背锅。
P1MpostDeploy 迁移逐项 wire
现状(还差什么)
自动 prisma migrate deploy 已从管线删除(撞 P1000)。每个带 schema 的项目要自己在 manifest.postDeploypnpm prisma migrate deploy + 自己的 DATABASE_URL,否则 schema 不同步。
P2Srollback 磁盘水位监控
现状(还差什么)
cp -a 快照让项目树峰值翻倍,× 3 份保留。当前靠人肉确认 /opt/mvp-apps 余量,需加 daily metric,最大项目 × 3 超阈值告警。
P2Svault CLI 输出形态对齐
现状(还差什么)
parseVaultOutput 假设 vault export -p <p> 输出 dotenv 行或 JSON 对象。若 menshen-vault 实际是嵌套对象 / 不同子命令,需对齐 runVaultExport;目前缺失即降级,未真正联调。

技术栈 · Stack

一句话:裸金属 Node + Express + PM2(interpreter:none)跑构建与常驻,Caddy Admin API 动态反代 + 自动 TLS,三层密钥合并注入,GitHub webhook/轮询自动部署,snapshot · caddyDiff · smoke · rollback 自愈,node:test 36 case 把关。
查看分类栈 chips
RuntimeNode ≥18 · Express · PM2 interpreter:none ProxyCaddy Admin API :2019 · 自动 TLS Buildpnpm / yarn / npm · extract-zip · archiver Secrets3-layer merge · systemd EnvironmentFile · vault AutoDeployGitHub webhook HMAC · ls-remote poll SelfHealsnapshot · caddyDiff · smoke · rollback AuthBearer · IP allowlist · audit NDJSON QAnode:test · 36 cases