SSD Nodes Learn 🎉 VPS từ $5.50/tháng
Hướng dẫn Matt ConnorBởi Matt Connor · Cập nhật ngày 2026-08-21

Self-host HarnessRouter: một API cho nhiều agent

Chạy Codex, Claude Code và Hermes qua một API tự host. Xem Docker deploy, 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 server do bạn sở hữu. Agent harness là chương trình command line điều khiển model trong một vòng lặp: nó duy trì session, chỉnh sửa file, chạy command và stream tiến độ về nơi đã yêu cầu thực hiện công việc. Codex, Claude Code và Hermes đều làm công việc này. Mỗi công cụ có cách cài đặt, format credential 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. Chi phí của cách này cần được nói rõ. 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 để biến 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 trade-off này, vì vậy hãy đọc trước khi deploy.

Toàn bộ nội dung dưới đây đã được kiểm tra với image tag 0.5.5, được pull vào ngày 19 tháng 8 năm 2026. Project thường publish tag mới vào hầu hết các ngày, vì vậy hãy kiểm tra tag bạn thực sự đang chạy thay vì tin hoàn toàn vào trang này sau một tháng. Các command 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 product 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, đồng thời báo cáo lỗi. Spec được version hóa theo ngày. Phiên bản đang được áp dụng tính đến ngày 19 August 2026 có ngày version là 2026-08-11. Website gọi đây là một draft standard, “đủ ổn định để xây dựng trên đó và được version hóa để có thể thay đổi an toàn”.

Hãy đọc cẩn thận cụm “open standard” trong trường hợp này. Cùng một công ty viết specification, reference implementation và conformance suite gồm 52 kiểm tra dùng để quyết định thành phần nào conform. Điều này là 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à standard đa vendor. 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 phải viết lại.

Bạn cần chuẩn bị gì trước khi bắt đầu

Docker và khoảng 4 GB dung lượng disk trống. Bạn cũng cần API key từ một model provider mà bạn đã đăng ký trả phí. Việc pull image cần khoảng 700 MB, phần disk 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 và cũng không có trial key, nên task sẽ fail cho đến khi bạn kết nối với một provider. HarnessRouter được cấp phép theo Apache-2.0. Các agent CLI không thuộc license đó, vì vậy chúng được fetch trong lần khởi động đầu tiên thay vì được đóng gói sẵn 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 sẽ cho biết lý do.

docker logs -f harnessrouter

Khi chạy thành cô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)…

Hãy chờ ready on :3000. Quá trình cài đặt này chỉ diễn ra 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ểm cần rút ra từ lần download đó, và cả hai đều quan trọng trên VPS. Thứ nhất, lần boot đầu tiên cần có quyền truy cập network outbound. Image không chứa sẵn mọi thứ, nên một máy bị egress filter chặn hoặc không có route ra ngoài sẽ bị treo ở đây và không bao giờ in ready on :3000. Lỗi xảy ra ngay lần start đầu tiên, không phải tại docker pull, nên rất dễ phát hiện quá muộn. Thứ hai, bạn đang cài software 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 quy định, vì vậy hãy kiểm tra cả hai 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 đều nằm trong /data: database SQLite, file đã lưu, kho secret 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 đã dừng, vì sao chép database SQLite trong lúc database đang được ghi có thể tạo ra file không mở được. Nguyên tắc dừng rồi mới sao chép cũng áp dụng cho mọi container có state trên máy, dù chi tiết khác nhau 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 cung cấp sẵn file compose. File này publish "3000:3000", nghĩa là bind trên mọi interface của host. Hãy thay đổi dòng đó trước khi khởi chạy 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 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ẽ khó debug. Sau đó, hãy copy file môi trường, khóa permission của file và khởi động.

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

.env lưu provider key dưới dạng plain text, vì vậy mode 600 là mức tối thiểu. Nếu chưa quen với subcommand docker compose, cheat sheet về lệnh Docker Compose trình bày các verb 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 listen 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

Lệnh ss in ra 127.0.0.1:3000 là đúng. 0.0.0.0:3000 nghĩa là console đang nằm trên public Internet. Trong trường hợp này, điều đó nguy hiểm hơn hầu hết ứng dụng self-host, vì console tạo harness, đọc toàn bộ transcript, chạy agent, đồng thời cấp cho các agent đó một shell và filesystem thật trong workspace của chúng. Console cũng giữ provider key mà bạn đã kết nối. Bất kỳ ai truy cập được console không được bảo vệ đều có thể đọc công việc của bạn, chạy command và sử dụng key của bạn.

Host firewall không bảo vệ được bạn trong trường hợp này. Docker publish port bằng cách ghi rule của chính nó vào bảng kernel nat. 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ị từ chối. Hãy test 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, sau đó chủ động quyết định cách bạn truy cập service đó.

Đổi thông tin đăng nhập mặc định trước khi làm bất kỳ việc gì khác

Đă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ị placeholder, không phải secret. Container sẽ cảnh báo ở mỗi lần khởi động cho đến khi bạn đổi chúng:

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

Đổi thông tin này từ trang Profile hoặc đặt ngay lúc khởi động khi triển khai bằng script. HR_AUTH_USERHR_AUTH_PASSWORD sẽ 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 password vì hệ thống không có account và cũng không có mail server. Nếu bạn mất password, hãy xóa auth file trong volume rồi restart. 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 chỉ áp dụng tùy chọn này cho “một máy 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 version vì các bản cũ không có lớp bảo vệ

Đây là phần cần đặc biệt chú ý. Các version 0.1.x0.2.0 được phát hành mà hoàn toàn không có bước xác thực: bất kỳ ai truy cập được port 3000 đều đã vào được console. 0.3.0 là bản phát hành đầu tiên có màn hình đăng nhập. Các tag cũ đó vẫn được công khai và vẫn có thể pull, nên 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ó xác thực chạy trên một port public ngay hôm nay.

