Skip to content

Changelog 2026 04 17 Self Hosted Infra

longsizhuo edited this page Apr 17, 2026 · 1 revision

2026-04-17 改动日志:自建基础设施重构

一句话总结:Neon 免费额度耗尽触发的连锁重构——数据库迁到自建 PG、 pgAdmin 上线、sa-token cookie 跨子域共享、Infisical 接管密钥管理、 开发者和管理员入口分开、文档归位。

对应 PR:

背景 / 触发点

凌晨 01:06 Neon 来邮件说 involution-hell 项目当月 100 CU-hour 计算配额耗尽, 计算节点即将被暂停。业务立刻开始报错:

  • /api/events 500(backend 连不上 Neon)
  • 前端 AI 对话 onFinish 里的 prisma 调用失败(前端 Prisma 也指向 Neon)
  • 管理员后台接口全部 500

原本 Neon 只是"免费的 managed Postgres",挂了之后复盘才发现它同时承担了 很多角色,每一个都是单点:

  1. 后端 Spring Boot 的主库
  2. 前端 Prisma 的直连目标(AI 对话、CI 脚本)
  3. 前端 NextAuth 时代遗留的 users/accounts/sessions
  4. Chat/Message 聊天历史

这次重构把它们逐个剥离。

架构变化前后

Before(2026-04-17 之前)

Neon Cloud (AWS Sydney, 免费 tier)
  │
  ├──◀── Spring Boot backend(pooler 直连)
  ├──◀── Vercel Next.js Prisma 运行时(prisma.chat.upsert 等)
  └──◀── CI 脚本(backfill / leaderboard / test)

凭据管理:每个 .env 手工维护,prod/dev 两份漂移

After(今天)

