用Convex替代Supabase
Supabase 云端服务正在吞噬我没有赚取的利润。所以我做了任何合理开发者会做的事情:我考虑了自托管。免费,对吧?只需启动容器就行。
微信 ezpoda免费咨询:AI编程 | AI模型微调| AI私有化部署
AI工具导航 | Tripo 3D | Meshy AI | ElevenLabs | KlingAI | ArtSpace | Phot.AI | InVideo
我完成了编程工作,正准备在阳台上喝一杯皮尼亚科拉达。这时:Convex 警报。使用量即将达到上限。
我想喝酒的念头立刻消失了。
太长不看: Supabase 自托管需要 12 个 Docker 容器和最少 8GB RAM(在共享 VPS 上不可行)。Convex 自托管更简洁:一个容器,相同的 API,无限的带宽。唯一真正的陷阱是反向代理多端口路由。这里提供完整的操作指南,包括防止我犯那两个错误的迁移提示。

当你的基础设施在一次部署中从混乱变得禅意
第一反应:"不可能。现在才 3 号。配额两天前刚重置。"第二反应:找出哪个项目是罪魁祸首。第三反应:查看查询。就在那里——到处都是 db.query("metrics").collect()。每次变更都会进行全表扫描。12,500 行数据。四个响应式查询在每次单个文档变更时循环运行。
这是我写的代码。嗯,是 Claude Code 写的。一样的事。
自托管 Convex 这个想法已经在我脑海里酝酿了一段时间了。我已经验证这是可行的。我只是不知道怎么做。那个晚上,我找到了必须这样做的答案。
1、为什么我离开 Supabase
这个故事实际上开始得更早。在 Convex 之前,在带宽警报之前——就有一个 Supabase 问题。
Supabase 云端服务正在吞噬我没有赚取的利润。所以我做了任何合理开发者会做的事情:我考虑了自托管。免费,对吧?只需启动容器就行。
一天后。12 个容器配置完成,Traefik 经过三次失败尝试后终于路由到正确的服务,Kong 与我扔给它的每个环境变量争论不休,服务间网络由于某些原因我花了两个小时才调试清楚。我的 VPS 在冒汗。但它运行起来了。
然后我运行了 docker ps。
supabase-db.
supabase-studio.
supabase-kong.
supabase-auth.
supabase-storage.
supabase-realtime.
supabase-meta.
supabase-imgproxy.
supabase-vector.12 个容器。仅仅为了一个后端。Kong 在负载下就可以占用 2.5 GB。生产环境推荐最少 8GB RAM。我有一个运行着五个其他服务的 VPS。这是不行的。
(技术上你可以禁用未使用的服务来减少资源使用。但那样你就要在与所有其他服务共享的服务器上维护一个部分定制的堆栈。完全控制,他们这样说。)
我需要更轻量级的东西。这就是我找到 Convex 的方式,并且发现了一些我没有预料到的优势。如果你想了解全貌,我写过为什么 Convex 成为我使用 Claude Code 构建 AI 驱动 SaaS 的默认栈。简短版本:一切都是 TypeScript,默认响应式查询,部署时自动迁移。而 Claude Code 生成的代码实际上第一次就能编译,因为它在整个堆栈中使用一种语言,而不是在单独的上下文中处理 SQL 迁移、Deno Edge 函数和 TypeScript 前端。
我有一个顾虑:厂商锁定。Convex 在 2025 年 2 月开源了。这个顾虑消失了。
所以我迁移了。一切都运行正常。然后,三个月后,我几乎达到了 1GB/天的带宽限制。
2、警钟:821 MB / 1 GB
问题是我写的查询。(我是说,你知道是谁……😉)
db.query("metrics").collect() 是全表扫描。每一行,每次。在一个 12,500 行的表上。而且因为 Convex 响应式查询在被查询表中任何文档变更时都会重新执行,这些查询不是只运行一次。它们在持续运行。
其中四个:getTopContents 扫描所有指标以按内容分组,crossPlatformMetrics 跨平台聚合,速度查询扫描整个表以生成迷你图,mediumOverview 读取所有关注者数量分布。
在 3 月 3 日,这个组合消耗了我 1GB 每日配额中的 821 MB。到了晚上💀
在你说"只需添加索引"之前——我知道。我做到了。但故事并没有就此结束。
3、快速修复(先做这些)
在考虑自托管之前的 80/20 做法:修复查询。
在模式中添加索引:
metrics: defineTable({
content_id: v.id("contents"),
platform_name: v.string(),
period: v.string(),
})
.index("by_period", ["period"])
.index("by_platform_period", ["platform_name", "period"])
.index("by_content_id", ["content_id"]),
按期间过滤而不是扫描所有内容:
// 之前:读取表中的每一行
const allMetrics = await ctx.db.query("metrics").collect();
// 之后:只读取当前月份
const currentPeriod = new Date().toISOString().slice(0, 7); // "2026-03"
const metrics = await ctx.db
.query("metrics")
.withIndex("by_period", (q) => q.eq("period", currentPeriod))
.collect();
将迷你图查询限制在最近的期间。 如果你正在绘制 6 个月图表,构建你需要的期间字符串列表,并使用索引查询每个期间。不要为了六个数据点读取三年的数据。
这三个变化将我的带宽减少了大约 80%。Claude Code 编写了原始查询——快速生成,零索引,在没有问题之前工作正常。修复大约花了一小时。
但 1GB 的上限仍然存在。每个新功能都会削弱它。每篇导入的文章,每次实时仪表板刷新。配额是结构性的。优化只是争取时间。
自托管移除了上限。
4、决定:自托管还是留在云端?
如果你已经有一台运行 Docker 的服务器,自托管 Convex 额外花费正好是 $0。
一个容器。相同的 CLI,相同的客户端库,相同的 API。你将你的应用程序指向不同的 URL,其余代码不会改变。后端是开源的。你拥有它。
如果你已经配置了 Docker 和反向代理,如果你正在接近带宽限制,或者如果你希望在托管 SLA 不重要的项目上拥有完全的数据主权,这是有意义的。
如果你需要专门为此租用服务器——云层比你想象的更便宜——这是不合理的。如果你使用 Convex Auth 或 CDN 文件存储,这两者在自托管版本中尚不存在,也是一样的。如果凌晨 4 点调试 Traefik 听起来像是一个糟糕的夜晚,留在云端。我这样说没有任何评判。
我的情况:我已经有一台运行 Traefik、n8n 和其他几个服务的专用服务器。添加一个容器不花任何成本。计算很明显。
5、为什么是两个子域
Convex 自托管从单个容器暴露两个端口。端口 3210 用于后端——查询、变更、WebSocket。端口 3211 用于 HTTP 动作——你的 REST 端点。
两个端口,两个子域:
convex.yourdomain.com → port 3210 (backend + WebSocket)
convex-http.yourdomain.com → port 3211 (HTTP actions)
这个架构也是我遇到的唯一真正问题的根本原因。
6、Traefik 陷阱
如果你从未使用过 Traefik 的快速背景:它是一个反向代理,位于 Docker 容器前面,将传入流量路由到正确的服务。它的好处是通过读取容器上的标签自动配置自己——无需维护配置文件。大多数自托管教程使用它。它运行得很好。
直到一个容器暴露两个端口。
04:31:36 CET。第一次 docker compose up -d。Traefik 日志立即显示:
ERR Router convex-actions cannot be linked automatically
with multiple Services: ["convex-actions" "convex-backend"]
ERR Router convex-backend cannot be linked automatically
with multiple Services: ["convex-actions" "convex-backend"]
Convex 容器甚至还没有完成启动。Traefik 已经拒绝路由。
我的初始标签看起来像这样:
labels:
- traefik.enable=true
- traefik.http.routers.convex-backend.rule=Host(`convex.yourdomain.com`)
- traefik.http.routers.convex-backend.entrypoints=websecure
- traefik.http.routers.convex-backend.tls.certresolver=letsencrypt
- traefik.http.services.convex-backend.loadbalancer.server.port=3210
- traefik.http.routers.convex-actions.rule=Host(`convex-http.yourdomain.com`)
- traefik.http.routers.convex-actions.entrypoints=websecure
- traefik.http.routers.convex-actions.tls.certresolver=letsencrypt
- traefik.http.services.convex-actions.loadbalancer.server.port=3211
看起来正确。其实不然。
当单个 Docker 容器声明多个 Traefik 服务时,自动链接会中断。Traefik 看到两个服务,不知道哪个路由器映射到哪个服务。在你读过的每个教程中,都有一个容器和一个端口——所以映射是隐式的并且有效。有两个端口,你必须使其显式化。两行:
labels:
- traefik.enable=true
- traefik.http.routers.convex-backend.rule=Host(`convex.yourdomain.com`)
- traefik.http.routers.convex-backend.entrypoints=websecure
- traefik.http.routers.convex-backend.tls.certresolver=letsencrypt
- traefik.http.routers.convex-backend.service=convex-backend # ← this
- traefik.http.services.convex-backend.loadbalancer.server.port=3210
- traefik.http.routers.convex-actions.rule=Host(`convex-http.yourdomain.com`)
- traefik.http.routers.convex-actions.entrypoints=websecure
- traefik.http.routers.convex-actions.tls.certresolver=letsencrypt
- traefik.http.routers.convex-actions.service=convex-actions # ← this
- traefik.http.services.convex-actions.loadbalancer.server.port=3211
错误和修复之间两分钟。规则是:每当你通过 Traefik 的 Docker 提供程序从一个容器路由多个端口时,添加显式的 service= 标签。每次,无例外。
关于 Cloudflare 的一个说明: 我在这些子域的 Cloudflare 前面运行,代理禁用(仅 DNS,灰色云)。Let's Encrypt 需要直接访问你的服务器以颁发证书。如果你保持橙色云处于活动状态,你会遇到 SSL 冲突,你会花 20 分钟将错误归咎于错误的层——DNS,然后是 Traefik,然后是 Convex。为迁移禁用代理。
这台服务器已经运行了几个自托管服务,包括在 API 弃用迫使我重建后从头开始重建我的 AI 代理设置背后的基础设施。哲学是一样的:拥有堆栈,消除经常性成本,保持开发者体验完整。
7、防止所有这些的提示
如果你使用 Claude Code 生成 docker-compose,提前给它这个上下文:
Self-host Convex (ghcr.io/get-convex/convex-backend) behind Traefik.
Single container, TWO ports: 3210 (backend + WebSocket) and 3211 (HTTP actions).
Each port needs its own subdomain with SSL.
Traefik Docker provider with Let's Encrypt already configured.
四行。这就是触发显式 service= 模式的上下文。没有它,每个 LLM 都默认为标准教程假设:一个容器,一个端口,隐式映射。直到你有两个端口之前都运行良好。
它仍然会产生其他幻觉。Claude Code 总是这样。但至少不是这一个。
8、迁移(我几乎没做的部分)
我问 Claude Code:"我们可以将我的 Convex 云项目迁移到自托管实例吗?"
我几乎没有打开文档标签来弄清楚从哪里开始。Claude Code 已经阅读了它们。*"检查先决条件。"*然后:通过 SSH 连接到服务器,读取现有的 docker-compose,拉取镜像,生成管理员密钥。我看着。这就是我的工作——看着终端滚动,速度比我阅读的速度快,而我的 AI 实习生在十五分钟内完成了我心里预算两小时的任务。
十五分钟后它告诉我更新两个 DNS 记录。我做了。完成。
(记录在案:我确实监管了。非常用心。从我的椅子上。)
对于好奇的人,顺序是:在容器运行和 SSL 证书颁发后——Let's Encrypt 大约需要 15 秒,第一次 curl 返回退出代码 60,不要惊慌——迁移是五个步骤。
生成管理员密钥:
docker compose exec backend ./generate_admin_key.sh
以 convex-self-hosted| 开头。存储它,永远不要提交。
在项目根目录中创建 .env.self-hosted:
CONVEX_SELF_HOSTED_URL=https://convex.yourdomain.com
CONVEX_SELF_HOSTED_ADMIN_KEY=convex-self-hosted|your-key-here
从现在起每个 npx convex 命令都需要 --env-file .env.self-hosted。没有它,CLI 默默地以云端为目标。容易忘记,错过很危险。
首先从云端导出:
npx convex export --path convex-backup.zip
这是你的回滚。在做任何其他事情之前先做这个。
首先部署模式,然后导入——按此顺序:
npx convex deploy --env-file .env.self-hosted --yes
npx convex import --env-file .env.self-hosted convex-backup.zip
模式首先创建表和索引。导入需要它们存在。交换顺序,导入失败。
设置环境变量:
npx convex env list # list cloud vars first
npx convex env set --env-file .env.self-hosted YOUR_KEY "value"
两件让人困惑的事情:Next.js 中的 NEXT_PUBLIC_* 变量在构建时被烘焙。在 Vercel 中更改它们直到你重新部署之前没有任何作用。仪表板看起来已更新。应用程序仍在与云端通信。重新部署。
如果你有 HTTP 动作消费者——静态站点、外部 webhook——它们需要单独更新 HTTP 动作 URL。后端工作并不意味着动作工作。不同的端口,不同的子域,测试两者。
9、备份:现在是你自己的问题
#!/bin/bash
BACKUP_DIR="/path/to/backups"
DATE=$(date +%Y%m%d-%H%M)
VOLUME_NAME="your-project_convex_data" # check: docker volume ls
mkdir -p "$BACKUP_DIR"
docker run --rm \
-v "${VOLUME_NAME}:/data:ro" \
-v "${BACKUP_DIR}:/backup" \
alpine tar czf "/backup/convex-${DATE}.tar.gz" -C / data
find "$BACKUP_DIR" -name "convex-*.tar.gz" -mtime +30 -delete
添加到 cron,每天凌晨 3 点。卷名包括你的项目目录作为前缀——如果不确信,用 docker volume ls 检查。
在迁移后保持你的 Convex 云实例存活 48 小时。你的云端数据从未被修改。回滚是一个环境变量更改和一个重新部署。三分钟。
10、结果
延迟:在法国的 OVH 服务器上自托管运行大约 461ms 往返时间,相比 Convex 云端美国的 413ms。可以忽略不计。在欧盟服务器上的欧盟用户可能看到比基于美国的云端默认更好的延迟。
带宽焦虑:消失。那周我发布了两个新的仪表板功能,一次都没有想到配额。
Supabase 比较在基础设施层面上也站得住脚:一个空闲的 Convex 容器使用不到 200 MB RAM。Supabase 自托管从 12 个容器和推荐的 8GB 开始。对于共享 VPS 上的副业项目,这个差异就是一切。
大约在午夜,我关闭了终端。皮尼亚科拉达还在桌子上,不知何故。
原文链接: I Replaced Supabase with Convex and Self-Hosted It for Free. Here's the Full Guide.
汇智网翻译整理,转载请标明出处