SSD Nodes Learn 🎉 VPS da $5.50/mese
Guide Matt ConnorDi Matt Connor · Aggiornato 2026-08-21

HarnessRouter self-hosted: una API per gli agent

Scopri il deploy Docker di HarnessRouter, il bind loopback, il login predefinito da cambiare e l’accesso TLS per Codex, Claude Code e Hermes.

Cosa elimina HarnessRouter

Puoi eseguire autonomamente HarnessRouter Community Edition per mettere un’unica API davanti a diversi agent harness su un server di tua proprietà. Un agent harness è il programma a riga di comando che esegue un modello in un ciclo: mantiene una sessione, modifica file, esegue comandi e trasmette l’avanzamento al client che ha richiesto il lavoro. Codex, Claude Code e Hermes svolgono tutti questo compito, ma ciascuno ha una propria procedura di installazione, un proprio formato per le credenziali e una propria gestione delle sessioni. HarnessRouter esegue tutti questi strumenti all’interno di un unico container e mette davanti un singolo endpoint HTTP, un singolo accesso e un unico archivio per i secret.

Questa è l’idea di base, ma è importante esplicitarne il costo. Aggiungi un container, un accesso, un volume e una procedura di aggiornamento al server per trasformare diversi componenti in un’unica soluzione. Se oggi esegui un solo harness, questa configurazione è peggiore rispetto all’installazione diretta dell’harness. Questo compromesso viene analizzato nell’ultima sezione; leggila prima di eseguire il deployment.

Tutto ciò che segue è stato verificato con il tag dell’immagine 0.5.5, scaricato il 19 agosto 2026. Il progetto pubblica nuovi tag quasi ogni giorno, quindi controlla il tag effettivamente in esecuzione invece di affidarti a questa pagina tra un mese. I comandi provengono dal README del progetto disponibile all’indirizzo github.com/HarnessRouter/harnessrouter.

Che cos'è realmente lo Unified Harness Protocol

HarnessRouter implementa lo Unified Harness Protocol (UHP), pubblicato su unifiedharnessprotocol.org. UHP descrive come un prodotto avvia un'attività su un harness, la segue durante l'esecuzione, gestisce sessioni e file e segnala gli errori. La specifica è versionata per data. La versione in vigore al 19 agosto 2026 è datata 2026-08-11 e il sito la definisce una bozza di standard, «abbastanza stabile da poter essere adottata come base, con versionamento che consente di modificarla in sicurezza».

Leggete con attenzione l'espressione «standard aperto». La stessa azienda redige la specifica, sviluppa l'implementazione di riferimento e gestisce la suite di conformità composta da 52 verifiche che stabilisce chi è conforme. È una situazione normale per un protocollo così recente e la licenza Apache-2.0 consente di creare un fork di qualsiasi sua parte. Questo significa anche che UHP non è ancora uno standard multi-vendor. Consideratelo un protocollo emergente: utile, in evoluzione e tale da richiedere che il vostro codice possa smettere di utilizzarlo senza dover essere riscritto.

Cosa serve prima di iniziare

Docker e circa 4 GB di spazio libero su disco. Serve anche una chiave API di un provider di modelli per cui disponi già di un abbonamento. Il download dell'immagine richiede circa 700 MB; lo spazio restante viene utilizzato dalle CLI degli agenti e dagli spazi di lavoro in cui scrivono. L'immagine non include alcun modello né una chiave di prova, quindi le attività falliscono finché non si configura un provider. HarnessRouter è distribuito con licenza Apache-2.0. Le CLI degli agenti non sono coperte da questa licenza; per questo vengono scaricate al primo avvio invece di essere incluse nell'immagine.

Self-host HarnessRouter con un solo comando docker run

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

Quindi monitora l'avvio del container. Il primo avvio è lento e i log spiegano il motivo.

docker logs -f harnessrouter

Durante l'avvio vedrai righe simili a queste:

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

Attendi ready on :3000. L'installazione viene eseguita una sola volta per volume. Ogni avvio successivo richiede quindi pochi secondi e non visualizza alcuna riga di installazione.

