AI 参与说明(Agent:Codex):本文由 Codex 协助完成 Convex 官方文档与源码快照核对、阿里云官方资料研究、架构设计、Compose/反向代理审计、Hugo 渲染验证和上线门禁整理。资料核验日期为 2026-08-25。本次没有获得阿里云账号、目标 VPC 或 staging 资源,因此文中的阿里云架构是可执行设计,不是已通过真实环境验收的上线报告。Context7 在本次查询时因配额用尽无法返回结果,相关结论改由 Convex 官方站点、
get-convex/convex-backend指定 commit、npm Registry 与阿里云官方文档交叉核对。
适用范围:方案面向
manaio.cn旗下自有 Web / Mobile 产品,以阿里云中国大陆基础设施为主数据面,不向第三方出售 deployment、database、functions 或 Dashboard,也不把它包装成通用 BaaS。如果目标是无停机多活、严格 SLA 或对外托管平台,需要重新做技术与许可评估。
先说结论#
在阿里云中国大陆部署开源 Convex 的结论是:有条件可行。
- 作为 PoC、staging 和可接受分钟级恢复的自有产品后端,可以进入落地。
- 阿里云 ECS、ALB、WAF、RDS PostgreSQL、OSS、ACR、SLS、CloudMonitor、KMS 和 RAM 可以组成完整的中国大陆运行底座。
- 生产数据应外置到 RDS PostgreSQL 和 OSS,ECS 只运行可重建的 Convex Backend、Dashboard 与反向代理。
- 当前不应承诺严格无停机或 active-active:Convex 自托管资料没有给出多 Backend 副本的生产 HA 操作手册,官方仅说明 Self-hosted 支持 Cloud 免费层功能,Cloud 才是为规模化运行优化的产品。Self Hosting Self-hosted limitations
- 两个最重要的技术门禁是 OSS 兼容性与公网 Admin token 隔离。Convex 支持自定义 S3-compatible endpoint,而 OSS 官方仅承诺 S3 API 子集;必须实测 multipart、checksum、SSE、range request、错误语义和 import/export。同时,公网路由 allowlist 不能单独阻止 Admin Key 走公共 HTTP/WebSocket 协议;生产必须在源码或协议网关层拒绝。
更准确的项目决策是:
| 决策层级 | 结论 | 条件 |
|---|---|---|
| 进入阿里云 PoC | Go | 不承载生产数据,完成 RDS、OSS、WebSocket 与恢复实测 |
| 单业务受控生产 | Conditional Go | 所有 Go/No-Go 门禁通过,且接受单 active Backend 与分钟级 RTO |
| 严格无停机、active-active | No-Go | 除非 Convex 官方确认并且目标版本实测通过 |
| 对外销售 Convex-like BaaS | No-Go | 超出本方案的工程与 FSL 许可边界 |
证据级别与当前阻塞#
本文用四种标记区分事实和设计:
[Official]:Convex 或阿里云官方文档直接说明。[Source]:可从指定 Convex commit 的源码或官方 Compose 确认。[Design]:本文给出的工程方案,不是产品官方承诺。[Unverified]:需在目标账号、地域、版本和真实入口上实测。
在解除下列阻塞前,只能说“基础设施方案可行”,不能说“生产已验证”:
manaio.cn的 ICP 备案主体、当前接入商、是否需要阿里云接入备案,以及上线后的公安备案状态尚未核验。- Convex 目标 Backend 尚未在阿里云 RDS PostgreSQL 17 上验证 TLS、migration、连接上限和 failover 恢复。
- Convex 对 OSS 的 S3 调用尚未经完整 PoC。OSS 只支持 virtual-hosted-style,不支持 path-style 和
aws-chunked。OSS 与 Amazon S3 API 兼容性 使用 AWS SDK 访问 OSS - ALB + WAF + Nginx 路径上的 WebSocket 心跳、空闲超时、断线重连和连接排空尚未长连实测。
- 没有官方依据证明多个当前 Self-hosted Backend 可以同时接受写流量。
- 候选 Backend、Dashboard、
convexnpm 包与 Node.js 运行时组合尚未完成兼容矩阵。 - RDS PITR、Convex export/import、OSS Versioning / Cross-Region Replication 还没有组成经过演练的一致恢复流程。
INSTANCE_SECRET、Admin Key、RDS 密码与 OSS 凭据的注入、轮换和泄露处置尚未在 staging 验证。- 固定源码的公共 HTTP API 会接受 Admin Key,WebSocket sync 协议也支持
tokenType: Admin。当前 Nginx allowlist 只隔离管理 URL,不能检查 WebSocket frame;严格管理面内网化要求尚未实现。
Convex 官方在 Self-hosted README 中将求助渠道指向 Discord #self-hosted 与 GitHub Issues;本次查阅的资料没有提供 Self-hosted 生产 SLA、值班支持或故障恢复承诺。因此运行团队必须自行承担版本跟踪、漏洞修复、备份演练、告警和事故响应,不应把社区求助等同于商业支持。Self-hosted limitations and support channels
本文交付边界#
本文交付的是一份可用于评审和 staging 实施的部署方案:包含域名/网络拓扑、数据与对象存储选型、候选 BOM、独立 Compose 骨架、Nginx 公私网路由、PoC 用例、备份/恢复/升级流程和 Go/No-Go 门禁。它没有创建真实 Terraform/OpenTofu 状态、阿里云资源、DNS 记录或密钥,也没有声称已通过 E2E/压测/故障演练。要形成可执行的 IaC 与真实验收报告,还需要确认阿里云账号/地域/VPC、manaio.cn 备案与 DNS 现状、费用上限、RPO/RTO 以及应用仓库和真实流量模型。
许可与产品边界#
convex-backend 根目录当前使用 FSL-1.1-Apache-2.0。许可文本明确将 internal use 列为允许目的,同时排除把软件作为替代 Convex 或提供实质相似功能的竞争性商业产品/服务。每个版本在首次提供两年后额外转为 Apache-2.0。Convex Backend LICENSE
因此,将 Convex 用作 manaio.cn 自有普通业务的内部后端,且不让第三方创建 deployment、数据库或 functions,与许可文本明示的 internal use 一致。这不是法律意见;如果未来产品让客户管理 deployment、编写函数、直接使用 Dashboard,或者按用量销售数据库/函数能力,应在继续前让律师审查并联系 Convex 书面确认。
推荐架构#
逻辑架构#
flowchart TD
clients["Web / React Native 客户端"] --> dns["Alibaba Cloud DNS\napp / api / actions.manaio.cn"]
dns --> alb["WAF-enhanced Public ALB\nHTTPS / WSS / Health Check"]
subgraph vpc["单一阿里云中国大陆地域与 VPC"]
alb --> guard["Public protocol guard\n源码补丁或 WS-aware gateway"]
guard --> proxy["Public Nginx proxy\n只允许固定版本的公共路由"]
private_clients["Internal ALB / VPN / CI / SSH tunnel"] --> admin_proxy["Private admin proxy"]
proxy --> api["Convex Backend :3210\nAPI / WebSocket / File Storage"]
proxy --> actions["Convex Backend :3211\nHTTP Actions"]
admin_proxy --> api
admin_proxy --> dashboard["Convex Dashboard :6791"]
api --> rds["RDS PostgreSQL 17 HA\ninternal endpoint / TLS"]
api --> oss["5 个私有 OSS bucket\ninternal S3 endpoint"]
actions --> rds
actions --> oss
end
acr["ACR Enterprise\n固定 digest"] --> proxy
acr --> api
kms["KMS / RAM\n运行时机密"] --> proxy
kms --> api
proxy --> sls["SLS / CloudMonitor\n日志、指标、告警"]
api --> sls
alb --> sls[Design] 公网只暴露 WAF Enhanced ALB 的 443。ECS 不绑 EIP,Convex 的 3210/3211/6791、Docker API 和 RDS 5432 都不对公网开放。Cloudflare 可继续用于中国大陆以外的全球前端或非主数据面,但不放在本文的大陆 Convex 主路径中。
manaio.cn 域名规划#
| 域名 | 用途 | 内部端口 | 暴露方式 |
|---|---|---|---|
app.manaio.cn | React / Vite / TanStack Web App | 由前端托管决定 | 公网 HTTPS |
api.manaio.cn | Convex 公共 API、WebSocket 和默认 File Storage | 3210 的固定公共路由 | WAF + Public ALB HTTPS/WSS |
actions.manaio.cn | Convex HTTP Actions | 3211 | WAF + ALB HTTPS |
admin-api.internal.manaio.cn | CLI/CI deploy、import/export 与 Dashboard API | 3210 全路由 | PrivateZone + Internal ALB/VPN/受控 CI |
dashboard.internal.manaio.cn | Self-hosted Dashboard | 6791 | PrivateZone + VPN/堡垒机,或 SSH tunnel |
files.manaio.cn | 可选的应用层 OSS 大文件直传通道 | OSS custom domain | 公网 HTTPS,bucket 仍为 private |
files.manaio.cn 不是 Convex Self-hosted 的必需域名。它只在业务需要绕过 Convex 默认上传路径、由应用服务签发 OSS STS/presigned URL 时使用。
Convex 与应用的 URL 配置应为:
| 位置 | 变量 | 值 | 是否机密 |
|---|---|---|---|
| Backend | CONVEX_CLOUD_ORIGIN | https://api.manaio.cn | 否 |
| Backend | CONVEX_SITE_ORIGIN | https://actions.manaio.cn | 否 |
| Dashboard | NEXT_PUBLIC_DEPLOYMENT_URL | https://admin-api.internal.manaio.cn | 否;Dashboard 浏览器必须可达 |
| CI / 开发者机 | CONVEX_SELF_HOSTED_URL | https://admin-api.internal.manaio.cn | 否;经 VPN/Internal ALB 访问 |
| CI / 开发者机 | CONVEX_SELF_HOSTED_ADMIN_KEY | 从 KMS/受控 Secret 注入 | 是 |
| Vite 客户端 | VITE_CONVEX_URL | https://api.manaio.cn | 否 |
| Vite 客户端 | VITE_CONVEX_SITE_URL | https://actions.manaio.cn | 否;这是应用自定义名,不是 Convex 官方变量 |
上表是 PrivateZone/VPN/Internal ALB 模式。如果选择纯 SSH tunnel,只转发 Dashboard 的 6791 不会工作,因为浏览器还会访问 NEXT_PUBLIC_DEPLOYMENT_URL。应另建并验证 SSH-only BOM:把 Dashboard 的该值改为 http://127.0.0.1:8081,同时将本地 6791 和 8081 转发到 active ECS 的两个 loopback 端口。另一种做法是同时转发管理 API 并为内部域名配置本地 DNS/匹配证书。不应绕过浏览器证书验证来省略这一步。
CONVEX_SELF_HOSTED_ADMIN_KEY 只属于 CLI/CI 控制面,绝不能以 VITE_ 变量、网页配置、移动端包或前端 Source Map 的形式交付给客户端。Convex Self-hosted 将公共数据面和带 Admin Key 的管理能力复用在 3210;因此只把 Dashboard UI 或管理 URL 放到私网还不够。
固定源码的 ExtractAuthenticationToken 在公共 HTTP handler 上接受 Authorization: Convex <key> 和 ?adminKey=,/api/function 与 /api/run/* 会以验证后的 identity 执行函数;WebSocket sync 的 Authenticate message 也明确接受 tokenType: Admin。因此,下面的 Nginx 可以拒绝 HTTP header/query 中的 Admin Key,但无法检查已升级连接里的 WebSocket frame。公共 HTTP 认证源码 Public Function 源码 WebSocket Admin token 源码
生产若要满足“Admin Key 只能在私网使用”的严格要求,必须对固定 Backend source 增加可审计补丁,或在公网前置一个能理解 Convex sync protocol 并拒绝 Admin Authenticate message 的 gateway。补丁不能直接依赖现有 ExtractResolvedHostname:候选源码只识别 Convex 官方 hostname,自定义的 public/internal hostname 会 fallback 到同一组 origin,无法据此区分入口。可行实现是让受信 public/private proxy 覆盖一个不可由客户端保留的 ingress-class header,或由独立 listener 传入入口类型,并把该上下文从 HTTP/WebSocket upgrade 一直携带到 sync Authenticate;Backend 只允许 private class 使用 Admin identity。Backend 端口必须仅对这些受信 proxy 可达。Hostname resolution source
两种方案都必须随每次 source 升级重新审计,并以黑盒测试证明:公网的 HTTP Admin header/query、WebSocket Admin token、internal function 和管理路由全部失败,而私网 Admin API 与正常用户认证仍然成功。未通过该门禁时,本文的 Nginx/Compose 只能用于 PoC,不是符合严格管理面隔离的生产方案。Convex 官方的自托管端口和 URL 关系见 Hosting on your own infrastructure。
地域与多可用区#
不应在没有用户分布、库存、价格与备案资料的情况下直接指定杭州。可以同日比较杭州、上海、深圳,按以下顺序选择:
- 主要用户的端到端延迟。
- 目标地域是否提供 OSS、KMS、ACR Enterprise 和 SLS,且至少两个可用区有 ALB vSwitch、ECS 与 RDS PostgreSQL 17 High-availability Edition 所需资源。
- ICP 备案与阿里云接入条件。
- 账号当日的配额、库存、带宽与报价。
- OSS Cross-Region Replication 目标地域与数据合规要求。
RDS、OSS、ECS 必须位于同一阿里云地域;应用、ALB 和 RDS 分布到至少两个可用区所属 vSwitch。但是 ALB 的多可用区不会自动让单个 Convex Backend 获得应用层 HA。
网络与最小暴露面#
| 来源 | 目标 | 允许 | 说明 |
|---|---|---|---|
| Internet | Public ALB | TCP 443 | 唯一业务公网入口 |
| ALB Local IP / ALB vSwitch | ECS Nginx | TCP 8080 | 含 Health Check;按控制台显示的回源源地址放行 |
| Internal ALB / VPN / 受控 CI | ECS admin proxy | TCP 8081/6791 | 不从 Public ALB 或 Internet 达到 |
| ECS app Security Group | RDS | TCP 5432 | RDS whitelist 再限制 ECS 私网地址 |
| ECS | OSS internal endpoint | TCP 443 | 同地域私网访问 |
| ECS | ACR / KMS / SLS | 所需 HTTPS | 优先 VPC 访问或受控出站 |
| 堡垒机 / VPN / Cloud Assistant | ECS | 最小管理端口 | 审计、MFA、固定来源 |
| Internet | ECS / RDS / Dashboard / Docker API | 拒绝 | 不建立任何直达路径 |
Security Group 是 stateful virtual firewall,应创建 custom Security Group,而不是依赖可能带宽松规则的 default Security Group。Security Group 规则 VPC 基础设施安全
计算层与高可用边界#
初期规格#
下表是工程起步值,不是 Convex 或阿里云官方容量建议:
| 环境 | ECS 起步规格 | 云盘 | 用途 |
|---|---|---|---|
| dev/PoC | 4 vCPU / 16 GiB | 80–100 GiB ESSD | 兼容性、功能与故障测试 |
| staging | 4 vCPU / 16 GiB | 100 GiB ESSD | 与生产同入口、同 RDS/OSS 类型的验收 |
| production 起步 | 8 vCPU / 32 GiB | 100–200 GiB ESSD | 单 active Backend,预留容器、导出与日志峰值空间 |
在 staging 以真实 Query/Mutation/Action、WebSocket 订阅数、文件大小、并发和数据量压测,再按 CPU、RSS、p95/p99、事务冲突、PostgreSQL 连接数与 OSS 错误率调整。不要从“用户数”直接猜 ECS 规格。
单 active + 可重建 standby#
[Design] 第一阶段只让一台 ECS 的 Backend 运行。另一个可用区的 standby 必须是 Backend 容器停止的主机或纯启动模板,不能只是“没有注册到 ALB”但仍连接 RDS/OSS 的第二个 Backend。数据和文件外置后,故障恢复流程是:
- 从 ALB Server Group 摘除故障 active,并立即停止/隔离旧 ECS。
- 通过 ECS Stop/Terminate、Security Group/RDS whitelist 隔离和实例专属 OSS/RDS 凭据撤销完成 fencing,确认旧 RDS 连接和后台任务已消失。
- 在另一可用区按固定镜像 digest 启动 ECS/Compose。
- 从 KMS 注入同一
INSTANCE_NAME、稳定INSTANCE_SECRET、RDS 与 OSS 配置。 - 确认
/version、数据库、对象存储、Query/Mutation、scheduled function 和 WebSocket 恢复。 - 再将新实例加入 Server Group。
这个方案目标是可演练的分钟级 RTO,不是 RTO=0。INSTANCE_SECRET 不可丢失:官方启动脚本在未显式传入时会把它和 INSTANCE_NAME 写入 /convex/data/credentials;即使 PostgreSQL 和 OSS 已经外置,单靠重建本地 volume 仍会改变实例密钥。Credential bootstrap source
RDS PostgreSQL 设计#
必须遵守的 Convex URL 语义#
[Official] Convex 当前自托管文档说明测试过 PostgreSQL 17,并建议 Backend 与数据库尽可能位于同一地域。POSTGRES_URL 必须包含用户名,但不能包含 database name 和 query string。Using Postgres or MySQL
[Source] Backend 会把 INSTANCE_NAME 中的连字号替换为下划线,将结果作为数据库名,然后追加 sslmode=require 与 target_session_attrs=read-write。例如:
INSTANCE_NAME=manaio-convex-prod
POSTGRES_URL=postgresql://convex_runtime:<secret>@<rds-internal-host>:5432
预先创建的 database: manaio_convex_prod生产不要设置 DO_NOT_REQUIRE_SSL。尤其不要写 DO_NOT_REQUIRE_SSL=false:当前启动脚本只判断该变量是否非空,字符串 false 也会触发“不强制 SSL”。Database URL construction Startup script
[Source] Backend 支持 PG_CA_FILE,但官方 Compose 没有透传该变量。阿里云 RDS 证书链若不在容器系统信任库中,需要在生产 override 中增加只读 CA 挂载和 PG_CA_FILE。PostgreSQL CA loading source
RDS 基线#
- 使用 PostgreSQL 17 Multi-zone High-availability Edition,只开 internal endpoint。
- 单独建立 Convex RDS instance 或至少独立 database / account,不与业务 SQL 库共用高权限账号。
- 使用 RDS hostname,不缓存 failover 前解析出的 IP。
- 开启 TLS、automatic data backup 和 log backup/PITR,上线前完成一次人工 switchover。
- 监控连接数、CPU、IOPS、存储、replication lag、慢查询、锁等待和 failover 事件。
- 初期将 Convex 的 PostgreSQL 连接上限设为 RDS 允许连接数内的保守子集,例如工程起点 64,再根据压测调整;不要盲目沿用源码默认值。
- 初期使用 direct connection。PgBouncer 只在验证 Convex transaction、prepared statement 和 session state 后再引入。
RDS 在这里是 Convex 的内部 persistence,不是对业务开放的 SQL 模型。业务 ORM、Drizzle、报表工具或人工脚本不得直接读写 Convex 内部表。Convex 是业务事实源;如果需要 SQL/D1/warehouse 读模型,只能通过可重放的事件/outbox 投影构建,不做双写,也不从读模型反向改写 Convex。
RDS High-availability Edition 由 primary 和 standby 组成,可自动 failover;standby 不提供只读能力。切换后 endpoint 不变,但长连接可能挂起数百秒,必须验证 Backend 的 timeout/reconnect。RDS PostgreSQL High-availability Edition Primary/standby switchover
OSS 与 File Storage:必须先纠正一个关键假设#
Convex 默认文件流量不是浏览器直连 OSS#
[Source] 在当前 Convex 快照中,storage.generateUploadUrl() 生成:
https://api.manaio.cn/api/storage/upload?token=...storage.getUrl() 生成:
https://api.manaio.cn/api/storage/<storage-id>两者均基于 CONVEX_CLOUD_ORIGIN,并不是 OSS presigned URL。浏览器将文件 POST 到 Convex Backend,Backend 再写入底层 S3-compatible storage;下载也由 Backend 从存储读取并流式回传。File Storage URL source
这一点带来四个设计结论:
- Convex 默认 File Storage 不需要浏览器访问 OSS internal endpoint,也不需要为这条路径配置 OSS CORS。
api.manaio.cn、WAF、ALB、Nginx 和 Backend 都会承载文件带宽,必须一起调整 body limit、request buffering、超时与流式响应。- OSS 可以全程保持 private,Backend 使用同地域 internal S3 endpoint。
- 默认路径不适合将超大媒体直接上传 OSS 来绕过应用入口。
Convex 通用 File Storage 文档说明,通过生成 upload URL 上传的 POST 没有固定文件大小上限,但有两分钟超时;候选 Self-hosted source 没有发现独立的 120 秒 upload timeout,其全局 HTTP_SERVER_TIMEOUT_SECONDS 默认为 300 秒。因此 Self-hosted 实际有效上限是客户端、WAF、ALB、Nginx 和 Backend 限制中的最小值,两分钟只能作为必测边界,不应写成该 Self-hosted 版本已证实的固定超时。通过 HTTP Action 上传则受 20 MB request/response 限制。Uploading and Storing Files HTTP Actions
两条文件通道#
| 通道 | 适合对象 | 路径 | 权限模型 |
|---|---|---|---|
| Convex File Storage | 头像、封面、可接受 bearer-link 的中小附件 | 浏览器 → api.manaio.cn → Backend → OSS | 上传使用短期 token;storage.getUrl() 是 bearer URL,持有者无需应用鉴权即可读 |
| 应用层 OSS 直传 | 大视频、大素材、分片/断点续传 | 浏览器 → files.manaio.cn / OSS | 自有服务签发 STS/presigned URL,Convex 仅保存 object key、owner、size、checksum 和状态 |
storage.getUrl() 返回的 URL 必须当作 bearer credential:未授权用户应无法从业务 API 获得 URL,但一旦 URL 泄露,持有者可在不带应用会话的情况下读取;官方给出的撤销方式是删除文件,若仍需要则重新上传。候选 source 还对 File Storage GET 返回 private cache max-age=30 days,删除后已缓存的客户端副本不会立即受服务端撤销。需要每次读取都重新鉴权的小文件可通过 HTTP Action 代理,但要受 20 MB 上限;大型私有文件应走应用层 OSS 短期签名 URL。Serving Files Self-hosted storage response source
第二条通道是自定义应用能力,不能冒充为 Convex File Storage。它需要独立 bucket/prefix、CORS、完成回调、checksum、过期 multipart 清理、STS/签名 URL 过期测试、所有者校验和防重放逻辑。
Convex 的五个 OSS bucket#
每个环境单独创建五个全局唯一、private bucket:
| Convex 变量 | 示例 bucket(需确认未被占用) |
|---|---|
S3_STORAGE_EXPORTS_BUCKET | manaio-convex-prod-exports |
S3_STORAGE_SNAPSHOT_IMPORTS_BUCKET | manaio-convex-prod-snapshot-imports |
S3_STORAGE_MODULES_BUCKET | manaio-convex-prod-modules |
S3_STORAGE_FILES_BUCKET | manaio-convex-prod-files |
S3_STORAGE_SEARCH_BUCKET | manaio-convex-prod-search |
以杭州作为仅供 PoC 的示例:
AWS_REGION=cn-hangzhou
S3_ENDPOINT_URL=https://s3.oss-cn-hangzhou-internal.aliyuncs.com
AWS_S3_FORCE_PATH_STYLE=false
S3_STORAGE_EXPORTS_BUCKET=manaio-convex-staging-exports
S3_STORAGE_SNAPSHOT_IMPORTS_BUCKET=manaio-convex-staging-snapshot-imports
S3_STORAGE_MODULES_BUCKET=manaio-convex-staging-modules
S3_STORAGE_FILES_BUCKET=manaio-convex-staging-files
S3_STORAGE_SEARCH_BUCKET=manaio-convex-staging-searchConvex 当前默认使用 S3 checksum 和 server-side encryption header,并提供 AWS_S3_DISABLE_CHECKSUMS、AWS_S3_DISABLE_SSE、AWS_S3_DISABLE_RANGE_PREFETCH 等兼容开关。不要在还没看到错误前一次性全关掉;应以默认行为为基线,通过单变量 PoC 确认哪个开关是必要的。如果关闭 request-level SSE header,由 OSS bucket default SSE-KMS 承担服务端加密。Convex S3 storage guide S3 compatibility switches
[Source] Convex 使用 AWS Default Credentials Chain,并没有明示支持阿里云 ECS RAM Role 的 metadata 协议。不应假设将 RAM Role 绑到 ECS 后 Convex 就会自动拿到 OSS 凭据。起步方案是为五个 bucket 创建极小权限 RAM 凭据,保存在 KMS Secrets Manager,启动时注入 root-only tmpfs 环境文件并通过受控重启轮换。更理想的长期方案是实测可轮换 STS 凭据代理,但在 Convex 能热加载凭据前不能直接宣称无长期 AccessKey。
OSS 基线应另外包含 Block Public Access、Versioning、Lifecycle(包括未完成 multipart 清理)、服务端加密、访问日志,并根据 RPO 决定 Cross-Region Replication。CRR 是异步副本,不是 RPO=0 的证明。
ALB、WAF 与 Nginx#
入口参数#
- ALB 使用 HTTPS Listener,按 Host 将
api.manaio.cn和actions.manaio.cn路由到同一 ECS Nginx:8080。 - ALB 原生支持 WebSocket。Listener idle timeout 和 request timeout 需在目标控制台配置并根据 quota 确认;客户端心跳周期明显小于 idle timeout。ALB Listener ALB WebSocket 场景
[Source]候选源码把 3211 命名为dev_site_proxy,其ConvexHttpService的全局 outstanding request concurrency 硬编码为 4;长时间 HTTP Action 会长期占用槽位,增加 ECS 规格不能解除这个代码上限。G5 必须对真实 webhook、并发与长请求压测并定义 HTTP Action SLO。如果 4 个槽位不够,只能在目标 revision 审计并实测 3210 的/http/*路由替代路径,或维护明确的源码补丁;两者都不能在没有兼容测试时直接切换。3211 proxy source HTTP concurrency layer- Health Check 用
GET /version穿过 Nginx 访问 3210;这也是官方 Compose 的 Backend healthcheck 路径。实测它在 RDS/OSS 失效时的语义,必要时另建 dependency readiness 监控。 - ALB 在所有后端不健康时仍可能尝试转发,因此还需外部 availability probe 和自动恢复/告警。ALB Health Check
- WAF 3.0 cloud-native mode 可保护 ALB HTTP/HTTPS 入口,但官方明确说明 WAF 只转发 WebSocket traffic,不检测 WebSocket frame 内容。消息级鉴权、tenant isolation、频率与 schema 校验仍由应用承担。WAF cloud-native mode FAQ
- 在 WAF 配置 HTTP 频率限制、扫描/机器人规则和紧急封禁,在应用层按用户、tenant 与业务操作限额;对 WebSocket 单独限制连接建立率、并发连接和消息频率。上线前根据攻击面和业务损失评估 Anti-DDoS 防护等级与切换手册,不把 WAF 视为 DDoS 和 WebSocket 消息滥用的唯一控制。
- API/WSS 失败时保留正确 HTTP 状态、Convex 错误体和 request ID,不用 ALB/Nginx 的品牌 HTML 错误页覆盖机器可读响应或 WebSocket handshake;只在
app.manaio.cn前端展示用户可读的降级页。 - 启用 connection draining,发布时先摘除实例,等待旧连接排空,再停 Backend。如果只有单 active,这会产生可见维护窗口。
反向代理基线#
下面是待 staging 验证的 nginx-public.conf 基线,不是已实测产物:
map $http_upgrade $connection_upgrade {
default upgrade;
'' '';
}
map $uri $convex_safe_uri {
default $uri;
"/api/storage/upload" /api/storage/upload;
~^/api/storage/[^/]+$ /api/storage/:id;
}
map $http_authorization $convex_http_admin_header {
default 0;
~*^Convex[ ]+ 1;
}
map $arg_adminKey $convex_http_admin_query {
default 0;
~.+ 1;
}
log_format convex_safe '$remote_addr - $remote_user [$time_local] '
'"$request_method $convex_safe_uri $server_protocol" '
'$status $body_bytes_sent request_id=$request_id '
'"$http_user_agent"';
resolver 127.0.0.11 valid=10s ipv6=off;
resolver_timeout 5s;
upstream convex_api {
zone convex_api 64k;
server backend:3210 resolve;
keepalive 32;
}
upstream convex_actions {
zone convex_actions 64k;
server backend:3211 resolve;
keepalive 16;
}
server {
listen 8080 default_server;
server_name _;
access_log /var/log/nginx/access.log convex_safe;
return 404;
}
server {
listen 8080;
server_name api.manaio.cn;
access_log /var/log/nginx/access.log convex_safe;
add_header X-Request-ID $request_id always;
# 仅为 HTTP 层 defense-in-depth;不能检查 WebSocket Admin message。
if ($convex_http_admin_header) { return 403; }
if ($convex_http_admin_query) { return 403; }
client_max_body_size 256m;
location = /metrics {
return 404;
}
location = /version {
proxy_pass http://convex_api/version;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Request-ID $request_id;
proxy_set_header Convex-Request-Id $request_id;
proxy_set_header X-Manaio-Ingress-Class public;
proxy_set_header X-Forwarded-Proto https;
}
# 下列 allowlist 固定到本文审计的 c0cb/7cce 路由快照。
# 升级 Backend 时必须重新与 router.rs 和 public_api.rs 对照。
location ~ ^/api/(?:[^/]+/sync|sync|query|query_ts|query_at_ts|query_batch|mutation|action|function)$ {
proxy_pass http://convex_api;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Request-ID $request_id;
proxy_set_header Convex-Request-Id $request_id;
proxy_set_header X-Manaio-Ingress-Class public;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_request_buffering off;
proxy_buffering off;
proxy_connect_timeout 10s;
proxy_read_timeout 650s;
proxy_send_timeout 650s;
}
location ~ ^/api/run/ {
proxy_pass http://convex_api;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Request-ID $request_id;
proxy_set_header Convex-Request-Id $request_id;
proxy_set_header X-Manaio-Ingress-Class public;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto https;
proxy_request_buffering off;
proxy_buffering off;
proxy_read_timeout 650s;
proxy_send_timeout 650s;
}
location = /api/storage/upload {
proxy_pass http://convex_api;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Request-ID $request_id;
proxy_set_header Convex-Request-Id $request_id;
proxy_set_header X-Manaio-Ingress-Class public;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto https;
proxy_request_buffering off;
proxy_buffering off;
proxy_read_timeout 650s;
proxy_send_timeout 650s;
}
location ~ ^/api/storage/[^/]+$ {
proxy_pass http://convex_api;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Request-ID $request_id;
proxy_set_header Convex-Request-Id $request_id;
proxy_set_header X-Manaio-Ingress-Class public;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto https;
proxy_buffering off;
proxy_read_timeout 650s;
proxy_send_timeout 650s;
}
location / {
return 404;
}
}
server {
listen 8080;
server_name actions.manaio.cn;
access_log /var/log/nginx/access.log convex_safe;
add_header X-Request-ID $request_id always;
# 3211 的 HTTP Action handler 同样可提取 Admin token。
if ($convex_http_admin_header) { return 403; }
if ($convex_http_admin_query) { return 403; }
client_max_body_size 20m;
location / {
proxy_pass http://convex_actions;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Request-ID $request_id;
proxy_set_header Convex-Request-Id $request_id;
proxy_set_header X-Manaio-Ingress-Class public;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Connection "";
proxy_request_buffering off;
proxy_buffering off;
proxy_connect_timeout 10s;
proxy_read_timeout 650s;
proxy_send_timeout 650s;
}
}私有 nginx-admin.conf 只在 Internal ALB/VPN/受控 CI 网络中使用;它与公共 proxy 分开启动:
map $http_upgrade $admin_connection_upgrade {
default upgrade;
'' '';
}
map $uri $convex_admin_safe_uri {
default $uri;
"/api/storage/upload" /api/storage/upload;
~^/api/storage/[^/]+$ /api/storage/:id;
}
log_format convex_admin_safe '$remote_addr - $remote_user [$time_local] '
'"$request_method $convex_admin_safe_uri $server_protocol" '
'$status $body_bytes_sent request_id=$request_id '
'"$http_user_agent"';
resolver 127.0.0.11 valid=10s ipv6=off;
resolver_timeout 5s;
upstream convex_admin_api {
zone convex_admin_api 64k;
server backend:3210 resolve;
keepalive 16;
}
upstream convex_dashboard {
zone convex_dashboard 64k;
server dashboard:6791 resolve;
keepalive 8;
}
server {
listen 8081 default_server;
server_name _;
access_log /var/log/nginx/access.log convex_admin_safe;
return 404;
}
server {
listen 8081;
server_name admin-api.internal.manaio.cn;
access_log /var/log/nginx/access.log convex_admin_safe;
add_header X-Request-ID $request_id always;
client_max_body_size 256m;
location / {
proxy_pass http://convex_admin_api;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Request-ID $request_id;
proxy_set_header Convex-Request-Id $request_id;
proxy_set_header X-Manaio-Ingress-Class private;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $admin_connection_upgrade;
proxy_request_buffering off;
proxy_buffering off;
proxy_read_timeout 650s;
proxy_send_timeout 650s;
}
}
server {
listen 6791 default_server;
server_name _;
access_log /var/log/nginx/access.log convex_admin_safe;
return 404;
}
server {
listen 6791;
server_name dashboard.internal.manaio.cn;
access_log /var/log/nginx/access.log convex_admin_safe;
add_header X-Request-ID $request_id always;
location / {
proxy_pass http://convex_dashboard;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Request-ID $request_id;
proxy_set_header Convex-Request-Id $request_id;
proxy_set_header X-Manaio-Ingress-Class private;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Connection "";
}
}Public proxy 与 Dashboard/admin API proxy 必须拆成两个容器,避免 Dashboard 故障让公共 API 无法启动。私有 proxy 可对 3210 全路由代理为 admin-api.internal.manaio.cn,并对 6791 代理为 dashboard.internal.manaio.cn;它只受 internal ALB、VPN/CI vSwitch 或 SSH tunnel 访问。公共 api.manaio.cn 则使用上面的 fail-closed allowlist,不将 deploy、import/export、environment 或 Dashboard API 暴露到公网。Allowlist 来自固定源码的 router.rs 与 public_api.rs,不是可以永久不变的通用列表。
X-Manaio-Ingress-Class 是为待实现的 Backend patch / protocol gateway 预留的入口上下文;原始官方 image 会忽略它,它本身不是安全控制。Nginx 必须覆盖而不是转发客户端同名 header,Backend 的 Docker 端口也只能由 public/private proxy 访问;补丁还必须把 public/private class 传入 WebSocket 会话并在每次 Admin Authenticate 时执行拒绝逻辑。
Nginx 向客户端返回通用的 X-Request-ID,同时向 Backend 发送源码实际读取的 Convex-Request-Id,使 Nginx/SLS 与 Backend 日志可以关联;只发送 X-Request-ID 不会被当前 Backend 当作 request ID。Request ID extraction source
上面的 dynamic upstream 写法需要固定 Nginx Open Source 1.27.3 或更高版本,因为 resolve 参数在该版本起才对开源版可用。它通过 Docker embedded DNS 跟踪 Backend/Dashboard recreate 后的新 IP。如果组织必须用更旧 Nginx,则不得使用这段配置,并要在每次 Backend/Dashboard recreate 后强制 recreate 两个 proxy 容器。Nginx upstream resolve
ECS Security Group 必须保证公共 8080 只能被 Public ALB 回源访问,私有管理端口只允许 Internal ALB/VPN/受控 CI 网段。ALB Health Check 要显式使用 Host api.manaio.cn 访问 /version,未知 Host 由 catch-all 返回 404。Nginx 中固定 X-Forwarded-Proto https。示例故意只把直连 peer 的 $remote_addr 传给 Backend,因此 Backend 初期看到的是 ALB 回源地址,客户端 IP 以 WAF/ALB 日志为准。如果应用必须获得客户端 IP,应先固定 ALB 信任网段,实测 Listener 对客户端自带 XFF 的覆盖/追加语义,再用 Nginx Real IP module 按固定 trusted hop 归一化;不得信任未清洗 XFF 的 leftmost 值,也不能用它做鉴权。实际 body limit 必须根据业务上限、ALB/WAF 上限和两分钟上传窗口共同决定;256m 只是 PoC 起点,大于该值的受控 import 需要在管理面单独调整并验证各层限制。
镜像、Compose 与版本 BOM#
官方基线不等于生产模板#
[Source] 当前官方 Compose 包含 Backend、Dashboard 和一个 Backend data volume;默认将 3210、3211、6791 发布到宿主机,使用可变的 :latest,并且没有 TLS、反向代理、restart policy、资源限制、secret provider、日志轮转或多副本设计。它是一个可运行基线,不是完整生产模板。Official Compose at 7cce8fbc
两个服务必须使用同一 source SHA;Self-hosted changelog 明确说明不保证不同 Backend/Dashboard 版本的兼容性。发布工作流会给两个镜像打同一 github.sha tag,latest 则由人工移动。Self-hosted changelog Image release workflow Move latest workflow
候选 staging BOM(尚未验证)#
下表是 2026-08-25 的 PoC 起点,不是“建议生产版本”:
| 项 | 候选值 | 状态 |
|---|---|---|
| Backend source release | precompiled-2026-08-10-c0cb7ae | 官方 GitHub Release |
| Source commit | c0cb7ae17f54e14846c243c5332a8a5e6d0e19d4 | 固定 |
| Backend upstream image | ghcr.io/get-convex/convex-backend:c0cb7ae17f54e14846c243c5332a8a5e6d0e19d4 | 待镜像内容验收 |
| Backend OCI index digest | sha256:1f2044e3eac463ac78973b136c0baf72d4ada602611d853d6f99f280e29e0a98 | 2026-08-25 从官方 GHCR 核对 |
| Dashboard upstream image | ghcr.io/get-convex/convex-dashboard:c0cb7ae17f54e14846c243c5332a8a5e6d0e19d4 | 待镜像内容验收 |
| Dashboard OCI index digest | sha256:284a2638e0c1a4ec0c2327d8219776f3a426ca5824b81686ae4d9454dc0ce8ed | 2026-08-25 从官方 GHCR 核对 |
| Public protocol guard | Backend patch SHA + derived image digest,或 gateway source/image digest | 尚未实现;严格管理面隔离的生产阻塞 |
| Official Compose source | commit 7cce8fbc9a4f6125c0f287351170d76caae3ba3c;file SHA-256 483d2bb9a32b70036c29f0fc59c55acf1a1421147c45add2080fd2142cb98cfc | 与该 Release 中 Compose 逐字节一致 |
convex SDK/CLI 候选 A | 1.43.0 | Release source 中的同 revision 版本;仍不是官方兼容保证 |
convex SDK/CLI 候选 B | 1.45.0 | npm 当日 latest,比该 Backend Release 晚 11 天;必须与 A 对比实测 |
| Node.js / pnpm 研究基线 | Node.js 22.22.2;pnpm 10.34.5 | Release source 的 .nvmrc / packageManager;应用仓库仍要用 lockfile 单独验证 |
| React / Vite / TanStack | 使用应用仓库当前 lockfile 的精确组合 | 本次未提供应用仓库,尚未验证 |
| PostgreSQL | RDS PostgreSQL 17 High-availability Edition | 待验证 TLS/failover |
| OSS | 目标地域 internal S3 endpoint,virtual-hosted-style | 待完整 S3 PoC |
| Validated in staging at | 空 | 阻塞生产 |
镜像复制到 ACR 后,必须重新记录 ACR repository 中的 digest,并与拉取后的 image config/layer 校验结果关联;不应假设镜像跨 Registry 复制后的 manifest digest 必然不变。官方工作流显式设置 provenance: false,本次也没有验证到官方 Cosign 签名、SBOM 或 provenance attestation;ACR 扫描、KMS 签名和组织自己的 SBOM 生成是必要的供应链补强。原始官方 Backend image 不能满足本文要求的公网 Admin token 严格拒绝;若采用 source patch,BOM 还必须记录 upstream SHA、patch SHA、可重复构建方式、测试报告和 derived image digest,不能继续把上游 digest 写成生产运行 digest。
上表的运行镜像候选固定在 c0cb7ae…,而部分研究链接固定到更新的审计快照 7cce8fbc…。本次已对比 File Storage、PostgreSQL URL/CA、AWS/S3、Keybroker 和 Self-hosted 启动文件,这些与本文相关的实现在两个快照之间一致。生产验收仍必须以 c0cb7ae… 容器的真实行为为准,不能用更新 source 的阅读结果代替 staging。
独立 staging / production-candidate Compose 骨架(禁止与官方 Compose 合并)#
下面是独立文件的骨架,不是用 -f official.yml -f production.yml 叠加的 override。Compose 会合并 ports 列表,官方 environment 也可以覆盖 env_file;实测叠加会保留 3210/3211/6791 宿主机端口,并可让 RDS/OSS 变量回到空值。因此若日后进入生产,只能对这个经过验收的完整文件运行 docker compose -f docker-compose.production.yml config --quiet 和 up -d,不得与官方文件合并。示例不包含真实 Registry、密钥或规格,且仍需通过 staging。若选择 Backend source patch,CONVEX_BACKEND_IMAGE 必须改为 derived digest;若选择独立 protocol gateway,还必须把其固定镜像、网络、healthcheck 和失败策略加入 Compose。两者都未在本次实现,所以当前骨架不能直接用于生产:
name: manaio-convex-prod
services:
preflight:
image: "${PREFLIGHT_IMAGE:?PREFLIGHT_IMAGE is required}"
restart: "no"
network_mode: none
env_file:
- path: /run/convex/backend.env
format: raw
volumes:
- type: bind
source: /run/convex/rds-ca.pem
target: /run/secrets/rds-ca.pem
read_only: true
bind:
create_host_path: false
entrypoint: ["/bin/sh", "-ec"]
command:
- |
required="INSTANCE_NAME INSTANCE_SECRET CREDENTIALS_DIR POSTGRES_URL PG_CA_FILE
AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_REGION S3_ENDPOINT_URL
S3_STORAGE_EXPORTS_BUCKET S3_STORAGE_SNAPSHOT_IMPORTS_BUCKET
S3_STORAGE_MODULES_BUCKET S3_STORAGE_FILES_BUCKET S3_STORAGE_SEARCH_BUCKET
CONVEX_CLOUD_ORIGIN CONVEX_SITE_ORIGIN POSTGRES_MAX_CONNECTIONS
REDACT_LOGS_TO_CLIENT DISABLE_BEACON DISABLE_METRICS_ENDPOINT"
for name in $$required; do
value="$$(printenv "$$name" || true)"
test -n "$$value" || { echo >&2 "missing required variable: $$name"; exit 1; }
done
! printenv DO_NOT_REQUIRE_SSL >/dev/null 2>&1 || {
echo >&2 "DO_NOT_REQUIRE_SSL must be completely unset"; exit 1;
}
test -s "$$PG_CA_FILE" || { echo >&2 "missing RDS CA"; exit 1; }
proxy:
image: "${NGINX_IMAGE:?NGINX_IMAGE is required}"
restart: unless-stopped
ports:
- "${ECS_PRIVATE_IP:?ECS_PRIVATE_IP is required}:8080:8080"
volumes:
- type: bind
source: ./nginx-public.conf
target: /etc/nginx/conf.d/default.conf
read_only: true
bind:
create_host_path: false
depends_on:
backend:
condition: service_healthy
logging:
driver: json-file
options:
max-size: "20m"
max-file: "5"
networks: [public-backend]
admin-proxy:
image: "${NGINX_IMAGE:?NGINX_IMAGE is required}"
restart: unless-stopped
ports:
- "${ECS_PRIVATE_IP:?ECS_PRIVATE_IP is required}:8081:8081"
- "${ECS_PRIVATE_IP:?ECS_PRIVATE_IP is required}:6791:6791"
- "127.0.0.1:8081:8081"
- "127.0.0.1:6791:6791"
volumes:
- type: bind
source: ./nginx-admin.conf
target: /etc/nginx/conf.d/default.conf
read_only: true
bind:
create_host_path: false
depends_on:
backend:
condition: service_healthy
dashboard:
condition: service_started
logging:
driver: json-file
options:
max-size: "20m"
max-file: "5"
networks: [admin-api, admin-ui]
backend:
image: "${CONVEX_BACKEND_IMAGE:?CONVEX_BACKEND_IMAGE is required}"
restart: unless-stopped
env_file:
- path: /run/convex/backend.env
format: raw
expose:
- "3210"
- "3211"
volumes:
- convex-data:/convex/data
- type: bind
source: /run/convex/rds-ca.pem
target: /run/secrets/rds-ca.pem
read_only: true
bind:
create_host_path: false
tmpfs:
- /run/convex/credentials:mode=0700
depends_on:
preflight:
condition: service_completed_successfully
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3210/version"]
interval: 5s
timeout: 3s
retries: 12
start_period: 30s
stop_signal: SIGINT
stop_grace_period: "${BACKEND_STOP_GRACE_PERIOD:?BACKEND_STOP_GRACE_PERIOD is required}"
logging:
driver: json-file
options:
max-size: "100m"
max-file: "5"
networks: [public-backend, admin-api]
dashboard:
image: "${CONVEX_DASHBOARD_IMAGE:?CONVEX_DASHBOARD_IMAGE is required}"
restart: unless-stopped
environment:
NEXT_PUBLIC_DEPLOYMENT_URL: https://admin-api.internal.manaio.cn
NEXT_PUBLIC_LOAD_MONACO_INTERNALLY: "true"
expose:
- "6791"
depends_on:
backend:
condition: service_healthy
logging:
driver: json-file
options:
max-size: "20m"
max-file: "3"
networks: [admin-ui]
networks:
public-backend:
driver: bridge
admin-api:
driver: bridge
admin-ui:
driver: bridge
volumes:
convex-data:CONVEX_BACKEND_IMAGE、CONVEX_DASHBOARD_IMAGE、NGINX_IMAGE 与 PREFLIGHT_IMAGE 必须是经过扫描和签名验收的 ACR 完整地址加 @sha256:...。固定 Docker Engine 28+、Nginx Open Source 1.27.3+ 与支持 env_file.format: raw、bind.create_host_path: false 的 Compose plugin 精确版本,并在目标 ECS 验证 docker port、私网 curl 和 ALB Health Check。/run/convex/backend.env 和 RDS CA 由 KMS/bootstrap 写入 root-only host tmpfs,不进 Git、Cloud-init 日志或 SLS;RDS 密码中的 @、:、# 等字符必须先做 URI percent-encoding。CI 只运行 config --quiet,不把包含机密的完整 rendered config 写入日志。
但 host tmpfs 不等于 secret 不落盘:Compose 读取 env_file 后会把值写入容器 environment,Docker daemon 会将容器配置持久化到 /var/lib/docker,docker inspect 也能由高权限主体读取。本骨架为了与官方 entrypoint 兼容而保留 env_file,只适合作为 staging 起点。严格生产基线应先验证一个启动 wrapper:从只读 tmpfs secret files 读取机密、只在容器进程内 export 后 exec 官方 entrypoint,避免把值写进 Docker container config。无论是否完成 wrapper,ECS system disk、Docker data root、image/snapshot 和旧容器 metadata 都必须视为 secret-bearing,使用 KMS 加密、最小化 snapshot ACL/保留期,并在轮换确认后清除旧 container metadata。更新 secret file 后执行 docker compose restart 不会刷新既有容器 environment;轮换必须 recreate Backend,并按顺序重建依赖它的 proxy,再验证旧 secret version 和旧容器已经失效。
BACKEND_STOP_GRACE_PERIOD 不提供示例默认值,必须由 staging 的真实任务决定。固定源码收到 SIGINT 后先 drain HTTP,再执行应用 shutdown;与此同时,源码默认允许 Node.js Action 运行 600 秒、V8 Action 运行 1800 秒。单纯 ALB connection draining 不会证明 scheduled function、Action 或外部副作用已经静默,过短的 grace 会让 Docker 最终发送 SIGKILL。上线前要实现停写/停止新任务、观察后台任务排空、保证外部副作用幂等,并以最大可接受任务时长设定和演练该值。Backend shutdown source Action timeout source
preflight 只检查必需变量和 CA 是否存在。Backend 启动后还必须检查日志,一旦出现 Falling back to local storage 就标记部署失败,并通过 module push、File Storage、search、export 和 snapshot import 五类真实操作证明没有退回 SQLite/本地文件系统。restart: unless-stopped 不会因 unhealthy 自动重启容器;需要额外的功能探针、CloudMonitor 恢复动作或受控 watchdog。容器 CPU/内存/PID 边界、no-new-privileges、capability 和非 root 运行只能在实测镜像写路径/启动脚本后写入最终 Compose 与 BOM,不用未经验证的示例值冒充容量或安全结论。
三个 non-internal bridge 将 public proxy、admin proxy 与 Dashboard 的 east-west 可达面拆开,同时保留 Backend 对 RDS/OSS 的出站;它们不代表允许任意 egress。ECS Security Group、Cloud Firewall 或 DOCKER-USER 规则应限制容器出站到 RDS、OSS、DNS/NTP 和确有必要的服务,并阻断容器访问 ECS instance metadata。目标 ECS 还要实测 Docker 转发链与 DOCKER-USER 规则,不能假设 host Security Group 自动表达了全部容器间 ACL。
没有在示例中盲目开启 read_only。Backend 会使用 /convex/data/credentials、temporary directory 和本地快照/导入路径;Dashboard/Nginx 也可能需要 cache/pid/tmpfs。应先通过文件系统跟踪确定写路径,再对 root filesystem 设只读并精确配置 tmpfs。
Backend 配置清单#
| 配置 | 用途 | 机密 | 来源/轮换 |
|---|---|---|---|
INSTANCE_NAME | 稳定实例名和 DB name 派生 | 否 | 版本化配置;灾备不变 |
INSTANCE_SECRET | 签发/校验 Admin Key 的 64 个十六进制字符根机密 | 是 | KMS Generic Secret;轮换前做完整影响演练 |
CREDENTIALS_DIR | Backend 将实例名/根机密写入的目录 | 否 | 生产指向 /run/convex/credentials tmpfs;必须实测重启/灾备 |
POSTGRES_URL | RDS 连接,不带 DB path/query | 是 | KMS RDS Secret;双账号轮换优先 |
PG_CA_FILE | RDS CA 证书路径 | 否 | 只读挂载;证书更新前演练 |
AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY | OSS S3-compatible 访问 | 是 | 最小权限 RAM 凭据置于 KMS;受控重启轮换 |
AWS_SESSION_TOKEN | 使用 STS 时的 token | 是 | 只在凭据刷新流程实测后启用 |
S3_ENDPOINT_URL / AWS_REGION / 五个 bucket | OSS 存储路由 | 否 | 版本化配置;每环境隔离 |
CONVEX_CLOUD_ORIGIN | API / WebSocket / File Storage origin | 否 | https://api.manaio.cn |
CONVEX_SITE_ORIGIN | HTTP Actions origin | 否 | https://actions.manaio.cn |
POSTGRES_MAX_CONNECTIONS | Backend PostgreSQL pool 上限 | 否 | 源码默认 128;例如从 64 起步,按 RDS 预算和压测调整 |
BACKEND_STOP_GRACE_PERIOD | Compose 等待 Backend 优雅退出的时长 | 否 | 不设通用默认;按 Action/scheduled workload 排空实测固定 |
REDACT_LOGS_TO_CLIENT | 避免把详细 Backend 日志发给客户端 | 否 | 生产设为 true,并继续在 SLS 前脱敏 |
DISABLE_BEACON | 关闭 Self-hosted usage beacon | 否 | 根据数据出境政策决定;大陆主数据面建议设为 true |
DISABLE_METRICS_ENDPOINT | 是否关闭 metrics | 否 | 默认 true;若开启则只允许内网采集 |
| Admin Key | Dashboard / CLI 管理权限 | 是 | 生成后放 KMS;不注入前端 |
Admin Key 的官方生成命令是:
docker compose -f docker-compose.production.yml exec backend ./generate_admin_key.sh当前脚本根据 INSTANCE_NAME 和 INSTANCE_SECRET 生成 key。read_credentials.sh 即使接收了 KMS 注入的 INSTANCE_SECRET,也会把它写入 CREDENTIALS_DIR;因此本方案将该目录指向 tmpfs。如果仍使用默认 /convex/data/credentials,必须将 data volume、ESSD 和快照都当作含根机密的敏感数据,使用 KMS 加密并严格限制 snapshot ACL。即使使用 tmpfs,process environment、Docker container config 和高权限诊断面仍可能读取机密;Docker group 必须按 root 权限管理,轮换验收必须检查 docker inspect、Docker data root、旧容器与 disk snapshot 的泄露面。
源码中未发现 self-hosted Admin Key revoke list 或自动过期校验;再次生成 key 也不会自动撤销旧 key。Dashboard 应再加网络隔离与身份认证,不能只依赖 Admin Key。Credential bootstrap source Admin Key script Key validation source
版本兼容验收矩阵#
候选版本只有通过下表后才能转为生产 BOM。表中 source A 是 c0cb7ae…,SDK A 是同 revision 的 convex@1.43.0;source B 代表每次升级时待审核的下一个完整 source SHA,SDK B 是该次升级目标的精确 npm 版本(本次预研值为 1.45.0):
| Backend | Dashboard | SDK/CLI | 目的 | 当前状态 |
|---|---|---|---|---|
| source A | source A | SDK A:convex@1.43.0 | 建立同 source revision 基线 | 未验证,首选起点 |
| source A | source A | SDK B:预研 convex@1.45.0 | 验证先升 SDK/CLI,服务端保持 A | 未验证 |
| source B | source B | SDK A:convex@1.43.0 | 验证先升服务端时的旧客户端 | 未验证 |
| source B | source B | SDK B | 验证 Backend、Dashboard 与 SDK 整体升级 | 未验证 |
| source B | source B | SDK A 的已发布浏览器 bundle | 验证旧页面长会话和重连 | 未验证 |
每个组合必须在同一 staging 路径执行:
pnpm exec convex dev。pnpm exec convex deploy --env-file .env.selfhosted。- codegen 与 TypeScript typecheck。
- Query、Mutation、Action、Internal Function。
- WebSocket 实时订阅、断线重连与长时间后会话。
- HTTP Action、3211 并发/长请求上限、CORS、webhook 签名和防重放。
- schema/index 新增、backfill、失败和回退限制。
storage.generateUploadUrl()、上传、storage.getUrl()、Action 读写和删除。convex export --include-file-storage、在隔离实例执行 import 并比对。- Dashboard 登录、查看数据/日志/functions/env 与常用操作。
Convex Auth CLI 当前官方 README 明确说明不支持 Self-hosted 自动设置,需按手工步骤配置。如果产品依赖 Convex Auth,这是独立的 Go/No-Go 项,不应在普通 Query/Mutation 通过后就视为已验证。Self-hosted README
OSS、RDS 与入口 PoC#
OSS 兼容性测试#
在 staging 的五个独立 bucket 上验证:
- virtual-hosted-style 和 internal S3 endpoint;请求中不存在
aws-chunked。 - Put/Get/Head/List/Delete、条件请求、metadata、Content-Type 和 Unicode object key。
- multipart create/upload/complete/abort,上传取消,未完成 part 清理。
- checksum、ETag、SSE header 与 bucket default SSE-KMS。
- range GET、断点与并发读取。
- 五个用途的实际路径:function module、File Storage、search、export、snapshot import。
- Versioning、Lifecycle、误删恢复与 CRR 延迟。
- 403、404、429、5xx、超时和短暂不可达时 Convex 的重试/错误表现。
如果关键 API 不兼容,不放宽 bucket 为 public-read 来掩盖问题。可选路径是:
- 暂时使用已实测的 AWS S3-compatible 存储,并接受网络/合规代价。
- 在阿里云内运行 MinIO 或经证实的 S3 gateway,同时承担它自身的 HA、备份与运维成本。
- 放弃 Convex 内建 File Storage,但要注意 modules、search、exports 等仍需存储;不能只替换业务文件上传。
- 如果五个 S3 用途均无法可靠落地,停止 OSS 路线,不进入生产。
RDS 测试#
- 以最小权限账号完成首次启动、schema 创建和函数部署。
- 验证 CA、hostname、TLS 与错误证书拒绝。
- 从空库、有量数据与并发写入三种状态验证 migration。
- 压测连接数、事务冲突、query/mutation p95/p99 与 slow query。
- 执行 planned switchover,记录中断、悬挂、重试和完全恢复时间。
- PITR 到新 RDS instance,重建 whitelist/secret,完成只读对比。
WebSocket 与文件入口测试#
- 在 WAF + ALB + Nginx 全路径上保持 30 分钟、2 小时和跨 idle timeout 的心跳连接。
- 测试完全静默连接、Wi-Fi/蜂窝网切换、ALB 配置热更新、Backend 崩溃、ECS 重启和 Server Group 摘除。
- 验证旧订阅能重建,没有重复 Mutation 或订阅永久停滞。
- 在
api.manaio.cn与actions.manaio.cn公网入口以真实 Admin Key 负向测试Authorization: Convex、?adminKey=、/api/function、/api/run/*和 WebSockettokenType: Admin,全部必须被 protocol guard 拒绝;同一操作从私网管理入口应成功,正常 User token/匿名订阅不受影响。 - 对 Convex File Storage 测试小文件、中等文件、业务上限、并发、取消、错误 MIME 和两分钟边界;分开验证“未授权用户拿不到 URL”与“已泄露 bearer URL 仍可读”。
- 验证 WAF 不会误杀正常 HTTP Action、CORS preflight 和文件 POST,同时不把 WAF 当作 WebSocket frame 级防护。
部署、备份、恢复与升级#
首次部署顺序#
核验 FSL 适用范围、
manaio.cn备案和业务 RPO/RTO。创建两可用区 VPC/vSwitch、custom Security Group、RDS PostgreSQL 17 HA 与五个 staging OSS bucket。
在 ACR 导入候选 Backend/Dashboard,核对来源、扫描、签名,并记录 ACR digest。
创建 KMS secret、ECS RAM Role 和受控运维入口。
启动 ECS/Compose,检查
/version、RDS TLS、五个 OSS 用途与 SLS 日志。生成 Admin Key,立即写入 KMS,清理终端和 CI 日志中的可见副本。
通过 SSH tunnel/VPN 登录 Dashboard,不创建公网 DNS/ALB route。
在应用 CI 临时注入
.env.selfhosted,执行:pnpm exec convex deploy --env-file .env.selfhosted配置 WAF Enhanced ALB、Health Check、idle timeout、connection draining、SLS 与告警。
执行完整兼容矩阵、备份恢复和故障演练,通过后才创建生产 DNS 记录。
备份分层#
| 层 | 机制 | 要点 |
|---|---|---|
| 应用一致导出 | convex export --include-file-storage | 停写后的导出是应用级恢复基线;在隔离环境验证 import |
| 数据库 | RDS automatic data backup + log backup/PITR | 恢复到 new instance,必须演练 whitelist、secret 与切换 |
| 对象 | OSS Versioning + Lifecycle + 可选 CRR | 恢复 object version,清理历史版本/未完成 multipart;CRR 为异步且不复制 bucket 配置 |
| 配置 | 应用仓库 + KMS + BOM | 保存 schema/functions、lockfile、镜像 digest、INSTANCE_NAME、INSTANCE_SECRET、secret version |
| 运行宿主 | ECS image/snapshot | 只用于快速重建,不代替 RDS/OSS/导出 |
Convex 官方 CLI 默认导出不包含 File Storage,只有加 --include-file-storage 才会包含。停止写入后生成的该导出才是数据+文件的应用级一致恢复基线。RDS PITR 与 OSS Versioning/CRR 之间没有原子时间点;若使用物理层恢复,必须记录数据库时间点、object-version manifest,并比对孤儿/缺失对象。CRR 不复制 bucket policy、Lifecycle、CORS 和加密配置,必须由 IaC 重建。
导出不包含应用代码、环境变量或 pending scheduled functions;恢复时需要从固定 app commit/lockfile 重新 deploy,再恢复受控 env。导入的 --replace-all 会替换目标数据,只能在隔离验证或经批准的停机恢复窗口使用。Backup & Restore Export Import
升级与回滚#
- 阅读目标 source commit 的 Self-hosted changelog 与 Git diff,准备 A/B 兼容矩阵。
- 同步 Backend 和 Dashboard 的同一 source SHA 镜像到 ACR,扫描、签名并固定 digest。
- 在 staging 使用生产备份的脱敏副本完成 migration、网络、OSS 与旧客户端验收。
- 生产升级前做 RDS manual backup 和 Convex export。
- 对有数据迁移的版本进入维护窗口:阻断新写入,排空连接,做最终导出,再切换镜像。
- 观察 migration 日志,执行 smoke test 和数据对比。
- 如果新版本已改变数据库 schema,回滚镜像不等于数据回滚。必须按预先演练的 RDS PITR 或隔离实例
import --replace-all程序处理。
官方自托管升级指南同时提供 in-place 与 export/import 方案,并建议升级前先导出。Upgrading Self-hosted Convex
可观测性与密钥#
日志与指标#
- 用 LoongCollector 收集 Docker stdout/stderr,使用
json-filelogging driver 并验证 Docker/LoongCollector 版本兼容。SLS 采集 Docker 日志 - 分别开启 ALB Access Log 和 WAF Log Service;不要假设它们自动共享完整日志。
- 关注 ECS CPU/RSS/磁盘、container restart、
/version、HTTP 4xx/5xx、upstream latency、WebSocket 重连率、RDS 连接数/延迟/failover、OSS 错误、export/import 与 secret 轮换。 - 官方 Compose 默认
DISABLE_METRICS_ENDPOINT=true。若改为开启,Nginx/WAF 必须阻断公网/metrics,仅由内网 collector 访问。 - 在进入 SLS 前脱敏 Authorization、cookie、Admin Key、DSN、OSS AK/SK 和 presigned URL query string。Nginx 自定义格式不记录 query string 并把 Storage ID 替换为占位符,但 ALB/WAF access log 仍可能在完整 URI 中记录
/api/storage/upload?token=或/api/storage/<id>。云侧 raw log 必须最小权限、短保留,并在 SLS ingestion/transformation 中删除 query、归一化 Storage path;用 seeded canary token/ID 在 Nginx、ALB、WAF、SLS、告警与导出各日志面检索,任何明文命中都阻塞 G8/G9。HTTP Action webhook secret 同样不得放在 URL path/query。 - 告警分级建议:外部全不可用、数据完整性风险、RDS failover 久不恢复或 OSS 持续失败为 P1,用电话/短信加钉钉群同时通知;5xx/延迟/重连率或资源压力超阈为 P2,钉钉通知并在值班时限内处理;容量趋势、备份报表和成本异常为 P3,进入工单。具体阈值必须由 staging 基线和业务 SLO 定义。
KMS 与 RAM#
ECS 通过 Instance RAM Role 访问 KMS,KMS Application Access Point 只授予目标环境所需 secret。KMS Agent 只绑 loopback,不向 VPC 或公网开放。RDS runtime credential 优先使用 RDS Secret,INSTANCE_SECRET、Admin Key 和 OSS 凭据使用 Generic Secret。ECS 快速访问 KMS 管理 RDS Secret
不能在还没证明 Convex 可以热加载数据库/存储凭据时直接启用无协调的自动轮换。正确流程是新建 secret version,在 staging 验证重新获取/受控重启,保留回退窗口,再在生产切换。
合规与证书#
- 使用中国大陆阿里云资源对外提供网站/API,需要根据当前主体与接入商完成 ICP 备案或接入备案。即使根域已备案,从其他接入商迁入阿里云也可能需要 add access。ICP Filing Application Overview ICP server/access check
- 根域完成备案后,普通子域通常无需逐个单独备案,但资源接入商与业务性质仍需核对。
- 正式联网上线后,按规定期限办理公安互联网备案;网络安全等级保护是另一套定级/备案过程,不应与公安联网备案混为一谈。非经营性互联网信息服务备案管理办法 国际联网安全保护管理办法
- 注册一张覆盖
manaio.cn与常用一级子域的证书。私网的admin-api.internal.manaio.cn和dashboard.internal.manaio.cn需要包含这两个 exact SAN 的证书,或使用*.internal.manaio.cn;纯 SSH-only BOM 则可仅经 loopback 访问。*.manaio.cn不覆盖根域,也不覆盖这两个二级子域。
本次没有修改 manaio.cn 的 DNS、证书或阿里云资源。在 staging 验收和备案门禁通过前,不建议创建生产 api / actions 记录。
分阶段实施#
Phase 0:决策与合规#
- 核对
manaio.cn的 ICP/接入备案、公安备案与证书。 - 锁定用户分布、RPO、RTO、业务文件上限、峰值 WebSocket 数和费用上限。
- 在同一天比较候选地域的产品库存、多可用区组合、配额和价格。
Phase 1:完成 staging 底座#
- 建两可用区 VPC/vSwitch、custom Security Group、RDS PostgreSQL 17 HA、5 个 OSS bucket、ACR、KMS 和 SLS。
- 创建一台无 EIP 的 ECS,用 Workbench/堡垒机/Cloud Assistant 运维。
- 导入、扫描、签名并固定候选 Convex 镜像。
- 先通过受控 staging DNS 运行 WAF + ALB 全路径,不指向生产数据。
Phase 2:兼容性与故障演练#
- 完成 Backend/Dashboard/SDK 版本矩阵。
- 完成 RDS TLS、failover、PITR 和连接压测。
- 完成 OSS 全功能 PoC、文件两通道与恢复测试。
- 完成 WebSocket 长连接、Admin token 公网拒绝、HTTP Action 并发、后台任务排空、ECS 崩溃与 standby 切换。
- 完成 secret recreate 轮换、
docker inspect/snapshot 泄露面、Admin Key 泄露、ACR 不可达与全日志面 seeded-canary 演练。
Phase 3:受控上线#
- 签署上线 BOM 与 Go/No-Go 记录,附 image digest、app commit、lockfile、secret version 和备份时间点。
- 完成最终 RDS backup 和 Convex export,用生产配置 smoke test。
- 创建
api.manaio.cn/actions.manaio.cnDNS,逐步放量。 - 监控 5xx、WebSocket reconnect、RDS latency/connection、OSS error、WAF block 和容器重启。
- 在已批准窗口内保留老镜像、老 secret version 和恢复点,但不同时运行未验证的双 active。
Phase 4:运行与演练#
- 每次升级都先跑 A/B 兼容矩阵,Backend/Dashboard 使用同 source SHA。
- 定期演练 ECS 重建、RDS PITR、OSS 版本恢复、Admin Key/数据库/OSS 凭据轮换。
- 将实测的 RTO/RPO、失败步骤和回退条件写回 runbook,不只保留纸面配置。
生产 Go/No-Go 清单#
| 门禁 | 通过标准 | 当前状态 |
|---|---|---|
| G1 许可 | 仅为自有业务内部使用;业务模型变化时重审 | 与当前前提相符,但非法律意见 |
| G2 版本 | Backend/Dashboard/SDK/Node/app 的精确 BOM 通过矩阵 | 未通过 |
| G3 RDS | TLS、migration、连接上限、failover、PITR 恢复通过 | 未通过 |
| G4 OSS | 五个用途、multipart、checksum/SSE/range、异常与恢复通过 | 未通过 |
| G5 入口 | WAF + ALB + protocol guard + Nginx 下公共 HTTP/WS Admin token 全部失败;正常 WebSocket、3211 并发、HTTP Action 与 File Storage 通过 | 未通过 |
| G6 拓扑 | 接受单 active;优雅停机能排空 Action/scheduled workload,或多副本得到官方确认并实测 | 当前只能选单 active,排空未通过 |
| G7 恢复 | RDS PITR、Convex import、OSS 恢复与 ECS 重建联合演练;外部副作用无重复/遗漏 | 未通过 |
| G8 密钥 | 注入、recreate 轮换、撤销/泄露、docker inspect/snapshot 与所有日志面脱敏通过 | 未通过 |
| G9 可观测性 | 应用、ALB、WAF、RDS、OSS 异常可检索、可告警;seeded secret 在日志链路零命中 | 未通过 |
| G10 合规 | ICP/接入备案、证书与必要许可就绪;公安备案责任人/期限明确并在上线后按期完成 | 待账号/主体核验 |
G1–G10 必须全部通过才能向生产切流;其中公安备案按法定的上线后时限完成。
最终建议#
建议继续,但将工作分成两个决策点:
- 先批准一个不承载生产数据的阿里云 staging PoC,以 RDS、OSS、WebSocket 和恢复为主验收项。
- 只有 PoC 形成精确 BOM、真实测试报告、可重复恢复时间与合规审核结果后,才批准单 active 生产。
如果 OSS 兼容性失败、public protocol guard 无法可靠拒绝 HTTP/WebSocket Admin token、3211 并发不满足 SLO、业务不接受单 active 恢复窗口,或者强制要求严格无停机,就应停止当前架构,而不是放宽安全边界或在缺少官方依据时直接启动多个 Backend。替代选择是联系 Convex 确认 Self-hosted HA 与认证边界,改用可满足大陆要求的其他数据平台,或在业务允许时重新评估 Convex Cloud 与跨境数据/网络边界。
主要一手资料#
Convex:
- Self Hosting
- Self-hosted README
- Official Docker Compose
- Hosting on your own infrastructure
- Using Postgres or MySQL
- Using S3-compatible storage
- Upgrading Self-hosted Convex
- Self-hosted changelog
- File upload
- HTTP Actions
- Export
- Import
- FSL license
阿里云与合规:
- ALB Listener
- ALB Health Check
- WAF cloud-native mode FAQ
- RDS PostgreSQL High-availability Edition
- RDS PostgreSQL backup
- RDS PostgreSQL restore
- OSS S3 API compatibility
- AWS SDK 访问 OSS
- OSS Versioning
- ACR image scan
- ACR image signing
- SLS 采集 Docker 日志
- ECS 快速访问 KMS
- ICP Filing Application Overview
- 非经营性互联网信息服务备案管理办法
- 计算机信息网络国际联网安全保护管理办法