部署指南
把 HybridInference 作为长期运行的部署来跑:会启动些什么、怎么运维,以及怎样在不丢数据(也不会意外留下数据)的前提下重置它。
首次搭建——克隆仓库、填写 .env、第一次 make up——见安装。本页假定整套服务栈已经能起来。
这套服务栈是什么
make up 会从 deploy/docker/docker-compose.yml 启动三个容器:
服务 |
镜像 / 构建 |
发布地址 |
|---|---|---|
|
由 |
|
|
由 |
|
|
|
|
frontend 默认监听 0.0.0.0,好让宿主机上的反向代理能访问到它;backend 和数据库默认只监听 loopback。三者都加入同一个 Compose 文件里定义的 bridge 网络,backend 在这个网络上以 postgres:5432 访问数据库。.env 里真正传到这个文件的只有 DB_PORT,而且只作为映射的宿主机那一半(127.0.0.1:${DB_PORT:-5432}:5432);宿主机侧的绑定地址是写死在 loopback 上的。DB_HOST 在 Compose 文件里被钉成 postgres,在 Compose 下会被忽略——它只对直接从源码启动的 backend 有意义。
同一个文件里还有两个服务,只有点名它们的 profile 时才会启动:pgadmin(profile admin)和 codex-oncall(profile oncall)。
frontend 和 codex-oncall(后者就是上面那两个被 profile 挡住的服务之一,所以平时并不运行)对 backend 的依赖用的是 condition: service_started 而不是 service_healthy——这是有意为之:backend 因为数据库日志写不进去而报告 unhealthy 时,不应该连带拦住控制台启动。
把它放到公网上
这套服务栈里没有任何一环负责终结 TLS,本仓库也不提供可以照抄的反向代理配置:证书,以及挡在这两个发布端口前面的代理,都要你自己准备。把它指向 ${BACKEND_HOST}:${BACKEND_PORT} 和 ${FRONTEND_HOST}:${FRONTEND_PORT}。
控制台自己响应哪些公网路径、又把哪些转发给后端,是另一个问题,答案在控制台自己的 next.config.js 里,而不在任何代理配置里。见边缘与控制台路由。
日常运维
以下命令都在仓库根目录执行:
make up # start everything
make down # stop everything (data survives; see below)
make restart # restart everything
make restart s=backend # restart one service
make ps # services and health status
make logs # tail all logs
make logs s=backend # tail one service
make build # rebuild images and restart
make build s=frontend # rebuild one service
make up 和 make build 会先执行 docker-volumes 目标,在外部卷 hybridinference_postgres_data 不存在时创建它。
要启动某个可选 profile,就在 make 命令行上传入——在那里设置的变量会导出到 recipe 的环境中,而在 Compose 里 shell 变量的优先级高于任何 --env-file:
make up COMPOSE_PROFILES=admin
COMPOSE_PROFILES 是逗号分隔的列表,所以 admin,oncall 会把两个都启动。它也可以写在 .env 里(.env.example 有说明),但想确定哪些 profile 真的生效时,优先用命令行这种写法。
一次改动到底需要做什么
答案有三种,选错的表现就是改动看起来没生效:
你改了什么 |
该怎么做 |
|---|---|
|
|
模型注册表或路由 YAML |
|
控制台的某个 |
|
后端或前端的源码 |
|
控制台的身份标识和它的 /agents rewrites 是 Next.js 的 build args(deploy/docker/docker-compose.yml 的 frontend.build.args),而 Next 会在构建时把 rewrites() 解析进 .next/routes-manifest.json。因此改动任何 NEXT_PUBLIC_* 取值或 AGENT_* URL 都需要一次 make build s=frontend;只在容器启动时提供的值没有任何东西会去读,症状就是旧页面照常提供服务,而 docker inspect 显示的却是新值。所以启用或回滚这些设置属于重新构建,不是重启。
配置
环境变量
所有配置都在仓库根目录的 .env 里;.env.example 是带注释的清单。Compose 以 --env-file .env 调用,backend 服务同时把它作为 env_file 加载。Compose 自身要求的变量,以及不该留空的那两个密钥,见安装。
配置文件的解析顺序
本仓库里没有 config/models.yaml,也没有 config/routing.yaml。resolve_config_path(apps/backend/serving/config/distribution.py)按优先级挑选每个文件:先看显式的 MODELS_CONFIG_PATH / ROUTING_CONFIG_PATH / ALERTS_CONFIG_PATH,再看处于 active 的 distribution manifest,最后是 config/examples/ 下的内置默认值。完整规则(包括 DISTRIBUTION_CONFIG_MODE 为什么默认是 dark)见安装。
注意,Compose 文件是显式把它们透传进去的:
ROUTING_CONFIG_PATH: ${ROUTING_CONFIG_PATH-}
MODELS_CONFIG_PATH: ${MODELS_CONFIG_PATH-}
DISTRIBUTION_CONFIG_PATH: ${DISTRIBUTION_CONFIG_PATH-}
光有 --env-file 并不能把变量放进容器的环境里;真正把它带进去的是这几行。少了一行,就会悄无声息地把一个部署的路由映射或告警阈值换成默认值——这正是测试要把它们钉住的原因。
本地推理服务器
backend 容器通过 host.docker.internal 访问宿主机上的服务器,Compose 文件用 extra_hosts: host.docker.internal:host-gateway 把它接好。要在模型注册表里显式写上这个地址:
route:
- kind: openai_compat
base_url: http://host.docker.internal:8001/v1
如果 backend 直接跑在宿主机上,就改用 localhost。网关从不改写 provider 的 URL。见添加新的本地模型。
健康检查
curl -s http://localhost:8080/health
{
"status": "healthy",
"routes_configured": 3,
"database_configured": true,
"database_connected": true,
"stores": {
"operational_store": {"status": "ok", "backend": "postgres", "cache": "in_memory"},
"log_store": {"status": "ok", "backend": "postgres"}
}
}
routes_configured统计的是已发布的路由条目——生效的注册表里每个模型 id 一条,每个别名再加一条。它完全取决于你自己的注册表定义了什么。database_configured区分的是「这个部署本来就不要数据库」和「数据库挂了」:DB_ENABLED=false时它是false,状态仍然是healthy;而配置了数据库、启动时却连不上时,/health返回 503,并带上"reason": "database_unavailable_at_startup"。当已配置的两个存储中一个挂了、另一个还在服务时,
status变成degraded——HTTP 状态码仍是 200。这个形态是有意设计的:容器的HEALTHCHECK用的是curl -f /health,若对部分降级返回 503,就会把仍在正常响应请求的 backend 拆掉。
要做严格的就绪探针,请用 /health/ready:它对已配置的各个存储取与逻辑,只要有一个没起来就返回 503。/health/deep 还会额外报告每个端点的健康状况。
告警
后端内置了一个进程内告警引擎,会向 Slack webhook 推送。不显式打开就是关闭的:
ALERTS_ENABLED=true
SLACK_ALERTS_WEBHOOK_URL=https://hooks.slack.com/services/...
SLACK_ALERTS_WEBHOOK_URL 为空时会回退到 SLACK_WEBHOOK_URL,所以一个 webhook 可以同时服务两条代码路径。
规则和阈值属于各个部署自己,本仓库不提供告警文件。把 ALERTS_CONFIG_PATH 指向你自己的文件;或者不设置它,那就使用内置阈值。规则类型和求值逻辑在 apps/backend/serving/observability/。
要让 Codex 以异步、只读的方式排查告警,见 Codex On-Call。
数据库
PostgreSQL 16 跑在 postgres 服务里,数据放在 Docker 卷 hybridinference_postgres_data 中。开一个 psql shell:
docker exec -it hybridinference-postgres psql -U "${DB_USER}" -d "${DB_NAME}"
表结构细节见数据库。
pgAdmin(可选)
make up COMPOSE_PROFILES=admin
pgAdmin 随后监听 127.0.0.1:5050,并带 SCRIPT_NAME=/pgadmin,所以向该端口开一条 SSH 隧道就足以访问它。控制台也可以在 /pgadmin/ 下代理它,由 apps/frontend/src/app/pgadmin/[[...path]]/route.ts 按 admin 会话把关;该文件在任何非预期情况下都拒绝,包括连不上后端的时候。
有一点要弄对:pgAdmin 是否另外要求它自己的登录,由 PGADMIN_CONFIG_SERVER_MODE 决定,而两处的默认值并不一致。Compose 服务回退到 False,也就是完全不需要登录就能用 pgAdmin;.env.example 建议 True,也就是在控制台这道关卡之后再开启 pgAdmin 自己的登录。两者之中 True 更安全。
故障排查
某个服务起不来
make logs s=backend
make ps
required variable DB_NAME is missing a value: DB_NAME must be set in .env file——Compose 在变量插值阶段就停下了,什么都还没启动。DB_NAME、DB_USER和DB_PASSWORD是用${VAR:?message}形式声明为必填的,所以冒号之后那半句是 Compose 文件自己写的文案,也是这一行里最好 grep 的部分。端口已被占用——覆盖
BACKEND_PORT、FRONTEND_PORT或DB_PORT。数据库连接失败——用
make ps查看postgres的健康状态。
重置这套服务栈
停止再启动,保留数据:
make down && make up
销毁数据库、从干净状态开始。docker compose down -v 做不到这件事。postgres_data 在 deploy/docker/docker-compose.yml 里声明为 external: true,而 Compose 从不删除外部卷——down -v 会返回成功,却把卷原封不动地留着,于是 make up 又回到完全相同的数据上。要按名字删除它:
make down
docker volume rm hybridinference_postgres_data
make up # docker-volumes recreates it empty; Postgres re-initialises
警告
docker volume rm 不可逆,会连同每一个账户、每一把 API key 和所有请求日志一起带走。只要其中还有任何东西重要,先做一次 pg_dump。
pgAdmin 自己的卷(hybridinference_pgadmin_data)和 on-call relay 的卷(hybridinference_codex_oncall_data)是普通的本地卷,所以 down -v 确实会把它们删掉。但要注意 make down 是不带 --volumes 的纯 docker compose down,这里没有任何命令会替你加上 -v——你得自己跑 docker compose --profile admin --profile oncall down -v,或者像上面那样按名字删卷。
改动代码后重新构建
make build # all images
make build s=backend # one service