Da questo download derivano due aspetti importanti, entrambi rilevanti su un VPS. Primo: il primo avvio richiede l'accesso alla rete in uscita. L'immagine non è autonoma. Se il server è protetto da un filtro per il traffico in uscita o non dispone di una route verso l'esterno, resta bloccato in questo punto e non visualizza mai ready on :3000. L'errore si verifica al primo avvio, non in docker pull, quindi può essere difficile individuarlo. Secondo: stai installando software di terze parti soggetto a condizioni di utilizzo di terzi. Claude Code viene fornito secondo le condizioni di Anthropic, mentre Hermes è soggetto alle condizioni indicate dal relativo progetto upstream. Verificale entrambe prima di usare questi componenti in ambito commerciale.

-v harnessrouter:/data crea un volume Docker denominato. Tutti i dati persistenti si trovano in /data: i database SQLite, i file archiviati, il gestore dei secret e gli spazi di lavoro degli agenti. Se elimini quel volume, elimini l'istanza, incluse le chiavi dei provider e tutte le trascrizioni. Esegui il backup con il container arrestato, perché copiare un database SQLite mentre è in scrittura produce un file che potrebbe non aprirsi. La stessa procedura, con arresto prima della copia, si applica a ogni container con stato persistente presente sul server. I dettagli variano però in base al servizio, perché PhotoPrism e Immich richiedono ciascuno comandi di backup specifici.

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

La variante Compose e la riga da modificare

Il repository include un file Compose. Pubblica "3000:3000", quindi è in ascolto su tutte le interfacce dell'host. Modifica quella riga prima di avviare il servizio su un server pubblico.

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:

Rispetto alla versione upstream cambiano due elementi: l'indirizzo di bind e un tag di versione fissato invece di latest. Il pinning è importante perché tra il 9 e il 18 agosto 2026 sono stati pubblicati sedici tag di versione. Un runtime dell'agente che cambia senza preavviso è difficile da sottoporre a debug. Copia quindi il file dell'ambiente, limita i relativi permessi e avvia il servizio.

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

.env contiene la chiave del provider in testo non cifrato, quindi la modalità 600 è il minimo necessario. Se il sottocomando docker compose non ti è familiare, la scheda rapida dei comandi Docker Compose illustra i comandi di uso quotidiano.

Perché la porta è pubblicata su 127.0.0.1 e non su 0.0.0.0

-p 3000:3000 pubblica la porta su tutte le interfacce disponibili sull'host. -p 127.0.0.1:3000:3000 la pubblica soltanto sull'interfaccia di loopback; di conseguenza, è possibile accedere al servizio soltanto dal VPS stesso. Il container resta sempre in ascolto sulla porta 3000 al suo interno, quindi devi modificare il valore a sinistra. Verifica il risultato:

docker port harnessrouter
sudo ss -ltnp | grep 3000

La visualizzazione di ss con 127.0.0.1:3000 è corretta. 0.0.0.0:3000 indica che la console è esposta su Internet. In questo caso il rischio è maggiore rispetto alla maggior parte delle applicazioni self-hosted, perché la console crea harness, legge ogni trascrizione, esegue agenti e fornisce a tali agenti una shell e un filesystem reale nella relativa directory di lavoro. Inoltre contiene la chiave del provider che hai configurato. Chiunque raggiunga una console non protetta può leggere il tuo lavoro, eseguire comandi e utilizzare la tua chiave generando costi.

Un firewall sull'host non è sufficiente. Docker pubblica le porte inserendo regole proprie nella tabella nat del kernel. Queste regole vengono valutate prima della catena gestita da ufw. Di conseguenza, una porta pubblicata resta raggiungibile anche quando sudo ufw status la elenca come negata. Esegui il test da un altro computer, non dal VPS; in caso contrario non verificherai l'accessibilità dall'esterno. È lo stesso principio di eseguire dsh senza interfaccia su port 3080: associa il servizio all'interfaccia di loopback, quindi stabilisci in modo esplicito come raggiungerlo.

Modificare l'accesso predefinito prima di procedere

Accedi a http://localhost:3000 con il nome utente harnessrouter e la password harnessrouter. Queste credenziali sono riportate nel README perché sono valori segnaposto, non secret, e il container mostra un avviso a ogni avvio finché non le modifichi:

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