Tính đến ngày 19 August 2026, tag mới nhất được phát hành là 0.5.5, có ngày phát hành 18 August 2026, và latest trỏ đến tag đó. Kiểm tra version đ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ỳ version nào thấp hơn 0.3.0 đều phải được thay thế ngay, không được lên lịch để xử lý sau. Các version bằng hoặc cao hơn version này 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 port 3000. Không được xem các version trên trang này là hiện tại. Chúng đúng tại thời điểm ghi ở đầu bài, còn project này phát hành rất nhanh.

Kết nối provider

Không có gì chạy cho đến khi kết nối một model provider. Thêm một provider từ trang Integrations trong console, hoặc truyền provider đó vào docker run trong environment. Giá trị này là JSON, vì vậy hãy đặt nó 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 từng họ 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 cấu hình một aggregator hoặc inference server của bạn. 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 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. Hãy 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 riêng agent Hermes trên VPS của bạn là phương án triển khai gọn hơn.

Gọi API không cần console

Console là tùy chọn. Cả hai cùng sử dụng một API và API này dùng contract theo 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 bạn đã 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}'

Một JSON object chứa output block và số token cho biết harness đã chạy. Thay harness_id từ codex sang claude sẽ gửi cùng request đến một harness khác. Việc chuyển đổi này chính là lý do phần mềm tồn tại. 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 port

Có 2 cách, và cả 2 đều không mở trực tiếp một port 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ó forward một port 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ữ lệnh đó chạy rồi mở http://localhost:3000 trong browser. Nếu SSH in ra bind: Address already in use, nghĩa là trên laptop đã có tiến trình chiếm port 3000. Hãy chọn một port local khác bằng -L 3100:127.0.0.1:3000 rồi truy cập port 3100.

Reverse proxy terminating TLS phù hợp khi người khác cũng cần truy cập. Proxy giữ certificate TLS (transport layer security) và forward request đế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 tạo stream token trong nhiều phút. Proxy buffer response sẽ giữ các token đó cho đến khi turn kết thúc, khiến 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 trỏ DNS name vào proxy và giữ container 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 máy của bạn.

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

Docker daemon chạy bằng root. Việc là thành viên của group docker tương đương với có quyền root, vì thành viên của group này có thể khởi động container rồi mount filesystem của host. Vì vậy, “thêm team vào group docker” đồ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. 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 an toàn hơn là dùng rootless Docker. Khi đó, chính daemon chạy bằng user không có đặc quyền này. Rootless Docker cần package uidmap cho newuidmapnewgidmap. User cũng cần ít nhất 65536 subordinate UID trong /etc/subuid/etc/subgid.

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 không có, systemd instance của user sẽ dừng khi session cuối cùng đóng. Container vì thế sẽ dừng khi bạn logout. 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 vào các port nhỏ hơn 1024 nếu không có cấu hình bổ sung. Điều này không ảnh hưởng ở đây vì port 3000 lớn hơn giới hạn đó. Cách tạo account được trình bày trong tạo user có quyền tối thiểu trên VPS.

Những lỗi thường gặp và dấu hiệu nhận biết

Container thoát sau một giây kể từ khi khởi động và log trống. docker ps -a hiển thị Exited (1). Đây là lỗi HR_BACKENDS được 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ể kết nối đến network để tải các agent CLI vì chúng không có trong image. Sửa route outbound hoặc proxy settings, rồi khởi động lại.

Console tải được nhưng mọi task đều fail. Chưa kết nối provider nào. Image không có model đi kèm và cũng không có free tier, nên 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 chừng khi chạy sau proxy. Output chỉ xuất hiện thành một block 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 in ra gì, 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 lưu trữ credential thay vì phải quản lý ba endpoint và ba kho credential. Cũng đáng chạy nếu bạn đang xây dựng một sản phẩm dựa trên UHP 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 cần quản lý hơn, và không có bước login nằm giữa bạn và harness. Đâ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 xử lý một task, thay vì đặt một API phía trước nhiều harness. Trường hợp đó dùng một công cụ khác: xem một harness nhiều agent như Omnigent. Dù chọn cách nào, các nguyên tắc triển khai vẫn không đổi: bind vào loopback, đổi password, dùng tag cố định ở 0.3.0 trở lên và chạy bằng user riêng.

FAQ

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

Không. Console tạo harness, đọc mọi transcript, chạy agent có quyền shell và filesystem, đồng thời lưu provider key mà bạn đã kết nối, nên một port mở sẽ để lộ toàn bộ các thành phần này. Chỉ public trên loopback bằng -p 127.0.0.1:3000:3000 và 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 kernel nat, nên port đã publish vẫn nhận kết nối từ Internet dù ufw cho thấy port đó bị deny. Xác minh bằng sudo ss -ltnp | grep 3000; kết quả cần 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 pull được, nên ai chạy chúng đang dựa vào việc không ai tìm thấy port. Tính đến 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 đ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 đ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?

Mọi giá trị HR_BACKENDS không có hermes đều khiến container thoát ngay với status 1 và không có error message. Đây là issue đã được ghi nhận trong README của project. Triệu chứng là Exited (1) 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. Giữ hermes trong danh sách, như trong HR_BACKENDS=claude,codex,hermes, cho đến khi upstream sửa lỗi.

HarnessRouter có cần truy cập Internet trong lần start đầu tiên không?

Có. Các agent CLI được fetch trong lần start đầu tiên 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 tới ready on :3000. Việc download chỉ diễn ra một lần cho mỗi volume, nên các lần start 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 cách nào để đăng nhập lại?

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