CapRover 后端开发指南:基于 AGENTS.md 的架构分层、兼容性契约与本地验证流水线
CapRover 后端开发指南:基于 AGENTS.md 的架构分层、兼容性契约与本地验证流水线
CapRover 是一个刻意保持"小而精"的 PaaS 控制层,在 Docker Swarm、nginx 与 Let's Encrypt 之上封装常见的部署工作流。本文以仓库根目录下的 AGENTS.md(面向后端开发者与 AI Agent 的工程契约)为骨架,结合 src/ 下真实源码与 tests/ 回归测试,系统讲解 CapRover 后端的目录职责、API v2 兼容性约束、Docker 资源所有权原则,以及从 npm ci 到完整 Jest 套件的本地验证流水线。读完你将掌握 CapRover 后端代码的架构分层方式,并具备在既有兼容性约束下安全新增或修改功能的完整方法。
一、AGENTS.md 是什么:一份给开发者与 Agent 的工程契约
AGENTS.md 位于仓库根目录,篇幅精炼,但承载的是 CapRover 后端最核心的工程共识:产品边界、仓库地图、变更约束与验证流程。它同时服务于两类读者——人类开发者,以及基于本仓库工作的 AI 编程 Agent——因此语言高度指令化,每一条都对应仓库中可验证的实现。
需要说明的是,CapRover 的前端代码维护在独立的 caprover/caprover-frontend 仓库,面向用户的文档维护在 caprover/caprover-website 仓库;本仓库的 src/ 是纯后端实现,也是本文的讨论范围。
二、产品边界:刻意小的控制层,而非工具的镜像
CapRover is a deliberately small control layer over Docker Swarm, nginx, and Let's Encrypt. Optimize common deployment workflows; do not mirror every capability of the underlying tools.
这一条是 AGENTS.md 的开篇,也是理解整个后端设计的钥匙:
- 定位是"控制层":CapRover 只做编排和封装,不做底层的通用替代品。部署、域名、SSL 证书、负载均衡等高频工作流是它的优化对象;
- 不镜像底层能力:Docker Swarm、nginx、Certbot 的高级或冷门能力,不在 CapRover 内重复实现,而是"Prefer existing customization hooks"——优先使用既有的自定义钩子(例如 template/ 下的 nginx EJS 模板、
certbotCertCommandRules自定义证书命令规则); - 变更要窄、要克制:没有具体用例就不新增 API 字段、配置项或抽象;大型或跨模块的功能要先讨论再实现。
从源码看,这种"控制层"姿态体现在 src/app.ts 中:Express 应用只负责挂载 API 前缀、静态资源、健康检查与一个 netdata 反向代理(http-proxy 转发到 captain-netdata-container:19999),真正的业务逻辑全部下沉到 src/user/ 的管理器与 src/docker/ 的 Docker 集成中。同时 src/utils/CaptainConstants.ts 维护的可调配置项(日志大小、端口、镜像名、Gzip 类型等)数量有限且都带默认值,进一步印证"小控制层"的设计意图。
三、仓库地图:十一个目录读懂 CapRover 后端
AGENTS.md 给出了官方目录地图,结合源码可以还原每一层的具体职责:
| 目录 / 文件 | 职责 | 关键实现 |
|---|---|---|
| src/app.ts | Express 启动引导与 API 挂载 | 中间件链、API 版本校验、健康检查、netdata 反向代理 |
| src/routes/ | HTTP 校验与响应编排 | LoginRouter、UserRouter、AppsRouter、OneClickAppRouter、ProRouter 等 |
| src/handlers/ | 请求级应用操作 | AppDataHandler、AppDefinitionHandler、ProjectHandler、BaseHandlerResult |
| src/user/ | 核心管理器与服务编排 | ServiceManager、UserManager、CaptainManager、LoadBalancerManager、BackupManager 等 |
| src/docker/ | Docker API 集成 | DockerApi、DockerBuildOutput、DockerUtils |
| src/datastore/ | 持久化的 CapRover 状态 | AppsDataStore、DataStoreProvider、ProDataStore、RegistriesDataStore |
| src/injection/ | 请求级依赖注入与认证 | Injector(四种注入器)、InjectionExtractor |
| src/models/ | 共享数据契约 | AppDefinition、DockerService、ICaptainDefinition、OneClickApp 等 |
| src/utils/CaptainConstants.ts | 运行时配置、标识符与文件系统路径 | 常量、config-override 覆盖机制 |
| tests/ | Jest 回归测试 | AppDeletion、DataStore、OneClickAppDeployManager、LoadBalancerManagerRenewal 等 |
3.1 src/app.ts:请求从进入到响应的完整链路
src/app.ts 是 Express 启动文件,其中间件顺序本身就是一张请求处理流程图:
- 基础中间件:EJS 视图引擎、favicon、morgan 日志(跳过健康检查与 netdata 路径)、
bodyParser.json(2mb 上限)、cookieParser; - 调试模式:
CAPTAIN_IS_DEBUG时开启 CORS 头并暴露/force-exit测试端点; - 全局注入:
Injector.injectGlobal()写入 namespace、initialized、forceSsl 等全局状态; - 强制 HTTPS:根据注入的
forceSsl值,将非 HTTPS 请求 302 重定向到 443 端口; - 静态资源:
dist-frontend与public; - 健康检查:
/checkhealth返回CaptainManager.get().getHealthCheckUuid(); - netdata 反向代理:校验登录 cookie 后通过
http-proxy转发; - API 前缀校验(src/app.ts):校验
apiVersionFromRequest是否为v2、Captain 是否已初始化、Docker 版本是否过旧; - 路由挂载:
/login/、/downloads/、/theme/为免鉴权端点,其余挂载到/user/下的 UserRouter; - 兜底:404 转发与统一错误处理器(
ApiStatusCodes.createCatcher(res))。
3.2 src/routes/ 与 src/handlers/:薄路由 + 请求级操作
路由层负责 HTTP 参数校验与响应编排,实际业务交给 handlers 与 managers。从目录结构可以确认:apps 路由下细分 appdata/、appdefinition/、webhooks/ 三个子路由;系统级路由(SystemRouter、ThemesRouter、自托管镜像仓库路由)与 Pro 路由、注册表路由相互独立。这样的划分保证"每个路由只做一件事",也是 AGENTS.md 中"保持路由薄"(Keep routes thin)的直接体现。
3.3 src/user/:核心业务管理器的集合
src/user/ 是 CapRover 业务逻辑最集中的一层,包含应用服务管理(ServiceManager)、用户认证(UserManager、Authenticator)、系统级能力(CaptainManager、BackupManager、CertbotManager、DiskCleanupManager、LoadBalancerManager)、OneClick 应用部署(OneClickAppDeployManager)、Pro 功能(ProManager、OtpAuthenticator)等。它们被 routes 调用、被 injection 层组装,是"控制层"真正的执行者。
3.4 src/injection/:请求级依赖注入与四种认证形态
src/injection/Injector.ts 实现了四种注入器,对应四种请求上下文:
- injectGlobal():所有请求共用。从
x-namespace请求头(缺省为captain根命名空间)解析 namespace,并注入initialized、forceSsl、userManagerForLoginOnly; - injectUser():通过
x-captain-auth请求头内的 JWT 解码用户,组装UserInjected(含 datastore、serviceManager、otpAuthenticator); - injectUserForBuildTrigger():针对构建触发器的伪用户注入,校验
x-captain-app-token与应用定义的appDeployTokenConfig是否匹配,仅能触发特定操作; - injectUserForWebhook():针对 webhook 的伪用户注入,从 query 参数取
token与namespace,解码应用推送 webhook token; - injectUserUsingCookieDataOnly():仅用
captainCookieAuthcookie 解码用户,用于 netdata 这类反向代理服务的鉴权(AGENTS.md 中"为高级/不常见场景保留既有定制钩子"在注入层也有体现)。
配套的 src/injection/InjectionExtractor.ts 提供从 res.locals 读取这些注入对象的静态方法(extractUserFromInjected、extractGlobalsFromInjected、extractAppAndUserForWebhook 等),是路由与中间件之间的标准数据通道。
3.5 src/docker/、src/datastore/ 与 src/models/
src/docker/封装 Docker Engine API(DockerApi.ts 基于 dockerode),并提供构建输出解析(DockerBuildOutput)与工具函数;src/datastore/管理 CapRover 自身的持久化状态(应用、项目、注册表、Pro 数据),通过 DataStoreProvider 按命名空间取用;src/models/存放跨层共享的数据契约(AppDefinition、DockerService、OneClickApp、ICaptainDefinition 等),保证各层之间的类型安全。
3.6 src/utils/CaptainConstants.ts:运行时配置中枢
src/utils/CaptainConstants.ts 是 AGENTS.md 特别指出的"运行时配置、标识符与文件系统路径"所在,其中几个要点与变更约束直接相关:
- API 版本:
apiVersion: 'v2',与 app.ts 的前缀校验联动; - 数据目录:
/captain/data(可备份)、/captain/temp、/captain/generated,基础目录由CAPTAIN_BASE_DIRECTORY环境变量控制; - 命名契约:服务名
captain-nginx、captain-captain、captain-certbot、captain-registry,容器名captain-netdata-container、captain-goaccess-container,网络captain-overlay-network——这些是持久化兼容契约的具体实例; - 认证头:
x-captain-auth、x-captain-app-token、x-namespace、x-caprover-version、cookie 名captainCookieAuth; - 端口覆盖:80/443/3000 端口分别可由
CAPTAIN_HOST_HTTP_PORT、CAPTAIN_HOST_HTTPS_PORT、CAPTAIN_HOST_ADMIN_PORT环境变量覆盖; - 配置覆盖机制:启动时会依次读取构建级
config-override.json与用户级/captain/data/config-override.json并合并覆盖默认配置(overrideConfigFromFile),这是"既有定制钩子"最典型的实现之一。
四、变更约束:四条不可逾越的兼容性红线
AGENTS.md 的 Change constraints 是整个文档的核心,也是贡献者最需要内化的部分。
4.1 保持 API v2 响应形态与既有安装兼容
Preserve API v2 response shapes, status handling, and compatibility with existing installations.
实现层面由三个组件共同保证:
- src/utils/CaptainConstants.ts 中的
apiVersion: 'v2'; - src/app.ts 的前缀校验:非 v2 版本请求直接返回
STATUS_ERROR_GENERIC并拒绝; - 统一响应对象 BaseApi(
status+description+data)与状态码集合 ApiStatusCodes(STATUS_OK=100、STATUS_OK_DEPLOY_STARTED=101、STATUS_ERROR_GENERIC=1000、STATUS_ERROR_CAPTAIN_NOT_INITIALIZED=1001等十余个语义化错误码)。
任何修改如果改变了这些响应结构或状态码语义,都会破坏既有客户端与部署的兼容性,因此被列为第一优先级约束。
4.2 只返回调用方所需的数据,绝不裸暴露 Docker 对象
Return only data required by the caller; never expose raw Docker objects merely because fields are available.
这一条约束了 handlers/routes 的输出边界:即使底层 Docker API 返回了大量字段,也只有在调用方确实需要时才透出。从分层看,src/docker/ 获取原始数据,src/user/ 的 managers 做领域处理,src/routes/ 只编排响应——任何新端点都应沿用这一数据裁剪路径,而非把 Docker 对象直接序列化。
4.3 持久化兼容契约:名称、标签、密钥、证书与 /captain/data
Treat service names, volume names, labels, secrets, certificates, and data under
/captain/dataas persistent compatibility contracts. Account for legacy resources explicitly.
这些资源一旦上线就会存在于运行中的 Swarm 集群里,是跨版本升级必须保留的"公共接口"。具体包括:
- 服务/容器名:
captain-nginx、captain-captain、captain-certbot、captain-goaccess-container、captain-netdata-container、captain-registry; - 数据目录:
/captain/data(含letencrypt/lib、letencrypt/etc、registry、nginx-shared、shared-logs等); - 网络:
captain-overlay-network; - 密钥与 secret:
captain-salt等。
修改这类标识符时,必须显式考虑历史版本遗留的资源(Account for legacy resources explicitly),否则升级后会找不到旧资源。
4.4 删除或变更 Docker 资源前,必须先证明所有权
Before deleting or mutating Docker resources, prove that CapRover owns the exact resource and that it is not shared. Prefer a safe no-op or diagnostic logging when ownership is uncertain.
这是"控制层"最重要的一条安全底线:CapRover 管理的资源可能与用户自建资源同名或共享,删除/变更前必须验证精确所有权;无法确定所有权时,宁可安全空操作或记录诊断日志。仓库中的 tests/AppDeletion.test.ts 正是针对应用删除路径的回归测试,验证删除逻辑对资源归属的判定行为。
4.5 请求体不得覆盖服务端派生字段
Never allow request payloads to overwrite server-derived or immutable fields.
例如应用定义中的部署状态、构建版本、证书状态等由服务端计算或维护的字段,属于不可变或服务端派生的内容,请求载荷无权覆盖。实现上通常由 handlers 在做 patch 前对字段做白名单过滤(相关路径可参考 src/handlers/users/apps/appdefinition/AppDefinitionHandler.ts 与 tests/PatchAppDefinition.test.ts 的覆盖用例)。
五、保持薄路由:复用既有模式,不另起炉灶
Keep routes thin. Use the existing managers,
InjectionExtractor,ApiStatusCodes,BaseApi, andLoggerpatterns instead of parallel mechanisms.
新增功能时的标准姿势是:
- 在
src/routes/新增/修改路由,只做参数校验与响应编排; - 通过
InjectionExtractor从res.locals获取注入的用户与应用上下文; - 业务逻辑调用
src/user/中既有的 manager; - 错误统一走
ApiStatusCodes与BaseApi,日志统一走src/utils/Logger.ts。
禁止在路由层另造一套错误处理、注入读取或日志机制,这既是可维护性的要求,也是上面 API v2 兼容性的延伸——统一的响应与错误语义才能保证客户端行为稳定。
六、本地验证流水线:从依赖安装到完整回归
AGENTS.md 明确要求使用 Node.js 24,并在交付前完整执行以下命令(详见 package.json 的 scripts):
npm ci
npm run formatter
npm run lint
npm run build
npm test -- --runInBand
各步骤的职责与底层实现如下:
| 命令 | 作用 | 底层实现 |
|---|---|---|
npm ci |
按 lockfile 精确安装依赖 | 依据 package-lock.json |
npm run formatter |
代码格式检查 | prettier --check src/**/*.ts(对应 .prettierrc 配置依赖 中的 prettier 3.9.6) |
npm run lint |
ESLint 静态检查 | eslint src/**/*.ts(规则见 eslint.config.mjs) |
npm run build |
编译 + 循环依赖检测 | 先 npx madge --circular --extensions ts ./ 检测模块循环引用,再 rm -rf ./built && npx tsc 输出到 built/ |
npm test -- --runInBand |
完整 Jest 回归 | jest 串行执行全部 tests/*.test.ts(配置见 jest.config.js) |
两点实践建议(原文明确强调):
- 迭代时跑最窄的用例:只运行与改动最相关的单个 Jest 测试文件(如
npx jest tests/AppDeletion.test.ts),快速验证行为; - 交付前跑完整套件:
npm test -- --runInBand串行执行全部测试,确保改动没有破坏其他模块的回归预期。
特别说明 npm run build 的独特之处:它同时承担了 TypeScript 编译与 madge 循环依赖检测(--circular --extensions ts)。由于 src/injection/ 与 src/user/、src/docker/ 之间引用密集,循环依赖是 CapRover 后端最容易引入的架构退化,AGENTS.md 将这一步纳入标准构建流程,正是为了让"依赖方向清晰"成为可机器校验的约束。
七、给贡献者的行动清单
综合 AGENTS.md 全文,一次合规的 CapRover 后端改动可以收敛为以下检查单:
- 确认边界:改动是否属于"优化常见部署工作流"?能否复用既有定制钩子?是否已讨论大型/跨模块方案?
- 确认范围:是否无端新增了 API 字段、配置项或抽象层?
- 确认兼容:API v2 响应形态是否保持?是否触碰了服务/卷/标签/secret/证书/
/captain/data等持久化契约?旧资源是否被显式考虑? - 确认安全:删除/变更 Docker 资源前是否证明了精确所有权?不确定时是否回退为安全空操作或诊断日志?请求体是否可能覆盖服务端派生字段?
- 确认模式:路由是否保持薄?是否复用了 managers、
InjectionExtractor、ApiStatusCodes、BaseApi、Logger? - 确认测试:是否为行为变更和失败路径补充了聚焦的回归测试,并避免无关重构?
- 确认验证:在 Node.js 24 下依次跑完
npm ci、formatter、lint、build、test -- --runInBand。
AGENTS.md 用极短的篇幅定义了一套完整、可执行且可在源码中逐条验证的工程契约。对于希望深入 CapRover 后端的开发者或 Agent,这份文档加上 src/ 与 tests/ 的对照阅读,就是最准确、最不会跑偏的入门路径。