运行非生产实例

一个 staging(或者预览、临时)实例与生产环境是同一套栈,由同一份 deploy/docker/docker-compose.yml 启动,只是跑在一台你不介意弄坏的机器上。本页只讲它与生产部署不同的那些部分:compose 文件实际会启动什么、怎么让它不暴露在网络上、怎么在不开出提权口子的前提下拿到一个管理员账号,以及怎么把数据库整个丢掉。

生产部署的操作流程见部署指南;想零依赖地过一遍路由引擎,见快速开始

compose 文件会启动什么

deploy/docker/docker-compose.yml 定义了五个服务:

服务

默认启动

说明

backend

FastAPI 网关

frontend

Next.js 控制台

postgres

postgres:16

pgadmin

Compose 的 admin profile

codex-oncall

Compose 的 oncall profile,见 Codex On-Call

这份文件里没有 metrics、tracing 或仪表盘服务。要可观测性的话,得自己在这套栈旁边另行部署。

profile 按每次调用启用,也可以写进 .env

make up COMPOSE_PROFILES=admin

发布出来的端口

下面每个地址都是端口在宿主机上发布的位置;容器一侧的端口是固定的,这些变量改的不是它。

服务

默认发布地址

覆盖用的变量

backend

127.0.0.1:8080

BACKEND_HOST, BACKEND_PORT

frontend

0.0.0.0:3001

FRONTEND_HOST, FRONTEND_PORT

postgres

127.0.0.1:5432

DB_PORT