/home/ubuntu/involution-hell/ 宿主机
  │
  ├── postgres:18-alpine 容器(业务主库 involution_hell + Infisical 专用 infisical 库)
  │     │
  │     ├──◀── Spring Boot backend 容器(内网 postgres:5432)
  │     └──◀── Infisical 容器
  │
  ├── pg-backup 容器(每天 03:00 pg_dump -Fc 到 pg-backups 卷,保留 30d/8w/12m)
  │
  ├── pgAdmin 容器(127.0.0.1:8082,SERVER_MODE=False)
  │     ▲ Caddy forward_auth /api/admin/devtool-check
  │     │
  │     └── api.involutionhell.com/admin/pgadmin/* ←── admin 专属
  │
  ├── Infisical 容器(127.0.0.1:8090)
  │     ▲ Caddy 直通反代(不套 gate)
  │     │
  │     └── secrets.involutionhell.com ←── 所有登录 dev 都能访问(RBAC 在 Infisical 内部)
  │
  └── Caddy(global-caddy-gateway, host 网络)
        终端 TLS, ACME, forward_auth, 按子域路由

Vercel Next.js frontend
  │ onFinish 不再 prisma.chat.upsert,改 fetch BACKEND_URL/api/chat/sessions/save
  │ 登录时把 satoken 同步写 Domain=.involutionhell.com cookie(跨子域共享)
  │ /admin/database 按钮 target=_blank 开 pgAdmin(不 iframe)
  │ /u/[username] 增加"密钥管理"按钮(owner 可见,开 Infisical)

时间线 & 关键决策

Phase 1:数据抢救 & 迁移

  1. pg_dump -Fc 从 Neon pooler 拉全库(14 张表、5487 行、264 KB 压缩)
  2. 先导入临时 involution_hell_test 对行数、约束、索引做 diff 确认 parity
  3. 停 backend → 再 dump 一次(cutover)→ drop + restore 到本地 involution_hell
  4. .env PGHOST 从 Neon endpoint 改为 postgres / 127.0.0.1(prod / dev 分别)
  5. 重建 backend 容器,/api/events 返回 4 条真实数据 ✅

Phase 2:pgAdmin 接入(坎坷)

踩过三种接入方式才定型:

  1. iframe 嵌 api.involutionhell.com:pgAdmin 的 CSRF cookie 走 SameSite=Lax, 跨站 iframe POST 不带 cookie,登录永远报 "CSRF session token is missing"
  2. Next.js rewrite 代到同源:pgAdmin 发绝对 URL 重定向带自己 host, 浏览器跟着跳 localhost:8082 变 ERR_CONNECTION_REFUSED
  3. 新标签打开(最终方案):pgAdmin 在自己 origin 里管自己的 session, 一切正常

中途还发现 pgAdmin SERVER_MODE=False 对公网完全无认证 —— 扫到路径就能 操作生产 DB。一度需要紧急 Caddy 503 封掉路径。最终方案:

Phase 3:Caddy forward_auth 接管 admin gate

api.involutionhell.com/admin/pgadmin/* {
    forward_auth 127.0.0.1:8080 {
        uri /api/admin/devtool-check    # sa-token + @SaCheckRole("admin")
        copy_headers Cookie
    }
    reverse_proxy 127.0.0.1:8082
}

为此新增:

  • 后端 AdminInfraController.devtoolCheck(泛化自原 pgadminCheck), @SaCheckRole("admin"),空响应体,只靠状态码说话
  • 前端 lib/use-auth.tsxsyncTokenCookie:登录成功 / 每次刷新时把 localStorage.satoken 复制一份到 Domain=.involutionhell.com 的 cookie, 浏览器跨子域自动带。localhost dev 不设 Domain
  • Caddy 剥 pgAdmin 默认的 X-Frame-Options: DENY,换成 CSP frame-ancestors

Cookie 同步这一步是关键——否则用户直连 api.involutionhell.com/admin/pgadmin/ 浏览器发不出 satoken header,forward_auth 永远 401。

Phase 4:前端 Prisma 退场

原本 app/api/chat/route.tsstreamText.onFinish 直连 prisma 写 Chat + Message,Neon 换了之后前端 Prisma 还指向 Neon → 数据写旧库, 和后端读自建 PG 分叉。

方案 A 落地(对比 B: 前端连自建 PG via Cloudflare Tunnel,C: 保持现状双写):

  • 后端加 POST /api/chat/sessions/save,body {chatId, userMessage, assistantMessage}
  • @Transactional 原子写 chat upsert(ON CONFLICT COALESCE userId) + 两条 message
  • 前端 onFinish 一次 fetch 搞定,Vercel AI SDK 的 streamText / toUIMessageStreamResponse 一字没改,UX 无感
  • scripts/ 下 3 个 Prisma 脚本(test / backfill / leaderboard)暂未迁移,P2

Phase 5:数据清理发现(副产品)

迁移期间查 user_accounts 表时发现:

  • longsizhuo id=177 roles=admin,user github_id=NULL(孤儿)
  • github_114939201 id=46 roles=user github_id=114939201(OAuth 实际命中)

OAuth 登录 AuthService.loginByGithubfindByUsername("github_<id>") 匹配, 永远找不到人类 username 的行。之前手工 insert 挂 admin 的管理员全变成孤儿。

热修:merge id=177 的 admin 角色到 id=46,删孤儿。
根治(待做):AuthServicefindByGithubId fallback,防止再次踩坑。

Mira190 / Crokily 还有同样问题,需要他们的 GitHub 数字 ID 才能修。

历史遗留 users / accounts / sessions / verification_token 四张表 (NextAuth 2026-03-25 退役前写的 85/85/121/0 行)当前无业务挂钩(Chat.userId 全 NULL),选项 A/B/C 处理方案待决。

Phase 6:Infisical 自托管上线

/home/ubuntu/infisical/ 新建 compose:

  • infisical/infisical:v0.120.0-postgres 镜像
  • 复用 involution-postgres 容器单建 infisical 库 + infisical superuser
  • 独立 infisical-redis 容器(不复用 umami-redis-1,后者会被 flushdb 清空)
  • 只绑 127.0.0.1:8090,公网由 Caddy 反代

Caddy secrets.involutionhell.com

  • 不套 forward_auth(与 pgAdmin 刻意不同)
  • Infisical 自带 GitHub OAuth + project-level RBAC + 审计 log
  • 面向所有协作者开放登录页,权限在 Infisical 内部细分

bootstrap .env/home/ubuntu/infisical/.env,极简 4 项):

  • ENCRYPTION_KEY(32 字符 hex,丢了所有业务 secrets 永久解不开,须离线备份)
  • AUTH_SECRET(JWT 签名)
  • DB_CONNECTION_URI
  • REDIS_URL

Phase 7:.env 大清洗

开发 .env 里历史累积的前端专属变量(NEXT_PUBLIC_* / R2_* / VERCEL_OIDC_TOKEN / STACK_* / DATABASE_URL / POSTGRES_URL* / INDEXNOW_* / INTERN_KEY / NEON_PROJECT_ID / AUTH_SECRET)全部清出,它们归前端 Vercel 环境变量管,后端根本不读。

同时:

  • prod OPENAI_* 从 sk- + gpt-4.1 切到 GLM(open.bigmodel.cn/api/paas/v4 + glm-4.6v-flash)。 代码本来就是 OpenAI 兼容协议,URL + Key + Model 三件套指哪打哪,切厂商不改代码
  • PGADMIN_PASSWORD 换成 openssl rand -base64 24 强随机值(虽然 SERVER_MODE=False 时没人用这个密码登录,但镜像启动硬要求存在)
  • 删掉 CADDY_HTTP_PORT / CADDY_HTTPS_PORT 这俩 dead vars
  • 删掉 NextAuth 遗留的 AUTH_SECRET

最终 prod vs dev .env 的差异只剩必要的部署差异:

差异项 prod dev
PGHOST postgres(Docker DNS) 127.0.0.1(宿主机走端口映射)
SERVER_PORT 8080 8081
AUTH_URL https://involutionhell.com http://localhost:3010
AUTH_GITHUB_*_DEV 开发用 OAuth App
PGADMIN_* / POSTGRES_* 存在 不需要(dev 不跑 compose)

Phase 8:开发者 vs 管理员入口分离

最后一个设计澄清:pgAdmin 和 Infisical 访问模型不同——

工具 入口位置 公网暴露 内部 auth
pgAdmin /admin admin 卡片(admin-only 可见) 有 forward_auth SERVER_MODE=False 无登录
Umami 同上 有 forward_auth(你自己处理) 本地账号
Infisical 个人主页 /u/[username] 的"开发者选项"块(owner 可见) 直通反代 GitHub OAuth + project RBAC

新加 DeveloperToolsIfOwner 组件和 AdminLinkIfOwnerAdmin 并列但放宽条件—— 任何本人访问自己主页都能看见"密钥管理 ↗"按钮。

非显而易见的坑点备忘

  1. Caddy 挂载 Caddyfile 要 restart 不能只 reload:Edit 工具做的是 atomic rename, inode 变了之后 bind mount 不自动换到新 inode,caddy reload 读的是已 staled 的文件描述符。要么直接 edit in-place 不换 inode,要么 docker restart
  2. pgpass 文件必须 UID 5050 + 0600:pgAdmin 容器用户 UID=5050, 主机 pgpass 所有权不对会 restart loop,日志 "can't open /pgpass: Permission denied"
  3. Neon 临时 endpoint 不能做公网 DNSep-xxx.neon.tech 是 Neon 管理的, 迁出后这些 hostname 立即失效,.env 要一次性改完
  4. git 仓库本地残留 CI token headergit config http.extraheader 里留着 过期 ghs_ token 会盖掉 URL 内联的 PAT,git push 持续 401 但诊断很难。 git config --local --unset-all http.https://github.com/.extraheader 清掉
  5. pgAdmin 的 SERVER_MODE=False 公网暴露是 critical 漏洞:任何扫到路径 的人都能对生产 DB 跑 SQL。外层必须有 gate,或者切 SERVER_MODE=True
  6. cookie 跨子域必须 Domain=.involutionhell.com 而非 Domain=involutionhell.com: 前者是通配所有子域共享,后者严格匹配主域+api子域但不含 www 子域。前缀那个点关键

待办清单(P2,非本次完成)

  • AuthService.loginByGithubfindByGithubId fallback,防孤儿 admin 再现
  • Mira190 / Crokily 孤儿 user_accounts 行的 github_id 回填
  • users / accounts / sessions / verification_token 四张 NextAuth 遗留表处置决策
  • scripts/ 下 3 个 Prisma 脚本(test/backfill/leaderboard)迁到自建 PG 或后端 API
  • 把业务 secrets 从 .env 全部导入 Infisical,compose 启动改走 infisical run
  • Neon involution-hell 项目保留 30 天观察期后删除
  • Neon involutionHell(31.39 MB 第二个项目)盘点内容决定去留