SSD Nodes Learn Hosting plans →
Guides Matt ConnorPar Matt Connor · Mis à jour le 2026-09-08

Auto-héberger HarnessRouter avec une seule API agent

Déployez Codex, Claude Code et Hermes derrière une API auto-hébergée : Docker exact, écoute loopback, identifiants par défaut à changer et accès TLS.

Ce que HarnessRouter supprime

Vous auto-hébergez HarnessRouter Community Edition pour placer une API devant plusieurs agent harness 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 client qui a demandé le travail. Codex, Claude Code et Hermes remplissent chacun ce rôle. Chacun possède sa propre installation, son propre format d’identifiants et sa propre conception d’une session. HarnessRouter les exécute tous dans un même conteneur et fournit un endpoint HTTP unique, une authentification unique et un stockage centralisé des secrets.

C’est toute l’idée, et il faut en exposer clairement le coût. Vous ajoutez un conteneur, une authentification, un volume et une procédure de mise à niveau à votre serveur pour transformer plusieurs composants en un seul. Si vous n’exécutez aujourd’hui qu’un seul harness, cette configuration est moins pertinente 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 qu’est réellement le Unified Harness Protocol

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 August 2026 est datée du 2026-08-11, et le site la présente comme un draft standard, « suffisamment stable pour servir de base, et versionnée pour pouvoir évoluer sans risque ».

Lisez attentivement l’expression « open standard ». La même entreprise rédige la spécification, fournit l’implémentation de référence et maintient la suite de tests de conformité en 52 points qui détermine qui est conforme. C’est courant 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 multi-éditeur. Considérez-le comme un protocole émergent : utile, en évolution, et que votre propre code doit pouvoir cesser d’utiliser sans nécessiter de réécriture.

Ce qu’il vous faut avant de commencer

Docker et environ 4 GB d’espace disque libre. Vous avez également besoin d’une clé API d’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 par les espaces de travail dans lesquels ils écrivent. Aucun modèle ni clé d’essai n’est inclus dans l’image. Les tâches échouent donc tant que vous n’avez pas connecté de fournisseur. HarnessRouter est 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.

Self-héberger HarnessRouter avec un seul docker run

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

Surveillez ensuite le démarrage du conteneur. Le premier démarrage est lent, et les journaux en indiquent la raison.

docker logs -f harnessrouter

Pendant le démarrage, vous verrez des lignes similaires à celles-ci :

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. Chaque démarrage suivant prend donc quelques secondes et n’affiche aucune ligne d’installation.

Ce téléchargement entraîne deux conséquences importantes 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 d’egress, ou sans route vers l’extérieur, reste bloqué à cette étape et n’affiche jamais ready on :3000. L’échec survient au premier démarrage, et non à docker pull, ce qui est un endroit particulièrement déroutant pour le découvrir. Deuxièmement, vous installez des logiciels tiers soumis aux conditions de tiers. Claude Code est distribué selon les conditions d’Anthropic, et Hermes selon les conditions indiquées par son projet en 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 de données 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 toutes les transcriptions. Effectuez la sauvegarde lorsque le conteneur est arrêté. La copie d’une base SQLite pendant son écriture produit un fichier qui peut ne pas s’ouvrir. La même discipline — arrêter puis copier — s’applique à tous les conteneurs avec état sur le 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 harnessrouter

La variante Compose et la ligne à modifier

Le dépôt contient 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 verrouillage de la version est important, car seize tags de version ont été publiés entre le 9 et le 18 août 2026. Un runtime d’agent qui change sans avertissement est difficile à déboguer. Copiez ensuite le fichier d’environnement, restreignez ses permissions et démarrez le service.

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

.env contient votre clé de fournisseur en clair. Le mode 600 est donc le minimum. Si la sous-commande docker compose ne vous est pas familière, la fiche de référence des commandes Docker Compose présente les commandes courantes.

Pourquoi le port est publié sur 127.0.0.1 et non sur 0.0.0.0

-p 3000:3000 publie le port sur toutes les interfaces de l’hôte. -p 127.0.0.1:3000:3000 le publie uniquement sur l’interface loopback. Le seul accès possible vient donc du VPS lui-même. Le conteneur écoute toujours sur le port 3000 en interne. La partie gauche est donc celle que vous devez modifier. Vérifiez le résultat :

docker port harnessrouter
sudo ss -ltnp | grep 3000

L’affichage de ss par 127.0.0.1:3000 est correct. 0.0.0.0:3000 signifie que la console est accessible depuis Internet. Dans ce cas, le risque est plus élevé que pour la plupart des applications auto-hébergées, car la console crée des harnesses, lit toutes les transcriptions, exécute des agents et fournit à ces agents un shell ainsi qu’un véritable système de fichiers dans leur espace de travail. Elle contient également la clé du fournisseur que vous avez configurée. Toute personne qui accède à une console non protégée peut lire votre travail, exécuter des commandes et utiliser votre clé à vos frais.

Un pare-feu sur l’hôte ne vous protège pas contre cela. Docker publie les ports en ajoutant ses propres règles dans la table nat du noyau. Ces règles sont évaluées avant la chaîne gérée par ufw. Un port publié reste donc accessible même lorsque sudo ufw status l’indique comme refusé. Effectuez le test depuis une autre machine, et non depuis le VPS, sinon vous ne testerez rien. C’est la même leçon que pour exécuter dsh sans interface sur le port 3080 : liez le service à l’interface loopback, puis déterminez délibérément comment vous souhaitez y accéder.

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 temporaires 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 scripté. 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/harnessrouter

Il n’existe aucune fonctionnalité 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 et redémarrez. Connectez-vous ensuite de nouveau avec les valeurs par défaut.

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