Modificale dalla pagina Profile oppure impostale all'avvio per una distribuzione tramite script. HR_AUTH_USER e HR_AUTH_PASSWORD sostituiscono i valori predefiniti.

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

Non esiste un'email per reimpostare la password, perché non esiste né un sistema di account né un mail server. Se perdi la password, elimina il file di autenticazione nel volume e riavvia il container, quindi accedi di nuovo usando i valori predefiniti.

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

HR_AUTH_DISABLED=1 rimuove completamente la richiesta di accesso. Il README ne limita l'uso a «un computer che nessun altro può raggiungere». Un VPS con un indirizzo IP pubblico non rientra in questo caso, quindi lascia attiva la richiesta di accesso, a meno che tu non stia eseguendo il servizio su un laptop.

Verifica la versione: quelle precedenti non avevano alcun controllo di accesso

Questa è la parte da prendere sul serio. Le versioni 0.1.x e 0.2.0 sono state rilasciate senza alcun controllo di autenticazione: chiunque potesse raggiungere la porta 3000 era già dentro la console. 0.3.0 è stata la prima release con accesso tramite login. Quei tag meno recenti sono ancora pubblicati e possono ancora essere scaricati. Di conseguenza, un tag obsoleto fissato nella configurazione o un file Compose copiato da un collega può esporre oggi una console senza controllo di accesso su una porta pubblica.

Al 19 agosto 2026, il tag pubblicato più recente è 0.5.5, con data 18 agosto 2026, e latest punta a questo tag. Verifica quale versione stai utilizzando, quindi confrontala con l'elenco dei tag su Docker Hub:

docker image ls harnessrouter/harnessrouter

Qualsiasi versione precedente a 0.3.0 deve essere sostituita subito, senza rimandare l'intervento. Anche le versioni pari o successive richiedono la modifica della password, perché per chi esegue una scansione sulla porta 3000 una password predefinita equivale all'assenza di una password. Non considerare aggiornati i numeri di versione riportati in questa pagina. Erano validi alla data indicata all'inizio e questo progetto pubblica release rapidamente.

Collegare un provider

Nulla viene eseguito finché non viene collegato un provider di modelli. Aggiungine uno dalla pagina Integrations nella console oppure passalo a docker run nell’ambiente. Il valore è in formato JSON, quindi racchiudilo tra virgolette nella shell:

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

.env.example definisce una variabile di connessione per ogni famiglia di provider: HR_SECRET_GLOBAL_HARNESS_CONN_ANTHROPIC per il backend claude-code, HR_SECRET_GLOBAL_HARNESS_CONN_OPENAI per il backend codex e HR_SECRET_GLOBAL_HARNESS_CONN_CUSTOM per qualsiasi endpoint compatibile con OpenAI, dove puoi usare un aggregatore o il tuo server di inferenza. Le variabili HR_SECRET_GLOBAL_HARNESS_POLICY_CLAUDE, HR_SECRET_GLOBAL_HARNESS_POLICY_CODEX e HR_SECRET_GLOBAL_HARNESS_POLICY_HERMES corrispondenti indicano quale connessione usa per impostazione predefinita ciascun backend. HR_SECRET_KEY è un elemento separato ed è necessario solo quando colleghi un database a un agente.

HR_BACKENDS seleziona i backend da caricare, come in HR_BACKENDS=claude,codex,hermes. È importante conoscere un problema noto prima di incontrarlo: qualsiasi valore che non include hermes fa terminare immediatamente il container con stato 1 e senza messaggi di errore. Un secondo dopo l’avvio, in docker ps -a viene visualizzato Exited (1), mentre docker logs non mostra informazioni utili. Mantieni hermes nell’elenco finché il problema non viene corretto a monte. Se vuoi usare Hermes come unico harness, eseguire l’agente Hermes sul proprio VPS è la soluzione con il deployment più semplice.

Chiamare l'API senza la console

La console è facoltativa. La stessa API serve entrambi e utilizza un contratto in stile Responses. Eseguire prima l'accesso per ottenere un cookie di sessione:

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

Inviare quindi un'attività, specificando l'harness in metadata.harness_id e un modello effettivamente fornito dal provider connesso:

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}'

