Auto-héberger HarnessRouter pour Codex et Claude Code
Déployez Codex, Claude Code et Hermes derrière une API auto-hébergée avec Docker. Vérifiez le bind loopback, changez le login par défaut et activez TLS.
Ce que supprime HarnessRouter
Vous auto-hébergez HarnessRouter Community Edition pour placer une API devant plusieurs agent harnesses sur un serveur que vous contrôlez. Un agent harness est le programme en ligne de commande qui pilote un modèle en boucle : il conserve une session, modifie des fichiers, exécute des commandes et transmet l’avancement au demandeur. Codex, Claude Code et Hermes remplissent tous ce rôle. Chacun possède son propre mode d’installation, son propre format d’identifiants et sa propre conception d’une session. HarnessRouter les exécute tous dans un seul conteneur et fournit un endpoint HTTP unique, une authentification unique et un stockage de secrets unique.
C’est toute l’idée, et son coût mérite d’être explicité. Vous ajoutez un conteneur, une authentification, un volume et une procédure de mise à niveau à votre serveur pour regrouper plusieurs composants en un seul. Si vous n’exécutez aujourd’hui qu’un seul harness, cette configuration est moins intéressante que l’installation directe de ce harness. Ce compromis est traité dans la dernière section ; lisez-la avant de déployer.
Tout ce qui suit a été vérifié avec le tag d’image 0.5.5, récupéré le 19 août 2026. Le projet publie de nouveaux tags presque tous les jours. Vérifiez donc le tag que vous exécutez réellement au lieu de vous fier à cette page dans un mois. Les commandes proviennent du README du projet, à l’adresse github.com/HarnessRouter/harnessrouter.
Ce que le protocole Unified Harness est réellement
HarnessRouter implémente le Unified Harness Protocol (UHP), publié sur unifiedharnessprotocol.org. UHP décrit comment un produit démarre une tâche sur un harness, la suit pendant son exécution, gère les sessions et les fichiers, puis signale les échecs. La spécification est versionnée par date. La version en vigueur au 19 août 2026 est datée du 2026-08-11, et le site la présente comme un standard en cours d’élaboration, « suffisamment stable pour servir de base, et versionné pour pouvoir évoluer sans risque ».
Lisez attentivement l’expression « standard ouvert ». La même entreprise rédige la spécification, fournit l’implémentation de référence et maintient la suite de conformité composée de 52 vérifications qui détermine quels produits sont conformes. Cette situation est courante pour un protocole aussi récent, et la licence Apache-2.0 vous permet de forker n’importe quelle partie. Cela signifie également que l’UHP n’est pas encore un standard multiconstructeur. Considérez-le comme un protocole émergent : utile, en évolution, et que votre propre code doit pouvoir abandonner sans nécessiter de réécriture.
Prérequis
Docker et environ 4 GB d’espace disque disponible. Vous avez également besoin d’une clé API fournie par un fournisseur de modèles auquel vous êtes déjà abonné. Le téléchargement de l’image représente environ 700 MB. Le reste de l’espace disque est utilisé par les CLI des agents et les workspaces dans lesquels ils écrivent. L’image n’inclut aucun modèle ni aucune clé d’essai. Les tâches échouent donc tant que vous n’avez pas connecté de fournisseur. HarnessRouter est lui-même distribué sous licence Apache-2.0. Les CLI des agents ne sont pas couvertes par cette licence. Elles sont donc téléchargées au premier démarrage au lieu d’être incluses dans l’image.
Installer HarnessRouter avec un simple docker run
docker pull harnessrouter/harnessrouter
docker run -d --name harnessrouter \
-p 127.0.0.1:3000:3000 \
-v harnessrouter:/data \
harnessrouter/harnessrouterSurveillez ensuite le démarrage du conteneur. Le premier démarrage est lent, et les journaux en expliquent la raison.
docker logs -f harnessrouterVous verrez des lignes similaires pendant l’installation :
installing Claude Code (Anthropic's terms apply)…
installing Codex (Apache-2.0)…
installing Hermes (check its upstream license before use)…Attendez ready on :3000. Cette installation n’a lieu qu’une fois par volume. Les démarrages suivants prennent donc quelques secondes et n’affichent aucune ligne d’installation.
Deux faits découlent de ce téléchargement, et tous deux sont importants sur un VPS. Premièrement, le premier démarrage nécessite un accès réseau sortant. L’image n’est pas autonome. Un serveur derrière un filtre egress, ou sans route vers l’extérieur, reste donc bloqué ici et n’affiche jamais ready on :3000. L’échec se produit au premier démarrage, et non au niveau de docker pull, ce qui complique le diagnostic. Deuxièmement, vous installez des logiciels tiers sous des conditions définies par des tiers. Claude Code est distribué sous les conditions d’Anthropic et Hermes sous celles définies par son projet amont. Vérifiez donc les deux avant toute utilisation commerciale.
-v harnessrouter:/data crée un volume Docker nommé. Toutes les données persistantes se trouvent dans /data : les bases SQLite, les fichiers stockés, le magasin de secrets et les espaces de travail des agents. Supprimer ce volume revient à supprimer l’instance, y compris les clés des fournisseurs et tous les transcripts. Effectuez la sauvegarde lorsque le conteneur est arrêté. Copier une base SQLite pendant une écriture produit un fichier qui peut ne plus s’ouvrir. La même règle — arrêter puis copier — s’applique à tous les conteneurs avec état du serveur, même si les détails varient selon le service, car PhotoPrism et Immich nécessitent chacun leurs propres commandes de sauvegarde.
docker stop harnessrouter
docker run --rm -v harnessrouter:/data -v "$PWD":/backup alpine \
tar czf /backup/harnessrouter-data.tgz -C / data
docker start harnessrouterLa variante Compose et la ligne à modifier
Le dépôt fournit un fichier Compose. Il publie "3000:3000", ce qui signifie qu’il écoute sur toutes les interfaces de l’hôte. Modifiez cette ligne avant de le démarrer sur un serveur public.
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:Deux éléments diffèrent de la version amont : l’adresse d’écoute et un tag de version figé à la place de latest. Le figement de la version est important : 16 tags de version ont été publiés entre le 9 et le 18 août 2026, et il est difficile de déboguer un runtime d’agent qui change sans contrôle. Copiez ensuite le fichier d’environnement, restreignez ses permissions, puis démarrez le service.
cp .env.example .env
chmod 600 .env
docker compose up -d
docker compose logs -f.env contient la clé de votre fournisseur en texte brut. Le mode 600 est donc le minimum requis. Si la sous-commande docker compose ne vous est pas familière, la fiche pratique des commandes Docker Compose présente les commandes courantes.
Why the port is published on 127.0.0.1 and not 0.0.0.0
-p 3000:3000 publishes the port on every interface the host has. -p 127.0.0.1:3000:3000 publishes it on loopback only, which means the only way in is from the VPS itself. The container always listens on 3000 inside, so the left-hand side is the part you change. Check what you got:
docker port harnessrouter
sudo ss -ltnp | grep 3000ss printing 127.0.0.1:3000 is right. 0.0.0.0:3000 means the console is on the public internet. That is worse here than for most self-hosted apps, because the console creates harnesses, reads every transcript, runs agents, and gives those agents a shell and a real filesystem in their workspace. It also holds the provider key you connected. Anyone who reaches an unprotected console can read your work, run commands, and spend your key.
A host firewall does not save you from this. Docker publishes ports by writing its own rules into the kernel nat table, and those are evaluated before the chain ufw manages, so a published port stays reachable even when sudo ufw status lists it as denied. Test from another machine, not from the VPS, or you will test nothing. This is the same lesson as running dsh headless on port 3080: bind the service to loopback, then decide deliberately how you reach it.
Modifier les identifiants de connexion par défaut avant toute autre chose
Connectez-vous à http://localhost:3000 avec le nom d’utilisateur harnessrouter et le mot de passe harnessrouter. Ces identifiants sont indiqués dans le README, car ce sont des valeurs par défaut et non des secrets. Le conteneur vous avertit à chaque démarrage tant que vous ne les avez pas modifiés :
using the DEFAULT password. Set HR_AUTH_PASSWORD, or change it from the profile page, before exposing this instance.Modifiez-les depuis la page Profile ou définissez-les au démarrage pour un déploiement automatisé. HR_AUTH_USER et HR_AUTH_PASSWORD remplacent les valeurs par défaut.
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/harnessrouterIl n’existe aucune procédure de réinitialisation par e-mail, car il n’y a ni système de comptes ni serveur de messagerie. Si vous perdez le mot de passe, supprimez le fichier d’authentification dans le volume, puis redémarrez. Connectez-vous ensuite de nouveau avec les identifiants par défaut.
docker stop harnessrouter
docker run --rm -v harnessrouter:/data alpine rm -f /data/selfhost-auth.json
docker start harnessrouterHR_AUTH_DISABLED=1 supprime complètement l’écran de connexion. Le README précise que cette option est destinée à « une machine à laquelle personne d’autre ne peut accéder ». Un VPS avec une adresse IP publique ne correspond pas à ce cas. Laissez donc l’authentification activée, sauf si vous exécutez ce service sur un ordinateur portable.
Vérifiez votre version, car les anciennes n’ont aucun contrôle d’accès
C’est le point à prendre au sérieux. Les versions 0.1.x et 0.2.0 ont été publiées sans aucun contrôle d’authentification : toute personne pouvant atteindre le port 3000 avait déjà accès à la console. 0.3.0 est la première version à intégrer une connexion. Ces anciennes versions sont toujours publiées et peuvent toujours être récupérées. Un tag ancien épinglé, ou un fichier Compose copié d’un collègue, peut donc exposer aujourd’hui une console sans contrôle d’accès sur un port public.
Au 19 août 2026, le dernier tag publié est 0.5.5, daté du 18 août 2026, et latest pointe vers celui-ci. Vérifiez la version utilisée, puis comparez-la à la liste des tags sur Docker Hub :
docker image ls harnessrouter/harnessrouterToute version antérieure à 0.3.0 doit être remplacée immédiatement, et non planifiée ultérieurement. Toute version égale ou supérieure à cette version nécessite également de changer le mot de passe, car, pour une personne qui analyse le port 3000, un mot de passe par défaut et l’absence de mot de passe reviennent au même. Ne considérez pas les numéros de version indiqués sur cette page comme actuels. Ils étaient valides à la date indiquée en haut de la page, et ce projet publie rapidement de nouvelles versions.
Connecter un fournisseur
Rien ne fonctionne tant qu’aucun fournisseur de modèles n’est connecté. Ajoutez-en un depuis la page Integrations de la console, ou transmettez-le à docker run dans l’environnement. La valeur est au format JSON : mettez-la entre guillemets dans le shell :
-e HR_SECRET_GLOBAL_HARNESS_CONN_ANTHROPIC='{"name":"anthropic","provider":"anthropic","api_key":"sk-ant-…"}'.env.example définit une variable de connexion par famille de fournisseurs : HR_SECRET_GLOBAL_HARNESS_CONN_ANTHROPIC pour le backend claude-code, HR_SECRET_GLOBAL_HARNESS_CONN_OPENAI pour le backend codex et HR_SECRET_GLOBAL_HARNESS_CONN_CUSTOM pour tout endpoint compatible avec OpenAI, notamment un agrégateur ou votre propre serveur d’inférence. Les variables HR_SECRET_GLOBAL_HARNESS_POLICY_CLAUDE, HR_SECRET_GLOBAL_HARNESS_POLICY_CODEX et HR_SECRET_GLOBAL_HARNESS_POLICY_HERMES correspondantes indiquent quelle connexion chaque backend utilise par défaut. HR_SECRET_KEY est distinct et n’est requis que lorsque vous connectez une base de données à un agent.
HR_BACKENDS sélectionne les backends à charger, comme dans HR_BACKENDS=claude,codex,hermes. Un problème connu doit être signalé : toute valeur qui omet hermes fait quitter immédiatement le conteneur avec le statut 1, sans message d’erreur. Exited (1) apparaît dans docker ps -a une seconde après le démarrage, et docker logs n’affiche rien d’utile. Conservez hermes dans la liste jusqu’à sa correction en amont. Si Hermes est le seul harness dont vous avez besoin, exécuter l’agent Hermes sur son propre VPS constitue le déploiement le plus simple.
Appeler l’API sans la console
La console est facultative. Les deux utilisent la même API, qui respecte un contrat de type Responses. Connectez-vous d’abord pour obtenir un cookie de session :
curl -c hr.cookies http://localhost:3000/api/selfhost/login \
-H 'content-type: application/json' \
-d '{"username":"harnessrouter","password":"your-password"}'Envoyez ensuite une tâche en indiquant le harness dans metadata.harness_id et un modèle effectivement fourni par votre fournisseur connecté :
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 objet JSON contenant un bloc de sortie et un nombre de tokens indique que le harness a exécuté la tâche. Remplacer codex par claude dans harness_id envoie la même requête à un autre harness. Ce changement est la raison d’être de ce logiciel. La connexion personnalisée présentée plus haut permet de connecter un harness à un endpoint compatible avec OpenAI que vous hébergez déjà, comme dans un harness DeepSeek auto-hébergé sur un VPS.
Y accéder depuis votre laptop sans publier de port
Deux méthodes sont possibles, et aucune n’expose directement un port sur 0.0.0.0.
Un tunnel SSH est la solution la plus simple. Aucun logiciel supplémentaire n’est nécessaire sur le serveur. Il redirige un port local de votre machine vers l’interface loopback du VPS.
ssh -N -L 3000:127.0.0.1:3000 you@your-vpsLaissez cette commande s’exécuter, puis ouvrez http://localhost:3000 dans votre navigateur. Si SSH affiche bind: Address already in use, un autre programme utilise déjà le port 3000 sur votre laptop. Choisissez alors un autre port local avec -L 3100:127.0.0.1:3000 et ouvrez le port 3100 dans votre navigateur.
Un reverse proxy avec terminaison TLS convient lorsque d’autres personnes doivent y accéder. Le proxy gère le certificat TLS (transport layer security) et transmet les requêtes vers l’interface loopback. Le README fournit une configuration 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 est la ligne souvent oubliée. Agent génère des tokens de flux pendant plusieurs minutes. Un proxy qui met la réponse en buffer conserve ces tokens jusqu’à la fin du tour. La console semble alors bloquée, puis affiche tout d’un coup. L’équivalent dans Nginx est proxy_buffering off; dans le bloc location. Quelle que soit la solution choisie, laissez le nom DNS pointer vers le proxy et le conteneur rester lié à l’interface loopback. Comparaison de Nginx, Caddy et Traefik comme reverse proxy explique lequel convient à votre serveur.
Exécutez-le avec un utilisateur dédié, pas avec root
Le daemon Docker s’exécute avec root, et l’appartenance au groupe docker équivaut aux privilèges de root, car un membre peut démarrer un conteneur qui monte le système de fichiers de l’hôte. Ajouter l’équipe au groupe docker revient donc à lui accorder les privilèges de root sur le serveur qui contient votre clé de fournisseur.
La solution simple consiste à créer un compte de service propriétaire du fichier Compose et de .env, puis à conserver ces fichiers en dehors de tout répertoire personnel partagé.
sudo adduser --disabled-password --gecos "" harness
sudo install -d -o harness -g harness -m 750 /srv/harnessrouterLa solution plus stricte consiste à utiliser Docker rootless : le daemon s’exécute alors lui-même avec cet utilisateur non privilégié. Cette configuration nécessite le paquet uidmap pour newuidmap et newgidmap, ainsi qu’au moins 65536 UID subordonnés dans /etc/subuid et /etc/subgid pour l’utilisateur.
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 dockerloginctl enable-linger est obligatoire ici. Sans lui, l’instance systemd de l’utilisateur s’arrête lorsque la dernière session se ferme, et le conteneur s’arrête lorsque vous vous déconnectez. Vérifiez le résultat avec docker info, qui affiche rootless dans Security Options. Le mode rootless ne peut pas utiliser les ports inférieurs à 1024 sans configuration supplémentaire. Cela n’a pas d’importance ici, car le port 3000 est supérieur à cette limite. La création du compte est expliquée dans créer des utilisateurs avec le principe du moindre privilège sur un VPS.
Ce qui casse et ce que vous verrez
Le conteneur s’arrête une seconde après son démarrage et les journaux sont vides. docker ps -a affiche Exited (1). Il s’agit du problème HR_BACKENDS décrit plus haut : votre valeur ne contenait pas hermes. Ajoutez-le.
Le premier démarrage ne se termine jamais. Le journal s’arrête après une ligne installing et ready on :3000 n’apparaît jamais. Le conteneur ne peut pas accéder au réseau pour récupérer les CLI des agents, car elles ne sont pas incluses dans l’image. Corrigez la route sortante ou les paramètres du proxy, puis redémarrez.
La console se charge, mais chaque tâche échoue. Aucun provider n’est connecté. Aucun modèle n’est inclus dans l’image et aucun free tier n’y est disponible. Une instance neuve peut donc vous authentifier sans pouvoir exécuter la moindre tâche.
La console se fige au milieu d’une réponse derrière un proxy. La sortie apparaît en un seul bloc à la fin du tour. Il s’agit d’un buffering de la réponse. Définissez flush_interval -1 dans Caddy ou proxy_buffering off; dans Nginx.
Vous ne pouvez pas y accéder depuis votre laptop alors que le tunnel est actif. Exécutez docker port harnessrouter sur le serveur. Si la commande n’affiche rien, le conteneur ne publie aucun port : il a donc été démarré sans -p.
Est-ce que cela vaut la peine de l’exécuter ?
Cela vaut la peine si vous utilisez réellement plusieurs harnesses et que vous voulez un endpoint et un seul magasin d’identifiants au lieu de trois de chaque. Cela vaut également la peine si vous développez un produit par-dessus et que vous voulez que le harness soit une valeur de configuration plutôt qu’une réécriture. C’est ce que vous apporte UHP, avec la réserve indiquée plus haut concernant la jeunesse du protocole.
Cela ne vaut pas la peine si vous utilisez un seul harness. Installer ce CLI sur le serveur implique moins de composants, et aucun login ne s’interpose entre vous et lui. Cette solution ne convient pas non plus si vous voulez que plusieurs agents coopèrent sur une même tâche plutôt que de placer une seule API devant plusieurs harnesses. Il s’agit d’un autre outil : consultez un harness multi-agent tel qu’Omnigent pour ce modèle. Dans tous les cas, les règles de déploiement restent les mêmes : bind sur loopback, mot de passe modifié, tag épinglé sur 0.3.0 ou une version ultérieure, et utilisateur dédié.
FAQ
Est-il sûr de publier HarnessRouter sur le port 3000 ?
Non. La console crée des harnesses, lit chaque transcription, exécute des agents avec un accès au shell et au système de fichiers, et contient la clé du fournisseur que vous avez configurée. Un port ouvert expose donc toutes ces fonctions. Publiez le service sur loopback avec -p 127.0.0.1:3000:3000, puis accédez-y par un tunnel SSH ou un reverse proxy avec terminaison TLS. Le pare-feu de l’hôte ne suffit pas à lui seul : Docker écrit ses propres règles dans la table du noyau nat. Un port publié répond donc depuis Internet, même lorsque ufw indique qu’il est bloqué. Vérifiez avec sudo ss -ltnp | grep 3000. La commande doit afficher 127.0.0.1:3000.
Quelle version de HarnessRouter a ajouté l’écran de connexion ?
0.3.0. Les versions 0.1.x et 0.2.0 ont été publiées sans aucune authentification. Ces deux tags sont toujours publiés et peuvent toujours être récupérés. Toute personne qui les exécute dépend donc du fait que personne ne trouve le port. Au 19 août 2026, le tag le plus récent est 0.5.5, daté du 18 août 2026. Exécutez docker image ls harnessrouter/harnessrouter pour voir ce que vous utilisez. Comparez ce résultat avec la liste des tags sur Docker Hub, et non avec cette page. Modifiez également le mot de passe par défaut avec une version actuelle.
Pourquoi le conteneur s’arrête-t-il juste après la définition de HR_BACKENDS ?
Toute valeur HR_BACKENDS qui omet hermes fait immédiatement quitter le conteneur avec le statut 1 et sans message d’erreur. Il s’agit d’un problème connu indiqué dans le README du projet. Le symptôme est Exited (1) dans docker ps -a au bout d’une ou deux secondes, sans information utile dans docker logs. Conservez hermes dans la liste, comme dans HR_BACKENDS=claude,codex,hermes, jusqu’à sa correction en amont.
HarnessRouter a-t-il besoin d’un accès à Internet lors du premier démarrage ?
Oui. Les CLI des agents sont récupérées lors du premier démarrage au lieu d’être incluses dans l’image, car chacune possède sa propre licence. Une machine sans route sortante affiche les lignes installing, puis n’atteint jamais ready on :3000. Le téléchargement a lieu une fois par volume. Les démarrages suivants prennent quelques secondes et n’ont besoin d’aucun accès réseau supplémentaire, à l’exception de celui requis par le fournisseur de modèles que vous avez configuré.
J’ai perdu le mot de passe de la console. Comment me reconnecter ?
Il n’existe pas de procédure de réinitialisation par e-mail, car il n’y a ni système de comptes ni serveur de messagerie. Arrêtez le conteneur, supprimez /data/selfhost-auth.json du volume, puis redémarrez-le. Connectez-vous ensuite avec les identifiants par défaut et définissez un nouveau mot de passe depuis la page Profile. Si le conteneur et le volume portent tous deux le nom harnessrouter, exécutez docker stop harnessrouter, puis docker run --rm -v harnessrouter:/data alpine rm -f /data/selfhost-auth.json, puis docker start harnessrouter.