SSD Nodes Learn Hosting plans →
Hướng dẫn Matt ConnorBởi Matt Connor · Cập nhật ngày 2026-09-08

Tự host HarnessRouter: một API cho nhiều agent

Chạy Codex, Claude Code và Hermes qua một API tự host. Xem lệnh Docker, bind loopback, tài khoản mặc định cần đổi và cách truy cập qua TLS.

HarnessRouter loại bỏ những gì

Bạn tự host HarnessRouter Community Edition để đặt một API phía trước nhiều agent harness trên máy chủ do bạn sở hữu. Agent harness là chương trình dòng lệnh điều khiển một model trong một vòng lặp: chương trình duy trì một session, chỉnh sửa file, chạy command và stream tiến độ về nơi đã yêu cầu công việc. Codex, Claude Code và Hermes đều thực hiện công việc này. Mỗi công cụ có quy trình cài đặt riêng, định dạng credential riêng và cách hiểu riêng về session. HarnessRouter chạy tất cả trong một container, đồng thời cung cấp một HTTP endpoint, một lần đăng nhập và một secret store duy nhất ở phía trước.

Đó là toàn bộ ý tưởng. Bạn cần cân nhắc rõ chi phí đi kèm. Bạn thêm một container, một lần đăng nhập, một volume và một quy trình upgrade vào server để gộp nhiều thành phần thành một. Nếu hiện tại bạn chỉ chạy đúng một harness, cách này kém hơn so với cài trực tiếp harness đó. Phần cuối sẽ phân tích đánh đổi này, vì vậy hãy đọc trước khi deploy.

Mọi nội dung dưới đây đã được kiểm tra với image tag 0.5.5, được pull vào ngày 19 August 2026. Project phát hành tag mới hầu hết các ngày, vì vậy hãy kiểm tra tag thực tế bạn đang chạy thay vì tin tuyệt đối vào trang này sau một tháng. Các command được lấy từ README của project tại github.com/HarnessRouter/harnessrouter.

Unified Harness Protocol thực sự là gì

HarnessRouter triển khai Unified Harness Protocol (UHP), được công bố tại unifiedharnessprotocol.org. UHP mô tả cách một sản phẩm khởi chạy task trên một harness, theo dõi task trong khi task chạy, quản lý session và file, rồi báo cáo lỗi. Đặc tả được lập phiên bản theo ngày. Phiên bản đang được sử dụng vào ngày 19 August 2026 có ngày 2026-08-11. Trang web gọi đây là một draft standard, “đủ ổn định để xây dựng trên đó và được lập phiên bản để có thể thay đổi an toàn”.

Hãy đọc kỹ cụm từ “open standard” trong trường hợp này. Cùng một công ty viết đặc tả, viết reference implementation và xây dựng conformance suite gồm 52 kiểm tra để quyết định thành phần nào đạt chuẩn. Điều này bình thường đối với một protocol còn mới như vậy. Giấy phép Apache-2.0 cũng cho phép bạn fork bất kỳ phần nào của nó. Tuy nhiên, điều đó có nghĩa UHP chưa phải là một multi-vendor standard. Hãy xem đây là một protocol đang hình thành: hữu ích, còn thay đổi, và code của bạn nên có khả năng ngừng sử dụng nó mà không cần viết lại.

Những thứ cần chuẩn bị trước khi bắt đầu

Docker và khoảng 4 GB dung lượng đĩa trống. Bạn cũng cần API key của một model provider mà bạn đã trả phí. Việc pull image cần khoảng 700 MB, phần dung lượng còn lại dành cho các agent CLI và workspace mà chúng ghi dữ liệu vào. Image không kèm model hoặc trial key, nên task sẽ fail cho đến khi bạn kết nối một provider. Bản thân HarnessRouter được cấp phép theo Apache-2.0. Các agent CLI không thuộc license đó, nên chúng được fetch ở lần khởi động đầu tiên thay vì được đóng gói trong image.

Tự host HarnessRouter bằng một lệnh docker run

