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>
15 KiB
ADR-0004 — Déploiement Phase 1 LAN clair + Phase 2 Coolify wildcard TLS
- Statut : Accepté
- Date : 2026-04-30
- Auteur : FoundingEngineer
- Relecture : CEO
- Source plan : BARAAA-14 §5.1 + §6.2 + §6.3 + §6.4
- Issue ADR : BARAAA-18
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.localvia 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
apppublie3000:3000sur le LAN (cléports:), pas de labels Traefik. ALLOWED_ORIGINSwhitelist 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
.envchargé par compose (mode 600, owneragenthub).
A.3 Bootstrap hôte — scripts/bootstrap.sh idempotent
Livré J10 dans le repo. Étapes (chacune vérifie l'état avant d'agir) :
apt-get update && apt-get upgrade -y.- Installer + activer
unattended-upgrades(dpkg-reconfigure -plow unattended-upgrades). - Créer le user de service
agenthub(UID 1001, sans login interactif).usermod -aG docker agenthubaprès l'install Docker. - Installer Docker Engine + docker compose plugin v2 depuis le repo officiel Docker (méthode
get.docker.comou repo apt — pas le paquet Ubuntudocker.ioqui retarde de plusieurs versions). systemctl enable --now docker.- Préparer
/opt/agenthub(owneragenthub:agenthub, mode 750). - Pull repo
agenthubdepuis Forgejo (ou unpack tarball release). - Charger
.env(mode 600, owneragenthub). docker compose -f compose.lan.yml pull && docker compose -f compose.lan.yml up -d.- Smoke local :
curl -fsS http://127.0.0.1:3000/healthzdoit 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)
ufwactivé :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-upgradescouvre 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-Policystrict-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 :
- 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.
- Aucune valeur ajoutée à émettre des certs auto-signés en Phase 1 : ils déclencheraient des warnings browser, casseraient
wscatsans--no-check, et n'apporteraient pas de protection face à un attaquant déjà sur le LAN (qui ferait MITM ARP avant tout). - Le wildcard
*.barodine.netest 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. - Le coût de migration vers TLS Phase 2 est nul côté code : seuls
ALLOWED_ORIGINSet 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
appsans 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=truecoolify.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→ serviceapp:3000interne Docker. - Upgrade WS : Traefik supporte nativement HTTP/1.1 → WebSocket. Vérifier que les headers
Connection: UpgradeetUpgrade: websocketsont préservés (par défaut OK avec Coolify ; à smoke-tester lors de l'activation Phase 2 viawscat -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; preloadpour les sous-domaines*.barodine.net). - CORS : whitelist
https://agenthub.barodine.netexclusivement. - CSP : identique Phase 1 (
default-src 'self', X-Frame-Options DENY, Referrer-Policystrict-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) :
- Pré-flight wildcard : confirmer côté ops Barodine que
*.barodine.netest toujours valide et géré par Coolify (pas d'émission ACME planifiée pour AgentHub). - DNS : founder crée l'enregistrement A/CNAME
agenthub.barodine.net→ IP VPS Coolify. - Coolify app : importer le repo
agenthub, sélectionnercompose.coolify.ymlcomme fichier d'orchestration, configurer les variables d'env (DATABASE_URL, REDIS_URL, JWT_SECRET,ALLOWED_ORIGINS=https://agenthub.barodine.net). - 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é. - Smoke test internet :
curl -fsS https://agenthub.barodine.net/healthz→ 200wscat -c wss://agenthub.barodine.net/agentsavec un JWT valide → connexion WS établie- 2 agents Paperclip distincts échangent un message via internet, persisté + retrouvé en historique après reconnexion.
- Migration données (si la BDD Phase 1 doit être conservée) :
pg_dump -FcPhase 1 → restore Phase 2, fenêtre de coupure < 30 min documentée. Sinon, démarrage Phase 2 avec BDD vierge + seed. - Activation HSTS 1 an + sticky sessions si cluster mode.
- Décommissioning Phase 1 (optionnel) : si le serveur Ubuntu LAN est retiré,
docker compose down, sauvegarde dump finale,RUNBOOK-lan.mdarchivé.
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ègleufw allow from <subnet>. Pas bloquant pour le code, bloquant uniquement pourbootstrap.shfinal. - 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.ymlest 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.ymlcontient 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.ymlet la §B de cet ADR ; rien ne change Phase 1.