HarnessRouter自托管部署:统一Codex和Claude Code API
用一个自托管API运行Codex、Claude Code和Hermes。本文给出精确Docker部署命令、loopback绑定、必须修改的默认登录信息,以及TLS访问配置。
HarnessRouter 会移除的内容
您可以自行托管 HarnessRouter Community Edition,在自己管理的服务器上为多个代理运行环境提供统一的 API 入口。代理运行环境是一个循环驱动模型的命令行程序:它会保持会话、编辑文件、运行命令,并将进度流式传回请求任务的一方。Codex、Claude Code 和 Hermes 都执行这类工作,但它们各自有独立的安装方式、凭据格式和会话定义。HarnessRouter 将它们全部运行在一个容器中,并在前端提供一个 HTTP 端点、一个登录入口和一个统一的密钥存储。
这就是全部思路,但其代价需要明确说明。您需要在服务器上增加一个容器、一个登录入口、一个卷和一套升级流程,才能将多个组件整合为一个整体。如果您目前只运行一个代理运行环境,那么直接安装该运行环境会比这种方案更简单。这项取舍将在最后一节说明,因此请先阅读该节,再进行部署。
以下内容均已针对镜像标签 0.5.5 进行核对,该镜像于 19 August 2026 拉取。项目大多数日期都会发布新标签,因此不要在一个月后继续直接信任本页面;请检查您实际运行的标签。相关命令来自项目 README:github.com/HarnessRouter/harnessrouter。
Unified Harness Protocol 实际上是什么
HarnessRouter 实现了 Unified Harness Protocol (UHP)。该协议发布于 unifiedharnessprotocol.org。UHP 规定了产品如何在 harness 上启动任务、跟踪运行中的任务、管理会话和文件,以及报告失败。该规范按日期进行版本管理。截至 19 August 2026,当前版本的日期为 2026-08-11。该网站称其为一项草案标准,"稳定到足以用于构建,并通过版本管理确保能够安全变更"。
请仔细理解这里的“开放标准”。同一家公司编写规范、开发参考实现,并维护用于判定兼容性的 52 项检查一致性测试套件。对于这样一个尚处早期的协议,这种情况很常见;而 Apache-2.0 许可证意味着您可以 fork 其中的任何部分。这也意味着 UHP 尚未成为多厂商标准。应将其视为一项发展中的协议:有用、持续变化,并且您的代码应能在不重写的情况下停止使用它。
开始前的准备
需要安装 Docker,并准备大约 4 GB 可用磁盘空间。您还需要一个已付费的模型提供商 API key。镜像拉取量约为 700 MB,其余磁盘空间用于 agent CLI 及其写入的工作区。镜像不包含模型,也不提供试用 key,因此在连接模型提供商之前,任务会失败。HarnessRouter 本身采用 Apache-2.0 许可证。agent CLI 不在该许可证覆盖范围内,因此它们会在首次启动时下载,而不是随镜像一同发布。
使用一次 docker run 自托管 HarnessRouter
docker pull harnessrouter/harnessrouter
docker run -d --name harnessrouter \
-p 127.0.0.1:3000:3000 \
-v harnessrouter:/data \
harnessrouter/harnessrouter然后监控容器启动。首次启动较慢,日志会说明原因。
docker logs -f harnessrouter运行期间,您会看到类似以下内容的日志行:
installing Claude Code (Anthropic's terms apply)…
installing Codex (Apache-2.0)…
installing Hermes (check its upstream license before use)…等待 ready on :3000。每个卷只需执行一次安装,因此之后每次启动只需几秒,并且完全不会输出安装日志。
这个下载过程会带来两个事实,而这两点在 VPS 上都很重要。首先,首次启动需要出站网络访问。该镜像并非完全自包含,因此如果服务器受到出站流量过滤,或没有外部网络路由,启动会卡在这里,并且永远不会输出 ready on :3000。问题发生在首次启动阶段,而不是 docker pull 阶段;等到后者才发现这一点,会很难排查。其次,您正在按照第三方条款安装第三方软件。Claude Code 受 Anthropic 条款约束,Hermes 则受其上游项目所规定的条款约束,因此在用于商业用途前,请先确认两者的条款。
-v harnessrouter:/data 创建一个命名 Docker 卷。所有持久化数据都存储在 /data 中,包括 SQLite 数据库、已存储的文件、密钥存储和代理工作区。删除该卷就等于删除整个实例,其中包括服务提供商密钥和所有对话记录。请在容器停止后进行备份,因为在 SQLite 数据库写入期间复制数据库文件,可能会得到无法打开的文件。对于服务器上的所有有状态容器,也应遵循先停止、再复制的原则;不过具体步骤因服务而异,因为 PhotoPrism 和 Immich 各自需要使用专用的备份命令。
docker stop harnessrouter
docker run --rm -v harnessrouter:/data -v "$PWD":/backup alpine \
tar czf /backup/harnessrouter-data.tgz -C / data
docker start harnessroutercompose 变体,以及必须修改的行
仓库附带一个 compose 文件。它发布 "3000:3000",这意味着主机上的所有网络接口都会监听该端口。在公网上的服务器上启动前,必须先修改这一行。
services:
harnessrouter:
image: harnessrouter/harnessrouter:0.5.5
ports:
- "127.0.0.1:3000:3000"
env_file:
- .env
volumes:
- harnessrouter-data:/data
restart: unless-stopped
volumes:
harnessrouter-data:与上游版本相比,有两处不同:绑定地址,以及使用固定的版本标签,而不是 latest。固定版本很重要,因为 2026 年 8 月 9 日至 18 日期间发布了 16 个版本标签;运行时版本在使用过程中发生变化,会很难排查问题。然后复制环境文件,限制其权限并启动服务。
cp .env.example .env
chmod 600 .env
docker compose up -d
docker compose logs -f.env 会以明文保存您的服务提供商密钥,因此权限模式至少应为 600。如果您不熟悉 docker compose 子命令,请参阅 Docker Compose 命令速查表,其中介绍了日常使用的命令。
为什么端口发布到 127.0.0.1,而不是 0.0.0.0
-p 3000:3000 会将端口发布到主机的所有网络接口。-p 127.0.0.1:3000:3000 只会将端口发布到回环接口,因此只有 VPS 本身可以访问。容器内部始终监听 3000,所以需要修改的是左侧的端口。检查实际结果:
docker port harnessrouter
sudo ss -ltnp | grep 3000ss 输出 127.0.0.1:3000 是正确的。0.0.0.0:3000 表示控制台已暴露在公网中。对于此处的情况,这比大多数自托管应用更危险,因为该控制台会创建执行环境、读取所有对话记录、运行代理,并向这些代理提供 shell 以及其工作区中的真实文件系统。它还保存您连接的服务提供商密钥。任何访问未受保护控制台的人都可以读取您的工作、运行命令,并消耗您的密钥。
主机防火墙无法解决这个问题。Docker 会将自己的规则写入内核的 nat 表,而这些规则会在 ufw 管理的链之前评估。因此,即使 sudo ufw status 将已发布端口列为拒绝,该端口仍然可以访问。请从另一台机器进行测试,不要从 VPS 本身测试,否则测试没有意义。这与在 3080 端口上无头运行 dsh的原则相同:先将服务绑定到回环接口,再明确决定访问方式。
先更改默认登录凭据
使用用户名 harnessrouter 和密码 harnessrouter 登录 http://localhost:3000。README 中列出这些凭据,是因为它们只是占位符,并非机密信息。容器在每次启动时都会提醒您更改它们,直到完成更改:
using the DEFAULT password. Set HR_AUTH_PASSWORD, or change it from the profile page, before exposing this instance.您可以在 Profile 页面中更改密码,也可以在启动时设置密码,以便用于脚本化部署。HR_AUTH_USER 和 HR_AUTH_PASSWORD 可覆盖默认值。
docker run -d --name harnessrouter \
-p 127.0.0.1:3000:3000 \
-v harnessrouter:/data \
-e HR_AUTH_USER='you' \
-e HR_AUTH_PASSWORD='the-password-you-chose' \
harnessrouter/harnessrouter系统不提供重置邮件,因为它没有账户系统,也没有邮件服务器。如果您忘记密码,请删除卷中的 auth 文件并重启,然后再次使用默认凭据登录。
docker stop harnessrouter
docker run --rm -v harnessrouter:/data alpine rm -f /data/selfhost-auth.json
docker start harnessrouterHR_AUTH_DISABLED=1 会完全移除登录限制。README 将此选项限定为“其他人无法访问的主机”。具有公网 IP 地址的 VPS 不属于这种主机,因此除非您在笔记本电脑上运行此服务,否则请保持登录限制启用。
检查版本,因为旧版本没有身份验证门槛
这一点必须认真对待。0.1.x 和 0.2.0 版本完全没有身份验证门槛:任何能够访问 3000 端口的人都已经进入控制台。0.3.0 是第一个包含登录功能的版本。这些旧标签仍在发布,也仍可拉取。因此,旧的固定标签或从同事处复制的 compose 文件,今天仍可能将一个没有身份验证的控制台暴露在公网端口上。
截至 19 August 2026,最新发布的标签是 0.5.5,发布日期为 18 August 2026,latest 指向该标签。先检查当前使用的版本,然后与 Docker Hub 上的标签列表进行比较:
docker image ls harnessrouter/harnessrouter低于 0.3.0 的任何版本都应立即替换,不要延后安排。等于或高于该版本的版本仍需修改密码,因为对于扫描 3000 端口的人来说,默认密码与没有密码没有区别。不要将本页上的版本号视为当前版本。它们只在页面顶部所示日期有效,而该项目的发布速度很快。
连接提供商
在连接模型提供商之前,不会运行任何内容。您可以在控制台的 Integrations 页面中添加提供商,也可以通过环境变量将其传递给 docker run。该值采用 JSON 格式,因此请在 shell 中为其加引号:
-e HR_SECRET_GLOBAL_HARNESS_CONN_ANTHROPIC='{"name":"anthropic","provider":"anthropic","api_key":"sk-ant-…"}'.env.example 为每类提供商指定一个连接变量:HR_SECRET_GLOBAL_HARNESS_CONN_ANTHROPIC 用于 claude-code 后端,HR_SECRET_GLOBAL_HARNESS_CONN_OPENAI 用于 codex 后端,HR_SECRET_GLOBAL_HARNESS_CONN_CUSTOM 用于任何兼容 OpenAI 的端点。聚合服务或您自己的推理服务器应配置在这里。对应的 HR_SECRET_GLOBAL_HARNESS_POLICY_CLAUDE、HR_SECRET_GLOBAL_HARNESS_POLICY_CODEX 和 HR_SECRET_GLOBAL_HARNESS_POLICY_HERMES 变量用于指定各后端默认使用的连接。HR_SECRET_KEY 是另一项独立配置,只有在将数据库连接到代理时才需要。
HR_BACKENDS 用于选择要加载的后端,如 HR_BACKENDS=claude,codex,hermes 所示。有一个已知问题需要提前注意:任何缺少 hermes 的值都会使容器立即以状态 1 退出,且不显示错误消息。启动一秒后,您会在 docker ps -a 中看到 Exited (1),而 docker logs 不会显示任何有用信息。在上游修复此问题之前,请将 hermes 保留在列表中。如果您只需要 Hermes 这一个 harness,在自己的 VPS 上单独运行 Hermes 代理是更简单的部署方式。
通过 API 调用,不使用控制台
控制台不是必需的。两者使用同一个 API,并遵循 Responses 风格的契约。请先登录以获取会话 Cookie:
curl -c hr.cookies http://localhost:3000/api/selfhost/login \
-H 'content-type: application/json' \
-d '{"username":"harnessrouter","password":"your-password"}'然后发送任务,在 metadata.harness_id 中指定 harness,并指定已连接的提供商实际提供的模型:
curl -s -b hr.cookies http://localhost:3000/api/harness/v1/responses \
-H 'content-type: application/json' \
-d '{"input":"Reply with exactly this and nothing else: it works.",
"metadata":{"harness_id":"codex"},
"model":"gpt-5.4-mini",
"stream":false}'如果返回的 JSON 对象包含输出块和令牌数量,说明 harness 已运行。将 harness_id 从 codex 改为 claude,即可将同一个请求发送到另一个 harness;这种切换正是此软件存在的全部原因。上文的自定义连接用于将 harness 指向您已托管的 OpenAI 兼容端点,方式与 在 VPS 上运行的自托管 DeepSeek harness 的连接方式相同。
在不公开端口的情况下从笔记本访问
有两种方式,而且都不会在 0.0.0.0 上直接开放端口。
SSH 隧道成本最低,也无需在服务器上安装任何软件。它会将本机的一个端口转发到 VPS 的 loopback 地址。
ssh -N -L 3000:127.0.0.1:3000 you@your-vps保持该命令运行,然后在浏览器中打开 http://localhost:3000。如果 SSH 输出 bind: Address already in use,说明笔记本上已有程序占用端口 3000。此时使用 -L 3100:127.0.0.1:3000 选择其他本地端口,并访问 3100 端口。
终止 TLS 的反向代理适用于其他人也需要访问的场景。代理负责保存 TLS(传输层安全)证书,并将请求转发到 loopback。README 中给出了 Caddy 配置:
console.example.com {
encode zstd gzip
reverse_proxy 127.0.0.1:3000 {
flush_interval -1 # agent turns stream for minutes; never buffer them
}
}flush_interval -1 是最容易遗漏的配置项。Agent 会持续数分钟输出流式令牌。如果代理缓冲响应,就会一直保留这些令牌,直到本轮结束。因此控制台看起来像是卡住,随后一次性输出全部内容。Nginx 的等效配置是在 location 块中添加 proxy_buffering off;。无论选择哪种代理,都应让 DNS 名称指向代理,并让容器仅监听 loopback。比较 Nginx、Caddy 和 Traefik 作为反向代理介绍了如何根据服务器环境选择合适的代理。
让它使用独立用户运行,不要使用 root
Docker daemon 以 root 身份运行,而加入 docker 组等同于获得 root 权限,因为组成员可以启动挂载主机文件系统的容器。因此,“将团队加入 docker 组”实际上会向持有 provider key 的服务器授予 root 权限。
简单方案是:创建一个拥有 compose 文件和 .env 的服务账户,并将这些文件放在共享主目录之外。
sudo adduser --disabled-password --gecos "" harness
sudo install -d -o harness -g harness -m 750 /srv/harnessrouter更严格的方案是使用 rootless Docker,让 daemon 本身以该非特权用户身份运行。它需要 uidmap 软件包提供 newuidmap 和 newgidmap,还需要在 /etc/subuid 和 /etc/subgid 中为该用户分配至少 65536 个 subordinate UID。Ubuntu 软件仓库中提供 uidmap,但不提供 docker-ce-rootless-extras:该软件包来自 Docker 自有的 apt 软件仓库 download.docker.com,安装 Docker engine 时会添加该仓库。如果未从该仓库安装 engine,grep -rl download.docker.com /etc/apt/sources.list.d/ 不会输出任何内容,下面的安装步骤也找不到该软件包。
sudo apt install -y uidmap docker-ce-rootless-extras
sudo loginctl enable-linger harness
sudo -iu harness
dockerd-rootless-setuptool.sh install
export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock
systemctl --user enable --now docker这里不能省略 loginctl enable-linger。如果没有它,用户的 systemd 实例会在最后一个会话关闭时停止,因此退出登录后容器也会停止。使用 docker info 确认结果;该命令会在 Security Options 下列出 rootless。rootless 模式无法在未进行额外配置的情况下绑定 1024 以下的端口,但这里不受影响,因为端口 3000 高于该范围。账户本身的配置方法见在 VPS 上创建最小权限用户。
会出现哪些故障,以及你将看到什么
容器启动后一秒就退出,日志为空。 docker ps -a显示 Exited (1)。这是上面提到的 HR_BACKENDS 问题:您的值遗漏了 hermes。将其补回。
首次启动始终无法完成。 日志在一行 installing 后停止,并且从未出现 ready on :3000。由于镜像中未包含 agent CLI,服务器无法访问网络来下载它们。修复出站路由或代理设置,然后重新启动。
控制台可以加载,但每个任务都失败。 没有连接任何 provider。镜像中没有内置模型,也没有免费套餐,因此新实例即使可以完成登录,也无法运行任何任务。
通过代理访问时,控制台在回答中途卡住。 输出会在本轮结束时一次性显示。这是响应缓冲导致的。请在 Caddy 中设置 flush_interval -1,或在 Nginx 中设置 proxy_buffering off;。
隧道已建立,但您的笔记本电脑无法访问服务。 在服务器上运行 docker port harnessrouter。如果没有输出,说明容器没有发布任何端口,因此启动时未使用 -p。
这值得部署吗?
如果您确实使用多个 harness,并且希望使用一个端点和一个凭据存储,而不是分别使用三个端点和三个凭据存储,那么部署它是值得的。如果您正在其上构建产品,并且希望将 harness 设为配置项,而不是在更换 harness 时重写代码,部署它也值得。UHP 正是为此提供支持,但请注意前文提到的协议仍处于早期阶段。
如果您只使用一个 harness,那么部署它没有必要。直接在服务器上安装该 CLI,需要维护的组件更少,也不会有登录层挡在您与它之间。如果您的需求是让多个代理协作完成一个任务,而不是在多个 harness 前面提供一个 API,那么它也不适合;这是另一类工具。对于这种模式,请参阅多代理 harness,例如 Omnigent。无论采用哪种方式,部署规则都不变:绑定到 loopback,修改密码,使用 0.3.0 或更高版本的固定 tag,并使用专用用户。
FAQ
发布 HarnessRouter 的 3000 端口安全吗?
不安全。控制台可以创建 harness、读取所有转录内容、运行具有 shell 和文件系统访问权限的代理,并持有您连接的提供商密钥,因此开放端口会暴露这些全部功能。使用 -p 127.0.0.1:3000:3000 让服务仅监听 loopback,再通过 SSH 隧道或终止 TLS 的反向代理访问。仅依靠主机防火墙并不够:Docker 会将自己的规则写入内核的 nat 表,因此即使 ufw 显示该端口被拒绝,已发布的端口仍会响应来自互联网的请求。使用 sudo ss -ltnp | grep 3000 验证,输出应为 127.0.0.1:3000。
哪个 HarnessRouter 版本加入了登录限制?
0.3.0。0.1.x 和 0.2.0 版本发布时完全没有身份验证,而且这两个标签仍然已发布并可拉取,因此运行这些版本就等于依赖没人发现该端口。截至 19 August 2026,最新标签是 0.5.5,日期为 18 August 2026。运行 docker image ls harnessrouter/harnessrouter 查看当前使用的版本,并将其与 Docker Hub 上的标签列表比较,不要与本页面比较;即使使用当前版本,也应修改默认密码。
为什么设置 HR_BACKENDS 后容器会立即退出?
任何缺少 hermes 的 HR_BACKENDS 值都会导致容器立即以状态码 1 退出,且不显示错误消息;这是项目 README 中记录的已知问题。其表现是,一两秒内在 docker ps -a 中出现 Exited (1),而 docker logs 中没有任何有用信息。在上游修复此问题前,请保留列表中的 hermes,如 HR_BACKENDS=claude,codex,hermes 所示。
HarnessRouter 首次启动时需要互联网访问吗?
需要。代理 CLI 会在首次启动时下载,而不是随镜像一起发布,因为每个 CLI 都带有自己的许可证。没有出站路由的主机会输出 installing 行,随后无法到达 ready on :3000。每个卷只需下载一次,因此后续启动只需几秒,除连接的模型提供商外不需要网络。
我忘记了控制台密码。如何重新登录?
没有重置密码的邮件,因为该系统没有账户系统,也没有邮件服务器。停止容器,从卷中删除 /data/selfhost-auth.json,然后重新启动容器;接着使用默认凭据登录,并在 Profile 页面设置新密码。如果容器和卷都命名为 harnessrouter,则依次执行 docker stop harnessrouter、docker run --rm -v harnessrouter:/data alpine rm -f /data/selfhost-auth.json 和 docker start harnessrouter。