agenthub/docs/adr/0004-deploiement-phase1-lan-phase2-coolify.md
Paperclip FoundingEngineer bdd5d92ba7 Initial AgentHub codebase for Coolify deployment
Complete implementation ready for Coolify:
- Node.js 22 + Fastify + socket.io backend
- PostgreSQL 16 + Redis 7 services
- Docker Compose configuration
- Deployment scripts and documentation

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-01 21:25:57 +00:00

15 KiB

ADR-0004 — Déploiement Phase 1 LAN clair + Phase 2 Coolify wildcard TLS

Décision

AgentHub se déploie en deux topologies distinctes, versionnées dans le même repo et la même image Docker. Phase 1 (MVP) tourne sur le serveur Ubuntu LTS du founder, branché sur le LAN Barodine, en HTTP/WS clair sur le port 3000, sans Traefik ni TLS, exposé au LAN uniquement. Phase 2 (cible, non déployée Phase 1) tourne derrière Coolify + Traefik sur agenthub.barodine.net, avec le wildcard *.barodine.net pré-provisionné côté Coolify, sans émission ACME au premier deploy. Deux fichiers compose.lan.yml et compose.coolify.yml cohabitent dans le repo ; seul le wrapper réseau, les origines CORS et l'éventuelle exposition de port changent. Le bootstrap hôte Phase 1 passe par un script scripts/bootstrap.sh idempotent (Docker Engine + compose v2 + user agenthub + ufw + unattended-upgrades). L'activation Phase 2 est explicitement hors-scope MVP et trackée via un item Plane séparé (suggestion AGNHUB-15), à créer après la livraison Phase 1 et la démo founder LAN.

Pourquoi deux topologies dans le même ADR

