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>
172 lines
15 KiB
Markdown
172 lines
15 KiB
Markdown
# 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](/BARAAA/issues/BARAAA-14#document-plan)
|
|
- Issue ADR : [BARAAA-18](/BARAAA/issues/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](/BARAAA/issues/BARAAA-14#comment-fe3df1aa-7ff8-47a8-b674-3f1057adc762)).
|
|
- **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](./0003-auth-tokens.md) 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](/BARAAA/issues/BARAAA-14#comment-b3501bb1-72dc-4ad9-908a-22bffe1f86f2)).
|
|
- **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](/BARAAA/issues/BARAAA-14#comment-769d86e2-612c-4f60-b559-93077c982184)) : 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](/BARAAA/issues/BARAAA-14#comment-5f60d5c7-a64a-4926-a81b-bfb520213bf7)), (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.
|