CapRover 后端开发指南:基于 AGENTS.md 的架构分层、兼容性契约与本地验证流水线

原创2026-10-02 16:43:341,862 阅读
文章标签:DevOps云原生运维

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 启动文件,其中间件顺序本身就是一张请求处理流程图:

  1. 基础中间件:EJS 视图引擎、favicon、morgan 日志(跳过健康检查与 netdata 路径)、bodyParser.json(2mb 上限)、cookieParser;
  2. 调试模式:CAPTAIN_IS_DEBUG 时开启 CORS 头并暴露 /force-exit 测试端点;
  3. 全局注入:Injector.injectGlobal() 写入 namespace、initialized、forceSsl 等全局状态;
  4. 强制 HTTPS:根据注入的 forceSsl 值,将非 HTTPS 请求 302 重定向到 443 端口;
  5. 静态资源:dist-frontend 与 public;
  6. 健康检查:/checkhealth 返回 CaptainManager.get().getHealthCheckUuid();
  7. netdata 反向代理:校验登录 cookie 后通过 http-proxy 转发;
  8. API 前缀校验(src/app.ts):校验 apiVersionFromRequest 是否为 v2、Captain 是否已初始化、Docker 版本是否过旧;
  9. 路由挂载:/login/、/downloads/、/theme/ 为免鉴权端点,其余挂载到 /user/ 下的 UserRouter;
  10. 兜底: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():仅用 captainCookieAuth cookie 解码用户,用于 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/data as 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, and Logger patterns instead of parallel mechanisms.

新增功能时的标准姿势是:

  1. 在 src/routes/ 新增/修改路由,只做参数校验与响应编排;
  2. 通过 InjectionExtractor 从 res.locals 获取注入的用户与应用上下文;
  3. 业务逻辑调用 src/user/ 中既有的 manager;
  4. 错误统一走 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 后端改动可以收敛为以下检查单:

  1. 确认边界:改动是否属于"优化常见部署工作流"?能否复用既有定制钩子?是否已讨论大型/跨模块方案?
  2. 确认范围:是否无端新增了 API 字段、配置项或抽象层?
  3. 确认兼容:API v2 响应形态是否保持?是否触碰了服务/卷/标签/secret/证书//captain/data 等持久化契约?旧资源是否被显式考虑?
  4. 确认安全:删除/变更 Docker 资源前是否证明了精确所有权?不确定时是否回退为安全空操作或诊断日志?请求体是否可能覆盖服务端派生字段?
  5. 确认模式:路由是否保持薄?是否复用了 managers、InjectionExtractor、ApiStatusCodes、BaseApi、Logger?
  6. 确认测试:是否为行为变更和失败路径补充了聚焦的回归测试,并避免无关重构?
  7. 确认验证:在 Node.js 24 下依次跑完 npm ci、formatter、lint、build、test -- --runInBand。

AGENTS.md 用极短的篇幅定义了一套完整、可执行且可在源码中逐条验证的工程契约。对于希望深入 CapRover 后端的开发者或 Agent,这份文档加上 src/ 与 tests/ 的对照阅读,就是最准确、最不会跑偏的入门路径。

登录后查看全文
caprover