HR_AUTH_DISABLED=1 supprime complètement la protection par connexion. Le README limite son utilisation à « une machine à laquelle personne d’autre ne peut accéder ». Un VPS doté d’une adresse IP publique n’est pas une telle machine. Laissez donc la protection activée, sauf si vous exécutez ce service sur un ordinateur portable.

Vérifiez votre version : les anciennes n’ont aucun contrôle d’accès

C’est la partie à 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 était déjà dans la console. 0.3.0 est la première version publiée avec une connexion. Ces anciennes versions sont toujours publiées et peuvent toujours être téléchargées. Un tag ancien épinglé, ou un fichier compose copié d’un collègue, peut donc encore exposer aujourd’hui une console sans contrôle d’accès sur un port public.

Au 19 août 2026, le tag publié le plus récent est 0.5.5, daté du 18 août 2026, et latest pointe vers celui-ci. Vérifiez ce que vous utilisez, puis comparez-le avec la liste des tags sur Docker Hub :

docker image ls harnessrouter/harnessrouter

Toute 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 le changement du 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. Entourez-la donc de 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 pour chaque 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. C’est ici que vous devez configurer 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 indépendant. Il est requis uniquement 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 mérite d’être signalé : toute valeur qui omet hermes fait quitter immédiatement le conteneur avec le statut 1, sans message d’erreur. Vous voyez Exited (1) dans docker ps -a une seconde après le démarrage, tandis que docker logs n’affiche rien d’utile. Laissez hermes dans la liste jusqu’à la correction du problème 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.

Appelez l’API sans la console

La console est facultative. La même API sert aux deux modes et utilise 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 provider 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 harness_id de codex par claude envoie la même requête à un autre harness. Ce changement est précisément la raison d’être de ce logiciel. La connexion personnalisée présentée plus haut permet de configurer un harness avec un endpoint compatible OpenAI que vous hébergez déjà, comme dans un harness DeepSeek auto-hébergé sur un VPS.

Accédez-y depuis votre ordinateur portable sans publier de port

Deux méthodes sont possibles. 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-vps

Laissez cette commande s’exécuter, puis ouvrez http://localhost:3000 dans votre navigateur. Si SSH affiche bind: Address already in use, le port 3000 est déjà utilisé par un processus sur votre ordinateur portable. 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 que l’on oublie souvent. Agent génère des tokens de réponse pendant plusieurs minutes. Un proxy qui met la réponse en buffer conserve ces tokens jusqu’à la fin du tour. La console semble donc figé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, pointez le nom DNS vers le proxy et gardez le conteneur sur l’interface loopback. Comparaison de Nginx, Caddy et Traefik comme reverse proxy explique lequel convient à votre serveur.

Exécutez-le avec son propre utilisateur, pas en tant que root

Le daemon Docker s’exécute en tant que root, et l’appartenance au groupe docker revient à disposer des privilèges root, car un membre peut démarrer un conteneur qui monte le système de fichiers de l’hôte. Ainsi, « ajouter l’équipe au groupe docker » donne un accès root sur le serveur qui conserve votre clé de fournisseur.

La version simple consiste à créer un compte de service qui possède le fichier Compose et .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/harnessrouter

La version plus stricte est Docker rootless, dans laquelle le daemon lui-même s’exécute avec cet utilisateur non privilégié. Elle 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. uidmap est disponible dans l’archive Ubuntu, mais pas docker-ce-rootless-extras : ce paquet est fourni par le dépôt apt de Docker, à l’adresse download.docker.com, que l’installation du moteur Docker ajoute. Si vous n’avez pas installé le moteur depuis ce dépôt, grep -rl download.docker.com /etc/apt/sources.list.d/ n’affiche rien et l’installation ci-dessous ne trouvera pas le paquet.

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 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 sous Security Options. Le mode rootless ne peut pas utiliser les ports inférieurs à 1024 sans configuration supplémentaire. Cela ne pose pas de problème 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 échoue 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 indiqué 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. La machine ne peut pas accéder au réseau pour télécharger les CLI de l’agent, 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é. L’image n’intègre aucun modèle et aucun forfait gratuit. Une nouvelle instance peut donc vous authentifier, mais ne rien exécuter.

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 ordinateur portable 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 sur 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. Ce n’est pas non plus la bonne approche si vous voulez que plusieurs agents collaborent à 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 cas d’usage. Dans tous les cas, les règles de déploiement restent les mêmes. Liaison sur loopback, mot de passe modifié, tag épinglé à 0.3.0 ou supérieur, 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 provider que vous avez configuré. Un port ouvert expose donc tous ces éléments. Publiez le service sur loopback avec -p 127.0.0.1:3000:3000 et utilisez-le via un tunnel SSH ou un reverse proxy avec terminaison TLS. Un pare-feu sur l’hôte ne suffit pas à lui seul : Docker écrit ses propres règles dans la table nat du kernel. Un port publié répond donc depuis Internet même lorsque ufw l’indique comme refusé. Vérifiez avec sudo ss -ltnp | grep 3000, qui 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 compte donc sur le fait que personne ne trouvera 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, même avec une version actuelle.

Pourquoi le conteneur s’arrête-t-il juste après la définition de HR_BACKENDS ?

Toute valeur de HR_BACKENDS qui omet hermes fait quitter immédiatement 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 rien d’utile dans docker logs. Conservez hermes dans la liste, comme dans HR_BACKENDS=claude,codex,hermes, jusqu’à sa correction par le projet upstream.

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 intégrées à 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 seule fois par volume. Les démarrages suivants prennent quelques secondes et n’ont besoin d’aucun réseau en dehors de celui du model provider que vous avez configuré.

J’ai perdu le mot de passe de la console. Comment récupérer l’accès ?

Il n’existe pas d’e-mail de réinitialisation, 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, redémarrez-le, puis connectez-vous 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.