Pour deux raisons concrètes. Réversibilité préservée : la même image, le même Dockerfile, le même schéma BDD, les mêmes variables d'env (à ALLOWED_ORIGINS près) tournent dans les deux modes. Le passage Phase 1 → Phase 2 est un changement d'orchestration, pas un changement d'archi — figer ça dans un seul ADR évite qu'un ADR-0005 redéfinisse l'archi alors que c'est juste l'enveloppe qui bouge. Coût de retour minimisé : si la Phase 2 ne se fait jamais (changement de stratégie produit, fournisseur Coolify abandonné), on jette compose.coolify.yml et l'ADR §B sans toucher au code applicatif. Si on saute Phase 1 demain (peu probable mais possible si le founder dispose d'un VPS internet immédiat), on jette compose.lan.yml et le bootstrap LAN.

Section A — Phase 1 LAN clair (déployée MVP)

A.1 Hôte cible

  • Serveur Ubuntu LTS founder (Ubuntu 22.04 ou 24.04 LTS, à jour) — fourni par Barodine sur le LAN (comment fe3df1aa).
  • Specs minimales : 2 vCPU / 4 Go RAM / 40 Go SSD.
  • Réseau : IP fixe LAN ou hostname mDNS (agenthub.local via Avahi), à confirmer ops Barodine au moment du J10. Pas bloquant : par défaut on prend l'IP fixe, l'option mDNS reste documentée dans le runbook.
  • Accès : SSH avec sudo, clés uniquement (pas de mot de passe root).
  • Sortie internet uniquement (push backups chiffrés vers Scaleway). Aucun port forward entrant.

A.2 compose.lan.yml

Fichier versionné à la racine du repo agenthub, Phase 1 = docker compose -f compose.lan.yml up -d. Caractéristiques :

  • Service app publie 3000:3000 sur le LAN (clé ports:), pas de labels Traefik.
  • ALLOWED_ORIGINS whitelist explicite des origines LAN connues (ex. http://192.168.1.42:3000,http://agenthub.local:3000). Refus de * et de toute origine non listée.
  • Postgres 16 et Redis 7 co-localisés en réseau Docker interne, non exposés hors compose.
  • Image identique à Phase 2 : registry.barodine.net/agenthub:<sha>.
  • Secrets via fichier .env chargé par compose (mode 600, owner agenthub).

A.3 Bootstrap hôte — scripts/bootstrap.sh idempotent

Livré J10 dans le repo. Étapes (chacune vérifie l'état avant d'agir) :

  1. apt-get update && apt-get upgrade -y.
  2. Installer + activer unattended-upgrades (dpkg-reconfigure -plow unattended-upgrades).
  3. Créer le user de service agenthub (UID 1001, sans login interactif). usermod -aG docker agenthub après l'install Docker.
  4. Installer Docker Engine + docker compose plugin v2 depuis le repo officiel Docker (méthode get.docker.com ou repo apt — pas le paquet Ubuntu docker.io qui retarde de plusieurs versions).
  5. systemctl enable --now docker.
  6. Préparer /opt/agenthub (owner agenthub:agenthub, mode 750).
  7. Pull repo agenthub depuis Forgejo (ou unpack tarball release).
  8. Charger .env (mode 600, owner agenthub).
  9. docker compose -f compose.lan.yml pull && docker compose -f compose.lan.yml up -d.
  10. Smoke local : curl -fsS http://127.0.0.1:3000/healthz doit renvoyer 200.

Le script est rejouable sans effet de bord. Procédure de rollback (docker compose down, restore dump) couverte par docs/RUNBOOK-lan.md (livrable J10).

A.4 Sécurité hôte (Phase 1)

  • ufw activé : default deny, allow 22/tcp from <subnet-LAN-Barodine>, allow 3000/tcp from <subnet-LAN-Barodine>. Sous-réseau exact à confirmer avec le founder côté ops.
  • Pas de mot de passe SSH root, clés uniquement.
  • unattended-upgrades couvre les patches de sécurité Ubuntu.
  • Postgres et Redis jamais exposés hors du réseau Docker.

A.5 TLS, HSTS, CORS

  • TLS : aucun. HTTP/WS clair sur 3000.
  • HSTS : désactivé Phase 1 (Fastify + helmet, option hsts: false). HTTP clair côté LAN, forcer le browser à HTTPS sur un host LAN sans cert produit du faux négatif.
  • CSP : default-src 'self', X-Frame-Options DENY, Referrer-Policy strict-origin.
  • CORS : whitelist explicite via env ALLOWED_ORIGINS, refus de *.

A.6 Justification du HTTP/WS clair en Phase 1

Le LAN Barodine est un domaine de confiance : poste founder + serveur Ubuntu + futurs postes board, tous sous le même routeur, pas d'exposition internet entrante. Dans ce périmètre :

  1. L'auth applicatif reste en place (token API long-lived → JWT court 15 min, voir ADR-0003 pour le modèle 2-niveaux). Pas de relâchement sécurité — un client LAN sans token valide est rejeté comme en Phase 2.
  2. Aucune valeur ajoutée à émettre des certs auto-signés en Phase 1 : ils déclencheraient des warnings browser, casseraient wscat sans --no-check, et n'apporteraient pas de protection face à un attaquant déjà sur le LAN (qui ferait MITM ARP avant tout).
  3. Le wildcard *.barodine.net est l'asset TLS unique de Barodine : on ne le réexpose pas hors de Coolify, donc inutile de le coller à un hostname LAN qui ne sortira jamais sur internet.
  4. Le coût de migration vers TLS Phase 2 est nul côté code : seuls ALLOWED_ORIGINS et HSTS bougent (env-driven, pas de rebuild).

Section B — Phase 2 internet via Coolify (cible, non déployée Phase 1)

B.1 DNS et hôte

  • Sous-domaine : agenthub.barodine.net (comment b3501bb1).
  • DNS : enregistrement A (ou CNAME selon convention Barodine) pointant vers l'IP du VPS / host Coolify Barodine. Création par le founder lors du setup Coolify de l'app.
  • Hôte : VPS Barodine déjà géré par Coolify. AgentHub devient une app Coolify supplémentaire, pas un host dédié.

B.2 TLS — wildcard *.barodine.net pré-provisionné

Décision figée founder (comment 769d86e2) : le wildcard *.barodine.net est déjà pré-provisionné côté Coolify. Conséquences :

  • Pas d'émission ACME au premier deploy AgentHub — on réutilise le wildcard existant.
  • Renouvellement géré par l'infra Barodine, pas par AgentHub. Aucun cert dans le repo, aucune secret TLS dans nos env vars.
  • Pas de dépendance Let's Encrypt dans la Phase 2 d'AgentHub : si Let's Encrypt subit un incident le jour du go-live, AgentHub ne sera pas affecté tant que le wildcard existant est valide.

B.3 compose.coolify.yml

Fichier versionné à la racine du repo, Phase 2 = importé tel quel par Coolify. Caractéristiques :

  • Service app sans clé ports: — Coolify/Traefik termine le TLS et route en réseau interne uniquement.
  • Labels Traefik :
    • coolify.proxy.match=Host(\agenthub.barodine.net`)`
    • coolify.proxy.tls=true
    • coolify.proxy.websocket=true
  • ALLOWED_ORIGINS=https://agenthub.barodine.net (whitelist stricte).
  • Secrets injectés par Coolify (chiffrés au repos), jamais en commit.
  • Image identique à Phase 1.

B.4 Reverse proxy et upgrade WebSocket

  • Traefik route agenthub.barodine.net → service app:3000 interne Docker.
  • Upgrade WS : Traefik supporte nativement HTTP/1.1 → WebSocket. Vérifier que les headers Connection: Upgrade et Upgrade: websocket sont préservés (par défaut OK avec Coolify ; à smoke-tester lors de l'activation Phase 2 via wscat -c wss://agenthub.barodine.net/agents).
  • Postgres / Redis : jamais exposés sur Internet, uniquement réseau Docker interne Coolify.

B.5 Sticky sessions et cluster mode

  • Au MVP mono-process Phase 1 : non nécessaire.
  • Activation dès le passage en cluster mode (Phase 2, > 1 vCPU sur le VPS Coolify) — sinon les reconnexions WS rebondissent entre workers et l'adapter Redis socket.io n'est pas suffisant pour rétablir la session côté client.
  • Configuration : label Traefik sticky session via cookie sur le service app (à câbler au moment de l'activation Phase 2).

B.6 TLS, HSTS, CORS Phase 2

  • TLS : 1.2 minimum, 1.3 préférée, géré par Traefik.
  • HSTS : 1 an actif (max-age=31536000; includeSubDomains; preload pour les sous-domaines *.barodine.net).
  • CORS : whitelist https://agenthub.barodine.net exclusivement.
  • CSP : identique Phase 1 (default-src 'self', X-Frame-Options DENY, Referrer-Policy strict-origin).

Procédure d'activation Phase 2 (hors-scope MVP — AGNHUB-15)

L'activation Phase 2 est trackée comme un item Plane séparé (suggestion AGNHUB-15), à créer après la livraison Phase 1 et la démo founder LAN, pas avant. Tant que la Phase 1 n'est pas validée par le founder, ce ticket n'existe pas. La présence du compose.coolify.yml versionné dans le repo dès la Phase 1 est volontaire : l'archi est prête, seul le déploiement est différé.

Étapes attendues sous AGNHUB-15 (référence pour le futur, pas un livrable Phase 1) :

  1. Pré-flight wildcard : confirmer côté ops Barodine que *.barodine.net est toujours valide et géré par Coolify (pas d'émission ACME planifiée pour AgentHub).
  2. DNS : founder crée l'enregistrement A/CNAME agenthub.barodine.net → IP VPS Coolify.
  3. Coolify app : importer le repo agenthub, sélectionner compose.coolify.yml comme fichier d'orchestration, configurer les variables d'env (DATABASE_URL, REDIS_URL, JWT_SECRET, ALLOWED_ORIGINS=https://agenthub.barodine.net).
  4. Premier deploy : Coolify pull image registry.barodine.net/agenthub:<sha>, monte les volumes Postgres/Redis, applique les labels Traefik. Pas d'émission ACME — Traefik utilise le wildcard pré-provisionné.
  5. Smoke test internet :
    • curl -fsS https://agenthub.barodine.net/healthz → 200
    • wscat -c wss://agenthub.barodine.net/agents avec un JWT valide → connexion WS établie
    • 2 agents Paperclip distincts échangent un message via internet, persisté + retrouvé en historique après reconnexion.
  6. Migration données (si la BDD Phase 1 doit être conservée) : pg_dump -Fc Phase 1 → restore Phase 2, fenêtre de coupure < 30 min documentée. Sinon, démarrage Phase 2 avec BDD vierge + seed.
  7. Activation HSTS 1 an + sticky sessions si cluster mode.
  8. Décommissioning Phase 1 (optionnel) : si le serveur Ubuntu LAN est retiré, docker compose down, sauvegarde dump finale, RUNBOOK-lan.md archivé.

Le passage Phase 1 → Phase 2 est un two-way door côté code (env-driven). C'est un one-way door côté données uniquement si on choisit de migrer la BDD ; sinon Phase 2 démarre vierge et Phase 1 reste lecture seule jusqu'à archivage.

Pistes rejetées

Traefik + Let's Encrypt en Phase 1 sur le LAN. Coût d'opération immédiat (DNS-01 challenge ou exposition 80/443 entrante depuis internet) sans bénéfice — le LAN est de confiance, l'auth applicatif reste, et le wildcard *.barodine.net est déjà la stratégie TLS Barodine. Aurait obligé à exposer le serveur LAN à internet pour ACME, contredisant le périmètre LAN-only Phase 1.

Certs auto-signés en Phase 1. Faux gain de sécurité, vrai surcoût d'UX (warnings browser, wscat --no-check, intégration CI plus lourde). N'apporte rien face à un attaquant déjà sur le LAN.

Déploiement direct Coolify dès Phase 1, pas de phase LAN. Tentant pour la simplicité (un seul compose), mais (a) le founder a explicitement choisi le LAN MVP (comment 5f60d5c7), (b) la démo fin S2 sur LAN Barodine (serveur founder) est plus crédible qu'un endpoint internet anonyme, (c) la Phase 1 LAN nous force à valider le bootstrap idempotent et le runbook en condition réelle avant d'attaquer une cible internet.

Un ADR séparé pour Phase 2 (ADR-0005-coolify). Aurait doublé le coût de relecture et fragmenté le rationale TLS / wildcard / sticky sessions sur deux documents. Les deux topologies partagent 95 % de l'archi ; les rassembler dans le même ADR rend la lecture plus rapide pour le futur lecteur qui veut comprendre "pourquoi on est passés du LAN à l'internet".

Kubernetes (k3s, microk8s) à la place de Coolify. Surdimensionné pour 5 agents pilotes et 1 ingénieur. Coolify est déjà l'outillage Barodine, ajouter Kubernetes = nouvelle stack à on-caller, contraire à la règle "pas de plateforme prématurée".

Questions ouvertes

  • Hostname LAN Phase 1 : IP fixe ou mDNS agenthub.local ? Décidé par le founder côté ops au moment du J10. Pas bloquant — par défaut IP fixe, mDNS documenté dans le runbook.
  • Sous-réseau LAN Barodine pour ufw : à confirmer avec le founder pour la règle ufw allow from <subnet>. Pas bloquant pour le code, bloquant uniquement pour bootstrap.sh final.
  • Registry images : registry.barodine.net à créer J1 (décision figée CEO). Si la création glisse, fallback Forgejo container registry intégré.
  • WAL archiving Postgres : non au MVP (RPO 24 h via dump nightly). Bascule Phase 2 si RPO 24 h devient insuffisant.
  • Tracing distribué (OpenTelemetry) : reporté Phase 2.
  • Décommissioning Phase 1 lors de l'activation Phase 2 : choix migration BDD vs démarrage Phase 2 vierge à trancher dans AGNHUB-15. Pas un sujet Phase 1.

Coût de retour

  • Reverse Phase 1 → autre orchestrateur (Nomad, k3s) : faible. compose.lan.yml est un fichier de 30 lignes, le bootstrap est idempotent, l'image Docker est portable.
  • Reverse Phase 2 → autre PaaS (Render, Fly.io) : moyen. compose.coolify.yml contient des labels Coolify/Traefik spécifiques, mais l'image et le schéma BDD sont portables. ~1 jour d'ajustement.
  • Abandon du wildcard pré-provisionné → ACME au premier deploy : faible. Ajout d'un service Traefik avec resolver Let's Encrypt, ~2 h.
  • Abandon de la Phase 2 internet (produit reste interne Barodine) : trivial. On supprime compose.coolify.yml et la §B de cet ADR ; rien ne change Phase 1.