运行非生产实例
一个 staging(或者预览、临时)实例与生产环境是同一套栈,由同一份 deploy/docker/docker-compose.yml 启动,只是跑在一台你不介意弄坏的机器上。本页只讲它与生产部署不同的那些部分:compose 文件实际会启动什么、怎么让它不暴露在网络上、怎么在不开出提权口子的前提下拿到一个管理员账号,以及怎么把数据库整个丢掉。
生产部署的操作流程见部署指南;想零依赖地过一遍路由引擎,见快速开始。
compose 文件会启动什么
deploy/docker/docker-compose.yml 定义了五个服务:
服务 |
默认启动 |
说明 |
|---|---|---|
|
是 |
FastAPI 网关 |
|
是 |
Next.js 控制台 |
|
是 |
|
|
否 |
Compose 的 |
|
否 |
Compose 的 |
这份文件里没有 metrics、tracing 或仪表盘服务。要可观测性的话,得自己在这套栈旁边另行部署。
profile 按每次调用启用,也可以写进 .env:
make up COMPOSE_PROFILES=admin
发布出来的端口
下面每个地址都是端口在宿主机上发布的位置;容器一侧的端口是固定的,这些变量改的不是它。
服务 |
默认发布地址 |
覆盖用的变量 |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
只有 backend 和 frontend 支持覆盖监听地址。另外三个在 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):
显式指定的环境变量——
MODELS_CONFIG_PATH、ROUTING_CONFIG_PATH、ALERTS_CONFIG_PATH;由
DISTRIBUTION_CONFIG_PATH指定的 distribution manifest 里的paths:段,但只有在DISTRIBUTION_CONFIG_MODE=active时才算数——默认的dark模式只加载并校验 manifest,把它本来会改动的内容记进日志,实际什么也不改;内置的回退值,也就是
config/examples/models.openrouter.yaml和config/examples/routing.minimal.yaml。
所以一份全新 clone 配一个空的 .env,启动时用的就是随仓库发布的示例注册表,一个配置变量都不用设。若某个 staging 实例要镜像一个真实部署,就把路径指向该部署位于 distributions/<name>/config/ 下的 overlay;compose 文件会把 distributions/ 以只读方式挂进容器,所以你写的是容器内路径,例如 /app/distributions/<name>/config/models.yaml。
三者中有两个在文件缺失时会静默回退——只有模型注册表缺失时才会吵起来,症状是 /v1/models 为空、每个请求都返回 404。启动时要看一眼 backend 日志。
环境变量
缺少 DB_NAME、DB_USER 或 DB_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_KEY 或 API_KEY_SECRET 为空时,只会记一行 critical 日志,网关照常运行下去,只是 token 不安全、API key 的哈希也不安全(apps/backend/serving/servers/app.py)。两个都要生成。
CORS_ALLOWED_ORIGINS 的默认值已经包含八个 origin——http://localhost 和 http://127.0.0.1 上的 3000、3001、3002 三个端口,外加 https://localhost:8443 和 https://127.0.0.1:8443(Settings.cors_allowed_origins)——所以走隧道访问的实例不需要再加 CORS 条目。只有当控制台由别的 origin 提供服务时才需要加。
.env.example 是完整清单;复制一份,按需填写。
账号,以及怎样才不会把管理员权限白送出去
警告
不要在别人能访问到的实例上同时用 SIGNUP_ENABLED=1、SIGNUP_REQUIRE_EMAIL_VERIFICATION=0 和 ADMIN_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_EMAILS。ops/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_HOST、DB_PORT、DB_NAME、DB_USER 和 DB_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_EMAILS(apps/backend/serving/config/settings.py 里的 get_signup_notify_emails)。
SIGNUP_NOTIFY_EMAILS=you@example.com
signup_enabled 和 signup_require_email_verification 也都能在运行时通过 settings store 翻转,而且运行时的值优先于环境变量。.env 里的一行只是起点,不是保证。
启动
make up # start
make build # rebuild images from the checkout, then start
make up 和 make 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。本地这一端要保持在 localhost 或 127.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 -v 和 make down(后者根本不传 --volumes)都不会删除它——依赖其中任何一个来做重置,都会悄无声息地把每一行数据原样留下。删掉它需要显式的一步:
make down
docker volume rm hybridinference_postgres_data
make up
这套流程的权威版本在数据库,那里还说明了 backend 之后会重建些什么。
make up 会重新创建这个空卷,后端则在启动时重建表结构。所有账号、API key 和请求日志都没了,所以之后要重新创建管理员账号。