docker pull harnessrouter/harnessrouter
docker run -d --name harnessrouter \
  -p 127.0.0.1:3000:3000 \
  -v harnessrouter:/data \
  harnessrouter/harnessrouter

Sau đó theo dõi container khởi động. Lần start đầu tiên sẽ chậm, và log cho biết nguyên nhân.

docker logs -f harnessrouter

Khi hoạt động, bạn sẽ thấy các dòng như sau:

installing Claude Code (Anthropic's terms apply)…
installing Codex (Apache-2.0)…
installing Hermes (check its upstream license before use)…

Chờ đến khi xuất hiện ready on :3000. Quá trình cài đặt này chỉ chạy một lần cho mỗi volume, nên các lần start sau chỉ mất vài giây và hoàn toàn không in các dòng cài đặt.

Có 2 điều cần rút ra từ lần download này, và cả 2 đều quan trọng trên VPS. Thứ nhất, lần boot đầu tiên cần có quyền truy cập mạng outbound. Image không tự chứa mọi thành phần cần thiết, nên máy nằm sau egress filter hoặc không có route ra ngoài sẽ bị treo tại đây và không bao giờ in ready on :3000. Lỗi xảy ra ở lần start đầu tiên, không phải tại docker pull, nên bạn rất dễ phát hiện vấn đề quá muộn. Thứ hai, bạn đang cài phần mềm của bên thứ ba theo điều khoản của bên thứ ba. Claude Code được cung cấp theo điều khoản của Anthropic, còn Hermes theo điều khoản do upstream của nó quy định, vì vậy hãy kiểm tra cả 2 trước khi sử dụng cho mục đích thương mại.

-v harnessrouter:/data tạo một Docker volume có tên. Mọi dữ liệu cần lưu lâu dài đều nằm trong /data: các cơ sở dữ liệu SQLite, file đã lưu, secret store và workspace của agent. Xóa volume đó đồng nghĩa với xóa instance, bao gồm các provider key và toàn bộ transcript. Hãy backup khi container đã stop, vì copy cơ sở dữ liệu SQLite trong lúc database đang được ghi có thể tạo ra file không mở được. Nguyên tắc stop rồi mới copy này cũng áp dụng cho mọi container có state trên máy, dù chi tiết sẽ khác tùy service, vì PhotoPrism và Immich đều cần các lệnh backup riêng.

docker stop harnessrouter
docker run --rm -v harnessrouter:/data -v "$PWD":/backup alpine \
  tar czf /backup/harnessrouter-data.tgz -C / data
docker start harnessrouter

Biến thể Compose và dòng bạn phải thay đổi

Repository có sẵn một compose file. File này publish "3000:3000", nghĩa là service lắng nghe trên mọi interface của host. Hãy thay đổi dòng đó trước khi khởi động trên public server.

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:

Có 2 điểm khác với upstream: bind address và version tag được pin thay cho latest. Việc pin version rất quan trọng vì đã có 16 version tag được phát hành trong khoảng từ ngày 9 đến ngày 18 tháng 8 năm 2026. Agent runtime tự thay đổi sẽ rất khó debug. Sau đó, copy file môi trường, giới hạn permission của file và khởi động.

cp .env.example .env
chmod 600 .env
docker compose up -d
docker compose logs -f

.env chứa provider key ở dạng plain text, nên mode 600 là mức tối thiểu. Nếu bạn chưa quen với subcommand docker compose, cheat sheet các lệnh Docker Compose sẽ bao quát những lệnh thường dùng hằng ngày.

Vì sao port được publish trên 127.0.0.1 thay vì 0.0.0.0

-p 3000:3000 publish port trên mọi interface mà host có. -p 127.0.0.1:3000:3000 chỉ publish port trên loopback, nghĩa là chỉ có thể truy cập từ chính VPS. Container luôn lắng nghe trên port 3000 ở bên trong, nên bạn chỉ cần thay phần bên trái. Kiểm tra kết quả:

docker port harnessrouter
sudo ss -ltnp | grep 3000

ss hiển thị 127.0.0.1:3000 là đúng. 0.0.0.0:3000 có nghĩa là console đang nằm trên Internet công cộng. Trong trường hợp này, điều đó nguy hiểm hơn hầu hết các app tự host khác, vì console tạo harness, đọc mọi transcript, chạy agent và cấp cho các agent đó một shell cùng filesystem thật trong workspace của chúng. Console cũng lưu provider key mà bạn đã kết nối. Bất kỳ ai truy cập được console khi chưa có bảo vệ đều có thể đọc công việc của bạn, chạy command và tiêu tốn key của bạn.

Host firewall không thể bảo vệ bạn khỏi việc này. Docker publish port bằng cách tự ghi rule vào bảng nat của kernel. Các rule này được xử lý trước chain mà ufw quản lý, nên port đã publish vẫn có thể truy cập ngay cả khi sudo ufw status liệt kê port đó là bị deny. Hãy kiểm tra từ một máy khác, không phải từ VPS; nếu không, bạn sẽ không kiểm tra được gì. Đây cũng là bài học giống như chạy dsh headless trên port 3080: bind service vào loopback, rồi chủ động quyết định cách bạn truy cập service đó.

Thay thông tin đăng nhập mặc định trước tiên

Đăng nhập tại http://localhost:3000 bằng username harnessrouter và password harnessrouter. Các thông tin này được ghi trong README vì chúng chỉ là giá trị giữ chỗ, không phải secret. Container sẽ cảnh báo mỗi lần khởi động cho đến khi bạn thay đổi chúng:

using the DEFAULT password. Set HR_AUTH_PASSWORD, or change it from the profile page, before exposing this instance.

Thay đổi trong trang Profile hoặc đặt giá trị khi khởi động để triển khai bằng script. HR_AUTH_USERHR_AUTH_PASSWORD ghi đè các giá trị mặc định.

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

Không có email đặt lại vì ứng dụng không có hệ thống tài khoản và mail server. Nếu bạn quên password, hãy xóa file xác thực trong volume rồi khởi động lại. Sau đó đăng nhập lại bằng thông tin mặc định.

docker stop harnessrouter
docker run --rm -v harnessrouter:/data alpine rm -f /data/selfhost-auth.json
docker start harnessrouter

HR_AUTH_DISABLED=1 tắt hoàn toàn bước yêu cầu đăng nhập. README giới hạn tùy chọn này cho “một máy mà không ai khác có thể truy cập”. VPS có public IP không phải là máy như vậy, nên hãy giữ bước đăng nhập trừ khi bạn chạy ứng dụng trên laptop.

Kiểm tra phiên bản, vì các bản cũ không có lớp bảo vệ

Đây là phần bạn cần đặc biệt lưu ý. Các phiên bản 0.1.x0.2.0 được phát hành mà hoàn toàn không có cơ chế xác thực: bất kỳ ai truy cập được cổng 3000 đều đã vào được console. 0.3.0 là bản phát hành đầu tiên có chức năng đăng nhập. Các tag cũ đó vẫn được publish và vẫn có thể pull, vì vậy một tag cũ được pin hoặc một file compose do đồng nghiệp sao chép có thể khiến console không có lớp bảo vệ xuất hiện trên một cổng public ngay hôm nay.

Tính đến ngày 19 tháng 8 năm 2026, tag mới nhất đã publish là 0.5.5, có ngày phát hành 18 tháng 8 năm 2026, và latest trỏ đến tag đó. Kiểm tra phiên bản bạn đang dùng, sau đó đối chiếu với danh sách tag trên Docker Hub:

docker image ls harnessrouter/harnessrouter

Bất kỳ phiên bản nào thấp hơn 0.3.0 đều phải được thay thế ngay, không được lên lịch để làm sau. Các phiên bản từ 0.3.0 trở lên vẫn cần đổi password, vì password mặc định và không có password là như nhau đối với người đang quét cổng 3000. Không xem các số phiên bản trên trang này là thông tin hiện tại. Chúng chỉ đúng tại ngày được ghi ở đầu trang, và project này phát hành phiên bản rất nhanh.

Kết nối một provider

Không có gì chạy cho đến khi bạn kết nối một model provider. Thêm một provider từ trang Integrations trong console, hoặc truyền nó vào docker run trong environment. Giá trị này là JSON, nên hãy đặt trong dấu nháy khi chạy trong shell:

-e HR_SECRET_GLOBAL_HARNESS_CONN_ANTHROPIC='{"name":"anthropic","provider":"anthropic","api_key":"sk-ant-…"}'

.env.example khai báo một biến connection cho mỗi nhóm provider: HR_SECRET_GLOBAL_HARNESS_CONN_ANTHROPIC cho backend claude-code, HR_SECRET_GLOBAL_HARNESS_CONN_OPENAI cho backend codex và HR_SECRET_GLOBAL_HARNESS_CONN_CUSTOM cho mọi endpoint tương thích với OpenAI. Đây là nơi bạn khai báo một aggregator hoặc inference server của riêng mình. Các biến HR_SECRET_GLOBAL_HARNESS_POLICY_CLAUDE, HR_SECRET_GLOBAL_HARNESS_POLICY_CODEXHR_SECRET_GLOBAL_HARNESS_POLICY_HERMES tương ứng cho biết mỗi backend mặc định sử dụng connection nào. HR_SECRET_KEY là một thành phần riêng và chỉ bắt buộc khi bạn kết nối database với một agent.

HR_BACKENDS chọn các backend được load, như trong HR_BACKENDS=claude,codex,hermes. Có một lỗi đã biết mà bạn cần lưu ý: mọi giá trị không có hermes sẽ khiến container thoát ngay với status 1 mà không hiển thị thông báo lỗi. Bạn sẽ thấy Exited (1) trong docker ps -a một giây sau khi khởi động, còn docker logs không hiển thị thông tin hữu ích nào. Giữ hermes trong danh sách cho đến khi upstream sửa lỗi này. Nếu chỉ muốn dùng Hermes làm harness, chạy agent Hermes trên VPS riêng sẽ là phương án triển khai gọn hơn.

Gọi API mà không cần console

Console là tùy chọn. Cả hai đều dùng cùng một API và API này tuân theo contract kiểu Responses. Trước tiên, hãy đăng nhập để lấy session cookie:

curl -c hr.cookies http://localhost:3000/api/selfhost/login \
  -H 'content-type: application/json' \
  -d '{"username":"harnessrouter","password":"your-password"}'

Sau đó gửi một task, chỉ định harness trong metadata.harness_id và một model mà provider đã kết nối thực sự cung cấp:

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 object chứa một output block và số lượng token cho biết harness đã chạy. Đổi harness_id từ codex sang claude sẽ gửi cùng request đến một harness khác. Việc thay đổi này chính là lý do tồn tại của phần mềm. Custom connection ở trên là cách trỏ một harness đến endpoint tương thích với OpenAI mà bạn đã tự host, giống như một harness DeepSeek tự host trên VPS được cấu hình.

Truy cập từ laptop mà không public một cổng

Có 2 cách, và không cách nào mở trực tiếp một cổng trên 0.0.0.0.

SSH tunnel là cách đơn giản nhất và không cần cài thêm gì trên server. Nó chuyển tiếp một cổng local trên máy của bạn đến loopback trên VPS.

ssh -N -L 3000:127.0.0.1:3000 you@your-vps

Giữ tunnel này chạy rồi mở http://localhost:3000 trong trình duyệt. Nếu SSH in ra bind: Address already in use, trên laptop của bạn đã có tiến trình khác chiếm cổng 3000. Hãy chọn một cổng local khác bằng -L 3100:127.0.0.1:3000 rồi truy cập cổng 3100.

Reverse proxy có TLS termination phù hợp khi người khác cần truy cập. Proxy quản lý chứng chỉ TLS (transport layer security) và chuyển tiếp đến loopback. README có sẵn cấu hình 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 là dòng nhiều người bỏ sót. Agent có thể tạo stream token trong nhiều phút. Nếu proxy buffer response, proxy sẽ giữ các token đó cho đến khi lượt xử lý kết thúc. Khi đó console trông như bị treo rồi in toàn bộ nội dung cùng lúc. Cấu hình tương đương trong Nginx là proxy_buffering off; bên trong location block. Dù chọn cách nào, hãy giữ DNS name trỏ đến proxy và để container bind trên loopback. So sánh Nginx, Caddy và Traefik khi dùng làm reverse proxy giải thích lựa chọn phù hợp với server của bạn.

Chạy bằng user riêng, không chạy bằng root

Docker daemon chạy bằng root, và việc thành viên của group docker tương đương với có quyền root, vì thành viên có thể khởi động một container mount filesystem của host. Vì vậy, “thêm team vào docker group” đồng nghĩa với việc cấp quyền root trên máy đang lưu provider key của bạn.

Cách đơn giản: tạo một service account sở hữu compose file và .env, đồng thời không đặt các file này trong home directory dùng chung.

sudo adduser --disabled-password --gecos "" harness
sudo install -d -o harness -g harness -m 750 /srv/harnessrouter

Cách bảo mật hơn là dùng rootless Docker, trong đó chính daemon chạy bằng user không có đặc quyền đó. Cách này cần package uidmap cho newuidmapnewgidmap, cùng ít nhất 65536 subordinate UID trong /etc/subuid/etc/subgid cho user này. uidmap có trong Ubuntu archive, nhưng docker-ce-rootless-extras thì không: package này được phát hành từ apt repository riêng của Docker tại download.docker.com, repository được Docker engine install thêm vào. Nếu bạn chưa cài engine từ repository đó, grep -rl download.docker.com /etc/apt/sources.list.d/ sẽ không in ra gì và lệnh install bên dưới sẽ không tìm thấy package.

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 là bắt buộc trong trường hợp này. Nếu thiếu nó, systemd instance của user sẽ dừng khi session cuối cùng đóng, khiến container dừng khi bạn đăng xuất. Xác nhận kết quả bằng docker info; lệnh này liệt kê rootless trong Security Options. Rootless mode không thể bind các port nhỏ hơn 1024 nếu không cấu hình thêm. Điều này không ảnh hưởng ở đây vì port 3000 lớn hơn ngưỡng đó. Cách thiết lập account được trình bày trong tạo user theo nguyên tắc đặc quyền tối thiểu trên VPS.

Các lỗi thường gặp và biểu hiện

Container thoát một giây sau khi khởi động và log trống. docker ps -a hiển thị Exited (1). Đây là lỗi HR_BACKENDS đã nêu ở trên: giá trị của bạn thiếu hermes. Thêm lại giá trị đó.

Lần khởi động đầu tiên không bao giờ hoàn tất. Log dừng sau dòng installingready on :3000 không bao giờ xuất hiện. Máy không thể truy cập mạng để tải các agent CLI vì chúng không có trong image. Sửa route outbound hoặc cấu hình proxy, rồi khởi động lại.

Console tải được nhưng mọi task đều thất bại. Chưa có provider nào được kết nối. Image không kèm model và cũng không có free tier, vì vậy một instance mới có thể cho phép bạn đăng nhập nhưng vẫn không chạy được gì.

Console bị treo giữa câu trả lời khi đi qua proxy. Kết quả chỉ xuất hiện thành một khối khi turn kết thúc. Đây là do response buffering. Đặt flush_interval -1 trong Caddy hoặc proxy_buffering off; trong Nginx.

Bạn không thể truy cập từ laptop dù tunnel đang hoạt động. Chạy docker port harnessrouter trên server. Nếu không có đầu ra, container không publish cổng nào vì đã được khởi động mà không có -p.

Có đáng chạy không?

Đáng chạy nếu bạn thực sự dùng nhiều hơn một harness và muốn có một endpoint cùng một kho credential thay vì ba endpoint và ba kho credential. Cũng đáng chạy nếu bạn đang xây dựng sản phẩm trên đó và muốn harness là một giá trị cấu hình thay vì phải viết lại code. Đó là lợi ích UHP mang lại, với lưu ý ở trên về việc protocol này còn mới.

Không đáng chạy nếu bạn chỉ dùng một harness. Cài CLI đó trực tiếp trên server sẽ có ít thành phần hơn, và không có bước đăng nhập nằm giữa bạn và CLI. Đây cũng không phải mô hình phù hợp nếu bạn muốn nhiều agent phối hợp trong một task, thay vì một API đứng trước nhiều harness; đó là một công cụ khác: xem một harness multi-agent như Omnigent để biết mô hình này. Dù chọn cách nào, các quy tắc triển khai vẫn không đổi. Bind vào loopback, đổi password, dùng tag được ghim ở 0.3.0 trở lên và chạy bằng user riêng.

FAQ

Có an toàn không khi public HarnessRouter trên cổng 3000?

Không. Console có thể tạo harness, đọc mọi transcript, chạy agent với quyền truy cập shell và filesystem, đồng thời giữ provider key mà bạn đã kết nối. Vì vậy, một cổng mở sẽ expose toàn bộ các quyền này. Hãy publish trên loopback bằng -p 127.0.0.1:3000:3000 rồi truy cập qua SSH tunnel hoặc reverse proxy có TLS termination. Chỉ dùng host firewall là chưa đủ: Docker tự ghi rule vào bảng nat của kernel, nên một cổng đã publish vẫn nhận kết nối từ Internet ngay cả khi ufw hiển thị cổng đó là bị deny. Xác minh bằng sudo ss -ltnp | grep 3000; kết quả phải in ra 127.0.0.1:3000.

Phiên bản HarnessRouter nào đã thêm login gate?

0.3.0. Các phiên bản 0.1.x0.2.0 được release mà hoàn toàn không có authentication. Cả hai tag vẫn được publish và vẫn có thể pull, nên ai chạy chúng đang dựa vào việc không ai phát hiện ra cổng đó. Tính đến ngày 19 August 2026, tag mới nhất là 0.5.5, có ngày là 18 August 2026. Chạy docker image ls harnessrouter/harnessrouter để xem phiên bản bạn đang dùng, đối chiếu với danh sách tag trên Docker Hub thay vì đối chiếu với trang này, và đổi password mặc định ngay cả khi bạn đang dùng phiên bản hiện tại.

Tại sao container thoát ngay sau khi tôi đặt HR_BACKENDS?

Bất kỳ giá trị HR_BACKENDS nào không có hermes đều khiến container thoát ngay với status 1 mà không có error message. Đây là issue đã được ghi nhận trong README của project. Dấu hiệu là Exited (1) xuất hiện trong docker ps -a sau một hoặc hai giây, còn docker logs không có thông tin hữu ích. Hãy giữ hermes trong danh sách, như trong HR_BACKENDS=claude,codex,hermes, cho đến khi upstream khắc phục issue này.

HarnessRouter có cần Internet khi khởi động lần đầu không?

Có. Các agent CLI được fetch khi khởi động lần đầu thay vì được ship sẵn trong image, vì mỗi CLI có license riêng. Một máy không có outbound route sẽ in các dòng installing rồi không bao giờ đạt đến ready on :3000. Quá trình download chỉ diễn ra một lần cho mỗi volume. Những lần khởi động sau chỉ mất vài giây và không cần network ngoài kết nối đến model provider mà bạn đã cấu hình.

Tôi quên password của console. Làm sao để đăng nhập lại?

Không có email reset vì hệ thống không có account system và cũng không có mail server. Hãy stop container, xóa /data/selfhost-auth.json khỏi volume, start lại container, rồi đăng nhập bằng credentials mặc định và đặt password mới từ trang Profile. Khi container và volume đều có tên harnessrouter, các lệnh tương ứng là docker stop harnessrouter, sau đó docker run --rm -v harnessrouter:/data alpine rm -f /data/selfhost-auth.json, rồi docker start harnessrouter.