Un oggetto JSON contenente un blocco di output e un conteggio dei token indica che l'harness è stato eseguito. Modificando harness_id da codex a claude, la stessa richiesta viene inviata a un harness diverso. Questo scambio è l'intero motivo per cui esiste questo software. La connessione personalizzata configurata sopra consente di indirizzare un harness verso un endpoint compatibile con OpenAI già ospitato dall'utente, come nel caso di un harness DeepSeek self-hosted su un VPS.

Accedere dal laptop senza pubblicare una porta

Esistono due metodi e nessuno dei due espone direttamente una porta su 0.0.0.0.

Un tunnel SSH è la soluzione più semplice e non richiede l'installazione di nulla sul server. Inoltra una porta locale del computer verso il loopback della VPS.

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

Lascia il tunnel attivo e apri http://localhost:3000 nel browser. Se SSH stampa bind: Address already in use, significa che sul laptop la porta 3000 è già occupata. Scegli quindi un'altra porta locale con -L 3100:127.0.0.1:3000 e apri la porta 3100 nel browser.

Un reverse proxy con terminazione TLS è la soluzione adatta quando devono accedere anche altre persone. Il proxy gestisce il certificato TLS (transport layer security) e inoltra le richieste verso il loopback. Il README include una configurazione per 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 è la direttiva che viene più spesso dimenticata. Agent invia i token dello stream per diversi minuti e un proxy che memorizza nella cache la risposta trattiene questi token fino al termine dell'elaborazione. La console sembra quindi bloccata e poi visualizza tutto in una volta. L'equivalente in Nginx è proxy_buffering off; all'interno del blocco location. Qualunque soluzione tu scelga, mantieni il nome DNS puntato al proxy e il container in ascolto sul loopback. Confronto tra Nginx, Caddy e Traefik come reverse proxy spiega quale soluzione è più adatta al tuo server.

Eseguilo con un utente dedicato, non con root

Il demone Docker viene eseguito come root e appartenere al gruppo docker equivale ad avere privilegi root, perché un membro può avviare un container che monta il filesystem dell'host. Quindi, «aggiungere il team al gruppo docker» assegna privilegi root sul server che contiene la chiave del provider.

La soluzione più semplice consiste nel creare un account di servizio proprietario del file Compose e di .env, mantenendo questi file al di fuori di qualsiasi home directory condivisa.

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

La soluzione più robusta è usare Docker rootless, in cui anche il demone viene eseguito dall'utente non privilegiato. Sono necessari il pacchetto uidmap per newuidmap e newgidmap e almeno 65536 UID subordinati in /etc/subuid e /etc/subgid per l'utente.

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 è indispensabile in questo caso. Senza di esso, l'istanza systemd dell'utente si arresta quando termina l'ultima sessione e il container si interrompe al logout. Verifica il risultato con docker info, che elenca rootless nella sezione Security Options. La modalità rootless non può associare porte inferiori a 1024 senza una configurazione aggiuntiva. Questo non è un problema in questo caso, perché la porta 3000 è superiore a tale limite. La configurazione dell'account è descritta in creazione di utenti con privilegi minimi su un VPS.

Cosa si rompe e cosa vedrai

Il container termina un secondo dopo l'avvio e i log sono vuoti. docker ps -a mostra Exited (1). Questo indica il problema HR_BACKENDS descritto sopra: nel valore manca hermes. Ripristinalo.

Il primo avvio non termina mai. Il log si interrompe dopo una riga installing e ready on :3000 non compare mai. Il server non riesce a raggiungere la rete per scaricare le CLI dell'agent, perché non sono incluse nell'immagine. Correggi la route in uscita o le impostazioni del proxy, quindi riavvia.

La console si carica, ma ogni attività ha esito negativo. Non è connessun provider. Nell'immagine non sono inclusi né un modello né un piano gratuito, quindi una nuova istanza può completare l'accesso senza riuscire a eseguire alcuna attività.

La console si blocca durante una risposta dietro un proxy. L'output compare in un unico blocco al termine del turno. Si tratta del buffering della risposta. Imposta flush_interval -1 in Caddy oppure proxy_buffering off; in Nginx.