pgadmin(profile admin

127.0.0.1:5050

PGADMIN_PORT

codex-oncall(profile oncall

127.0.0.1:8091

CODEX_ONCALL_PORT

只有 backendfrontend 支持覆盖监听地址。另外三个在 Compose 文件里把 127.0.0.1 写死了,所以它们的 *_PORT 变量能改端口,但改不了绑定地址。

控制台的默认地址不是回环地址——改掉它

frontend 是唯一一个默认 host 为 0.0.0.0 的服务,所以开箱状态下它在这台机器的每个网络接口上都应答。被暴露的不只是控制台:apps/frontend/next.config.js 会把 /v1/*/anthropic/*/auth/*/user/*/admin/*/health 以及若干 /internal/* 路径重写到 BACKEND_INTERNAL_URL。于是任何能连到 3001 端口的人,都能连到那个本想靠 127.0.0.1:8080 这个发布地址保持私有的 API。

如果打算通过 SSH 隧道访问这个实例(见下一节),要把控制台也钉在回环地址上:

FRONTEND_HOST=127.0.0.1

反过来,如果希望这个实例在网络上可达,就把它放到一个终结 TLS 并做鉴权的反向代理后面,并且把上面每一条被重写的路径都当作已公开暴露来对待。

配置:它对外提供哪些模型

仓库根目录下没有 config/models.yaml,也没有 config/routing.yaml。后端按以下顺序解析每个配置文件(apps/backend/serving/config/distribution.py):

  1. 显式指定的环境变量——MODELS_CONFIG_PATHROUTING_CONFIG_PATHALERTS_CONFIG_PATH

  2. DISTRIBUTION_CONFIG_PATH 指定的 distribution manifest 里的 paths: 段,但只有DISTRIBUTION_CONFIG_MODE=active 时才算数——默认的 dark 模式只加载并校验 manifest,把它本来会改动的内容记进日志,实际什么也不改;

  3. 内置的回退值,也就是 config/examples/models.openrouter.yamlconfig/examples/routing.minimal.yaml

所以一份全新 clone 配一个空的 .env,启动时用的就是随仓库发布的示例注册表,一个配置变量都不用设。若某个 staging 实例要镜像一个真实部署,就把路径指向该部署位于 distributions/<name>/config/ 下的 overlay;compose 文件会把 distributions/ 以只读方式挂进容器,所以你写的是容器内路径,例如 /app/distributions/<name>/config/models.yaml

三者中有两个在文件缺失时会静默回退——只有模型注册表缺失时才会吵起来,症状是 /v1/models 为空、每个请求都返回 404。启动时要看一眼 backend 日志。

环境变量

缺少 DB_NAMEDB_USERDB_PASSWORD 时,Compose 会拒绝启动(它们声明为 ${VAR:?...})。网关自身还要两个密钥:

DB_NAME=hybridinference
DB_USER=postgres
DB_PASSWORD=<generated>

# python -c "import secrets; print(secrets.token_urlsafe(48))"
JWT_SECRET_KEY=<generated>
API_KEY_SECRET=<generated>

这两个密钥在启动时都不是强制的:JWT_SECRET_KEYAPI_KEY_SECRET 为空时,只会记一行 critical 日志,网关照常运行下去,只是 token 不安全、API key 的哈希也不安全(apps/backend/serving/servers/app.py)。两个都要生成。

CORS_ALLOWED_ORIGINS 的默认值已经包含八个 origin——http://localhosthttp://127.0.0.1 上的 3000、3001、3002 三个端口,外加 https://localhost:8443https://127.0.0.1:8443Settings.cors_allowed_origins)——所以走隧道访问的实例不需要再加 CORS 条目。只有当控制台由别的 origin 提供服务时才需要加。

.env.example 是完整清单;复制一份,按需填写。

账号,以及怎样才不会把管理员权限白送出去

警告

不要在别人能访问到的实例上同时用 SIGNUP_ENABLED=1SIGNUP_REQUIRE_EMAIL_VERIFICATION=0ADMIN_EMAILS。这三者凑在一起就是一份提权配方:

  • 关闭验证后,POST /auth/signup 创建账号时把 email_verified 设成 require_verification 的取反值——也就是「已验证」——却从未往那个地址发过邮件(apps/backend/serving/servers/routers/auth_routes.py);

  • 登录时以及每一次 token 刷新时,后端都会把地址出现在 ADMIN_EMAILS 里的账号从 free 提升为 admin,并不检查注册的人是否真的拥有那个地址(同一个文件,以及 apps/backend/serving/config/settings.py 里的 is_admin_email)。

于是,猜到或看到你 ADMIN_EMAILS 取值的陌生人,用那个地址注册,第一次登录就已经是管理员。

改用下面两种做法之一。

推荐——在带外创建管理员,并且不设置 ADMIN_EMAILSops/admin/create_admin.py 直接写这一行数据:它以 role='admin'status='active'email_verified=TRUE 创建账号,或者把同地址的已有账号提升上去。等后端至少启动过一次之后(表结构由后端创建),在仓库根目录运行:

python ops/admin/create_admin.py --email you@example.com

要在项目环境里运行(make setup-dev 之后 source .venv/bin/activate),这样才 import 得到 serving 包。它从 .env 读取 DB_HOSTDB_PORTDB_NAMEDB_USERDB_PASSWORD,而 Postgres 发布在 127.0.0.1:5432,所以在宿主机的 shell 里就能跑。不加 --password 它会交互提示,密码也就不会进入 shell 历史。这样安排好之后,实例就可以关掉注册来运行:

USER_AUTH_ENABLED=1
SIGNUP_ENABLED=0

备选——保持注册开放,但保留邮箱验证SIGNUP_REQUIRE_EMAIL_VERIFICATION 默认为 true,开着的时候,账号必须先点开发往该地址的链接才能登录,这就补回了 ADMIN_EMAILS 自己不做的所有权检查。这需要一套可用的 SMTP;没有 SMTP 就没人能完成注册。

ADMIN_EMAILS 同时决定注册审批邮件的默认收件人。要在不改变谁持有管理员角色的前提下缩小通知名单,就设置 SIGNUP_NOTIFY_EMAILS(逗号分隔);它为空时,通知回退到 ADMIN_EMAILSapps/backend/serving/config/settings.py 里的 get_signup_notify_emails)。

SIGNUP_NOTIFY_EMAILS=you@example.com

signup_enabledsignup_require_email_verification 也都能在运行时通过 settings store 翻转,而且运行时的值优先于环境变量。.env 里的一行只是起点,不是保证。

启动

make up          # start
make build       # rebuild images from the checkout, then start

make upmake build 会先创建外部 Docker 卷 hybridinference_postgres_data(如果它还不存在)。

通过 SSH 隧道访问

两个服务都在服务器上钉到回环地址之后,从你的工作机把它们转发过来:

ssh -L 3001:127.0.0.1:3001 -L 8080:127.0.0.1:8080 <user>@<your-server>

然后打开 http://localhost:3001。本地这一端要保持在 localhost127.0.0.1:刷新用的 cookie 默认带 Secure 标志(COOKIE_SECURE),而浏览器只在 HTTPS 或回环 origin 下才接受 Secure cookie。

控制台访问 API 用的地址,取决于镜像构建时烤进去的 NEXT_PUBLIC_API_BASE——默认是 http://localhost:8080——这也是隧道要连 8080 一起转发的原因。改这个值意味着重新构建前端,而不是重启。

VS Code 系列的编辑器可以在自带的端口面板里管理同样的转发。

确认这套栈有应答:

curl -s http://localhost:8080/health
curl -s http://localhost:8080/v1/models

日常操作

make ps                    # container status
make logs s=backend        # tail one service
make restart s=frontend    # restart one service
make build s=backend       # rebuild and restart one service
make down                  # stop everything, keep the data

重置数据库

compose 文件里把 postgres_data 声明为 external: true,这意味着 docker compose down -vmake down(后者根本不传 --volumes)都不会删除它——依赖其中任何一个来做重置,都会悄无声息地把每一行数据原样留下。删掉它需要显式的一步:

make down
docker volume rm hybridinference_postgres_data
make up

这套流程的权威版本在数据库,那里还说明了 backend 之后会重建些什么。

make up 会重新创建这个空卷,后端则在启动时重建表结构。所有账号、API key 和请求日志都没了,所以之后要重新创建管理员账号。