AIRI 后端(AIRI Backend)本地部署与 Railway 生产部署完整指南
【免费下载链接】airi💖🧸 Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wishing to achieve Neuro-sama's altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi
本篇技术指南围绕 Project AIRI 的托管后端(hosted backend)展开,全面讲解server/目录的架构布局、本地一键启动流程(pnpm dev:backend+ Docker Compose 五服务栈)、以及基于 Railway 的生产部署方式(Resource API 与 Auth 双服务、Config File Path、健康检查与定向私有链接配置)。读完本文,你将掌握如何在本地跑起包含 PostgreSQL(vchord 向量扩展)、Redis、API、Auth 与 Caddy 网关的完整后端,并按照"服务到服务契约"在 Railway 上正确配置跨服务变量与迁移归属,避免常见的 JWT issuer 不匹配与迁移竞态问题。
后端在仓库中的位置与总体职责
AIRI 的托管后端源码集中在仓库的server/目录下。这里需要注意一个明确的边界:应用仓库只承载服务源码与数据库归属,生产部署拓扑(生产 Caddy 路由、OpenTelemetry Collector 配置、可观测性存储、Grafana 面板)存放在proj-airi/airi-railway中,避免部署拓扑在应用仓库内重复维护(参见 server/README.md)。
从仓库根目录看,后端整体由以下部分组成:
server/apps/api:资源 API(Resource API),承载业务域、数据库迁移与 API 运行时;server/apps/auth:独立的 Better Auth 与 OIDC 服务;server/packages/auth-shared:Auth 归属的数据库 schema 与主体验证(principal)契约;server/packages/server-sdk-shared:托管聊天 WebSocket 的 Eventa 契约;server/dev/caddy:仅本地使用的公共边缘路由(为共享的 Auth/API 来源服务);server/docker-compose.yaml:完整的本地 API + Auth + PostgreSQL + Redis + Caddy 栈。
本地运行:一条命令拉起完整后端
从仓库根目录执行:
pnpm dev:backend该命令在根 package.json 中定义为:
"dev:backend": "docker compose -f server/docker-compose.yaml up --build"它会读取 server/docker-compose.yaml,构建并启动全部服务,但只对外暴露 Caddy 网关:http://localhost:6112,API 与 Auth 的容器端口保持在内部网络,不直接暴露给宿主机。
本地栈的五个服务
server/docker-compose.yaml 定义了完整的本地后端栈(compose 项目名为proj-airi-backend):
| 服务 | 镜像 / 构建来源 | 关键配置 |
|---|---|---|
db | ghcr.io/tensorchord/vchord-postgres:pg18-v1.0.0 | 端口127.0.0.1:5435:5432,挂载./apps/api/sql/init.sql到/docker-entrypoint-initdb.d/init.sql,健康检查pg_isready |
redis | redis:7-alpine | 端口127.0.0.1:6379:6379,健康检查redis-cli ping |
api | server/apps/api/Dockerfile | 启动命令pnpm -F @proj-airi/api-server start,依赖 db/redis 健康后启动 |
auth | server/apps/auth/Dockerfile | 启动命令pnpm -F @proj-airi/auth-server start,依赖 api 健康后启动 |
caddy | caddy:2-alpine | 端口127.0.0.1:6112:3000,挂载./dev/caddy/Caddyfile |
注意两个细节:
数据库使用 vchord PostgreSQL。初始化脚本 server/apps/api/sql/init.sql 的内容只有一行:
CREATE EXTENSION vchord CASCADE;也就是说本地数据库首次初始化时会启用
vchord向量扩展——这是 AIRI 后端数据库带向量检索能力的直接证据(vchord 为 pgvector 兼容的向量索引方案)。API 与 Auth 通过 env_file 读取可选环境文件:
./apps/api/.env与./apps/api/.env.local(required: false),并以environment块注入核心变量。本地栈中 API 的AUTH_SERVER_URL与 Auth 的PUBLIC_URL均被显式设为http://localhost:6112,与 Caddy 网关地址一致,保证本地 JWT issuer 校验一致。
本地 Caddy 路由规则
server/dev/caddy/Caddyfile 是本地公共边缘的唯一入口,规则清晰对应生产语义:
:3000 { route { @internal path /internal /internal/* respond @internal 404 @auth path /api/auth /api/auth/* /auth /auth/* /.well-known/oauth-authorization-server/api/auth reverse_proxy @auth auth:3000 reverse_proxy api:3000 } }要点:
/internal/*在公共边缘直接 404 拒绝,保证内部 Auth→API 调用边界不暴露;- Auth 相关路径(
/api/auth/*、/auth/*以及 OAuth 授权服务器发现端点)反向代理到auth:3000; - 其余请求全部转发到
api:3000。
按服务单独开发的命令
若需要源码级调试,可以跳过 compose 而分别启动两个服务。API 侧(见 server/apps/api/README.md):
pnpm -F @proj-airi/api-server dev pnpm -F @proj-airi/api-server typecheck pnpm -F @proj-airi/api-server exec vitest run pnpm -F @proj-airi/api-server buildAuth 侧(见 server/apps/auth/README.md):
pnpm -F @proj-airi/auth-server devAuth 服务从自身目录读取.env.local。PUBLIC_URL是经 Caddy 对外呈现的公共 issuer 来源;RESOURCE_SERVER_URL是用于内部调用的私有资源 API 地址。
两个服务的职责边界
Resource API(@proj-airi/api-server)
根据 server/apps/api/README.md,其职责包括:
- Hono 业务 API 与 WebSocket 端点;
- 角色(characters)、聊天(chats)、提供商(providers)、Flux、Stripe、模型路由与计费;
- 共享数据库的 PostgreSQL 迁移所有权:Drizzle 在启动时读取检入(checked-in)的
drizzle/journal 与 SQL 文件; - Redis 缓存、配置 KV 与跨实例 Pub/Sub;
- 通过公共 JWKS 本地校验 Auth 签发的 OIDC JWT。
从源码布局看,API 路由覆盖了routes/下的chat-ws(v1/v2 两代 WebSocket 协议,含未认证对等方与 payload 限流)、openai/v1(OpenAI 兼容网关,含 billing、telemetry、traffic-control 中间件与 chat-completions/speech-catalog/speech-generation 操作)、stripe(checkout/webhook 与价格目录)、characters、chats、providers、voice-packs、flux、audio-speech-ws、audio-transcription-stream等模块;services/domain/下则承载计费、llm-router(并发账本、配置加载、密钥轮换、错误映射)、llm-tracing、provider-catalog、user-deletion 等业务域。它同时提供/readyz健康检查与 OpenTelemetry 仪表(见src/otel/gauges/下的 db-pool、tts-pool、ws-online-users 等 gauge)。
Auth(@proj-airi/auth-server)
根据 server/apps/auth/README.md,其职责包括:
- Better Auth 会话、社交登录、magic-link、密码与 OIDC 流程;
/api/auth/*、/auth/*与认证发现端点;- Auth 归属的 Redis 配置、事务邮件与认证遥测;
- 删除业务数据前通过私有网络调用资源 API。
其代码刻意保持扁平,主要边界为:auth.ts(Better Auth 配置与身份生命周期钩子)、routes.ts(完整公共 Auth HTTP 面与请求认证)、server.ts(依赖组合、健康检查与进程生命周期)、resource-api.ts(唯一的 Auth→资源 API 私有边界)、rate-limit.ts与otel.ts(跨路由运维策略)、email.ts与oidc-jwt-bearer.ts(大型外部集成模块)。
明确不做的事:产品 API、计费、模型路由、聊天或 WebSocket 业务状态;导入server/apps/api的模块;在正常进程启动时运行共享数据库迁移历史。Auth 的表与主体验证契约全部来自@proj-airi/auth-shared,共享迁移文件由 API 启动时由 Drizzle 读取——迁移所有权始终在 API 侧。
Railway 生产部署
API 与 Auth 是同一仓库构建出的两个独立 Railway 长期运行服务。核心约束是:每个服务的 Root Directory 必须保持在仓库根目录,因为两份 Dockerfile 都需要从该构建上下文复制 workspace 清单与共享包。
两份 Dockerfile 的构建输入
API 的 server/apps/api/Dockerfile(生产用 Railway 专用版位于 server/apps/api/production/railway/Dockerfile,内容一致)基于node:24-alpine,启用 corepack,复制pnpm-lock.yaml、pnpm-workspace.yaml、package.json、tsconfig.json、patches/,再复制server/apps/api、server/packages/auth-shared、server/packages/server-sdk-shared,随后以--frozen-lockfile --ignore-scripts安装依赖,先后构建@proj-airi/server-sdk-shared与@proj-airi/api-server,并以非 root 用户airi运行,EXPOSE 3000。
Auth 的 server/apps/auth/Dockerfile 与之类似,但只复制server/apps/auth与server/packages/auth-shared(它不消费 server-sdk-shared),安装命令带--filter @proj-airi/auth-server...过滤。
在 Railway 中显式配置 Config File Path
由于仓库根目录下没有默认的railway.toml,必须为每个服务显式指定 Config File Path:
| 服务 | Config File Path | 公共角色 | 私有依赖 |
|---|---|---|---|
| Resource API | /server/apps/api/railway.toml | 产品与资源 API | Auth issuer 与 JWKS |
| Auth | /server/apps/auth/railway.toml | Better Auth 与 OIDC issuer | Resource API 的删除端点 |
server/apps/api/railway.toml 的实际内容:
[build] builder = "DOCKERFILE" dockerfilePath = "/server/apps/api/production/railway/Dockerfile" watchPatterns = [ "server/apps/api/**", "server/packages/auth-shared/**", "server/packages/server-sdk-shared/**", "package.json", "pnpm-lock.yaml", "pnpm-workspace.yaml", "tsconfig.json", "patches/**" ] [deploy] startCommand = "pnpm -F @proj-airi/api-server start" healthcheckPath = "/readyz" healthcheckTimeout = 100server/apps/auth/railway.toml 与之对应,dockerfilePath 为/server/apps/auth/Dockerfile,watchPatterns 不包含server/packages/server-sdk-shared/**,启动命令为pnpm -F @proj-airi/auth-server start。
每一份 config 都自持 Dockerfile、启动命令、/readyz健康检查与 watch 模式。只有当变更触及该服务本身、其复制的某个共享包、或复制的根构建输入时,该服务才会触发部署——例如只修改server/apps/api/**不会让 Auth 重新构建。
服务到服务的变量契约(关键)
共享数据库、Redis 与可观测性变量,应使用Railway 引用变量(reference variables)在两个服务间传递,而不是复制敏感值。两个方向的私有链接按如下配置:
| 消费方 | 变量 | 值来源 | 用途 |
|---|---|---|---|
| Resource API | AUTH_SERVER_URL | Auth 的规范公共 issuer URL | JWT issuer、audience 与公共 JWKS 身份 |
| Resource API | AUTH_SERVER_INTERNAL_URL | Auth 的 Railway 私有域名 | 私有网络内拉取 JWKS;不会改变 issuer 校验 |
| Auth | PUBLIC_URL | Auth 的规范公共 issuer URL | Better Auth 与 OIDC issuer URL;必须等于 API 的AUTH_SERVER_URL |
| Auth | RESOURCE_SERVER_URL | API 的 Railway 私有域名 | 删除用户业务数据前的私有调用 |
两个最容易踩的坑
- issuer 必须严格一致:
PUBLIC_URL与AUTH_SERVER_URL必须是同一个值。否则 API 校验 JWT 的 issuer/audience 会失败。AUTH_SERVER_INTERNAL_URL只是私有 JWKS 拉取通道,即便它指向私有域名,也不影响 issuer 与 audience 的校验逻辑(issuer 依旧取AUTH_SERVER_URL)。 /internal/*必须保持私有:Auth 通过私有域名调用 API 的内部路径;公共路由(Caddy 边缘)必须拒绝/internal/*,且 API 服务不应有独立的公共入口(参见 server/apps/api/README.md 与 Caddyfile 中respond @internal 404的实现)。
代理信任与限流
只有在直接接收 Railway 代理流量的服务上才设置RATE_LIMIT_TRUSTED_PROXY=railway。该标志会告知限流中间件(server/apps/api/src/middlewares/rate-limit.ts与 Auth 的rate-limit.ts)信任 Railway 代理转发,从而正确解析客户端真实 IP;误设会导致任意请求伪造X-Forwarded-For绕过限流。
迁移归属与部署成功判据
- API 是共享数据库迁移的唯一 owner:不要给 Auth 添加 Railway pre-deploy 迁移命令,也不要让 Auth 启动时执行共享迁移。共享迁移在 API 启动时由 Drizzle 读取
drizzle/journal 与 SQL 文件执行(API 侧迁移文件位于 server/apps/api/drizzle/ 下,包含 0000~0022 共 23 个 SQL 迁移及对应 snapshot)。 - 部署成功 ≠ 就绪:任一个服务部署后,Railway 必须从该服务的
/readyz收到200。健康检查超时在 railway.toml 中配置为healthcheckTimeout = 100。只有当/readyz返回 200,才说明服务能连上其依赖(数据库、Redis、对端服务);仅凭"部署完成"不能证明服务可达。
包边界与契约的归属
AIRI 对后端包的放置有明确规则(见 server/README.md):
- 前端应用保持在根
apps/下; - 定义了资源 API 协议的托管后端包可以放在
server/packages/下,即使前端消费其生成的契约(例如 server/packages/server-sdk-shared 承载托管聊天 WebSocket 的 Eventa 契约); - 跨运行时的服务器 SDK 与协议包仍放在根
packages/下,因为 Web、Electron、插件与独立服务都要消费它们; - server/packages/auth-shared 是 Auth 归属的数据库 schema 与主体验证契约:Auth 表结构集中于此,API 与 Auth 均引用它,而二者互不 import 对方的应用模块(见 server/apps/api/README.md:
no module under server/apps/auth is imported)。
这种"协议归服务、跨运行时归根包"的划分,配合 Railway 的 watchPatterns 精准控制构建触发范围,让 API 与 Auth 两个服务在共享同一仓库、同一数据库的前提下,仍能保持独立部署与演进。
生产可观测性拓扑的去向
生产环境的 Caddy 路由、OpenTelemetry Collector 配置、可观测性存储与 Grafana 面板不在本应用仓库内,而统一维护在proj-airi/airi-railway,以保持"应用仓库 = 代码与契约"、"部署仓库 = 拓扑与观测"的职责分离。应用仓库内只保留本地开发用的 Caddyfile 与 server/docker-compose.yaml,如需扩展本地可观测性,可围绕server/apps/api/src/otel/的仪表与脚本(server/apps/api/src/scripts/otel/下的 http-smoke、ws-smoke、smoke)自行接入。
小结
AIRI 后端的核心设计可以概括为三条主线:
- 本地一条命令可复现:
pnpm dev:backend拉起 vchord PostgreSQL + Redis + API + Auth + Caddy 完整栈,且只有 Caddy 网关暴露在localhost:6112; - 生产两服务强契约:API(资源域 + 迁移 owner)与 Auth(身份与 OIDC issuer)共享同一数据库,通过
PUBLIC_URL == AUTH_SERVER_URL、AUTH_SERVER_INTERNAL_URL(私有 JWKS)、RESOURCE_SERVER_URL(私有删除回调)四个变量 + 私有/internal/*边界完成安全互通; - 部署粒度精细可控:每份 railway.toml 自持 Dockerfile、start command、
/readyz与 watchPatterns,只有触及服务自身或其复制的共享包才触发部署,配合"部署成功需以/readyz200 为准"的判据,保障变更安全。
如需在 Railway 上部署,请严格按照"Root Directory 保持仓库根目录 + Config File Path 指向各自 railway.toml + 定向私有链接 + 迁移仅由 API 执行"的契约配置;任何对PUBLIC_URL/AUTH_SERVER_URL的改动,都需要同时同步到两个服务后一并部署验证。
【免费下载链接】airi💖🧸 Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wishing to achieve Neuro-sama's altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考