Non riesci a raggiungerla dal laptop, anche se il tunnel è attivo. Esegui docker port harnessrouter sul server. Se non stampa nulla, il container non pubblica alcuna porta, perché è stato avviato senza -p.

Vale la pena eseguirlo?

Vale la pena eseguirlo se usi davvero più di un harness e vuoi un unico endpoint e un unico archivio delle credenziali invece di tre endpoint e tre archivi. Vale la pena anche se stai sviluppando un prodotto basato su questa soluzione e vuoi che l’harness sia un valore di configurazione anziché richiedere una riscrittura. È questo il vantaggio offerto da UHP, con la riserva già indicata sul fatto che il protocollo è ancora recente.

Non vale la pena eseguirlo se usi un solo harness. Installare quella CLI sul server comporta meno componenti e nessun accesso intermedio tra te e il servizio. È inoltre la soluzione sbagliata se vuoi che più agenti collaborino alla stessa attività invece di avere una singola API davanti a più harness: si tratta di uno strumento diverso. Per questo scenario, consulta un harness multi-agent come Omnigent. In ogni caso, le regole di distribuzione non cambiano: binding su loopback, password modificata, tag fissato a 0.3.0 o superiore e un utente dedicato.

FAQ

È sicuro pubblicare HarnessRouter sulla porta 3000?

No. La console crea harness, legge ogni trascrizione, esegue agenti con accesso alla shell e al filesystem e contiene la chiave del provider collegato. Una porta esposta rende quindi disponibili tutte queste funzioni. Pubblica il servizio sull'interfaccia loopback con -p 127.0.0.1:3000:3000 e raggiungilo tramite un tunnel SSH o un reverse proxy con terminazione TLS. Un firewall sull'host non è sufficiente da solo: Docker inserisce le proprie regole nella tabella del kernel nat, quindi una porta pubblicata risponde da Internet anche quando ufw la mostra come negata. Verifica con sudo ss -ltnp | grep 3000, che dovrebbe stampare 127.0.0.1:3000.

Quale versione di HarnessRouter ha aggiunto il controllo di accesso?

0.3.0. Le versioni 0.1.x e 0.2.0 sono state rilasciate senza alcuna autenticazione. Entrambi i tag sono ancora pubblicati e possono ancora essere scaricati, quindi chi li esegue si affida al fatto che nessuno trovi la porta. Al 19 agosto 2026, il tag più recente è 0.5.5, datato 18 agosto 2026. Esegui docker image ls harnessrouter/harnessrouter per vedere quale versione è installata, confrontala con l'elenco dei tag su Docker Hub e non con questa pagina, quindi cambia la password predefinita anche se usi una versione aggiornata.

Perché il container termina subito dopo che imposto HR_BACKENDS?

Qualsiasi valore HR_BACKENDS che omette hermes fa terminare immediatamente il container con stato 1 e senza messaggi di errore. Si tratta di un problema noto, documentato nel README del progetto. Il sintomo è Exited (1) in docker ps -a entro uno o due secondi e l'assenza di informazioni utili in docker logs. Mantieni hermes nell'elenco, come in HR_BACKENDS=claude,codex,hermes, finché il progetto upstream non risolve il problema.

HarnessRouter richiede l'accesso a Internet al primo avvio?

Sì. Le CLI degli agenti vengono scaricate al primo avvio invece di essere incluse nell'immagine, perché ciascuna ha una propria licenza. Un host senza una route in uscita stampa le righe installing e poi non raggiunge mai ready on :3000. Il download avviene una volta per volume. Gli avvii successivi richiedono pochi secondi e non necessitano di rete oltre a quella del provider del modello collegato.

Ho perso la password della console. Come posso accedere di nuovo?

Non esiste un'email di reimpostazione, perché non esistono né un sistema di account né un server di posta. Arresta il container, elimina /data/selfhost-auth.json dal volume, avvialo di nuovo, quindi accedi con le credenziali predefinite e imposta una nuova password dalla pagina Profile. Se il container e il volume si chiamano entrambi harnessrouter, esegui docker stop harnessrouter, poi docker run --rm -v harnessrouter:/data alpine rm -f /data/selfhost-auth.json e infine docker start harnessrouter.