diff --git a/CHANGELOG.md b/CHANGELOG.md index 0728b67..b22ba17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,23 @@ incompatibles. Chaque ligne renvoie à un commit dédié (un artefact = un commi ## [Unreleased] +## [0.33.0] — 2026-06-23 — infra locale déployée (gitea + n8n-chlova) +### Added +- **`infra/gitea/docker-compose.yml`** : serveur git Gitea 1.26.4 sur l'hôte + `local` (réseau `proxy`, `git.pogoo.app`), source du GitOps de CHLOVA. + **Déployé** via Portainer (stack id 6, running). +- **`infra/n8n-chlova/docker-compose.yml`** : n8n dédié à CHLOVA (2.20.8) sur + `local` (`n8n-chlova.pogoo.app`), MCP natif interne `http://n8n-chlova:5678`. + **Déployé** via Portainer (stack id 7, running healthy). +### Changed +- Topologie cible repliée sur **un seul hôte (`local`)** : tout sur le réseau + `proxy`, joignable en interne par nom de conteneur. +- `docker-compose.prod.yml` : `MCP_N8N_URL` → `n8n-chlova`, `PORTAINER_URL` → + interne `http://portainer:9000` (même hôte), sidecar `mcp-portainer` ajouté au + réseau `proxy` (pour joindre le serveur Portainer). +- `docs/deploy.md` réécrit pour la topologie locale (DNS, gitea, push, users + restreints, déploiement GitOps, vérif). + ## [0.32.0] — 2026-06-23 — prêt au déploiement (GitOps Portainer, Phase 1) ### Added - **`infra/docker-compose.prod.yml`** : compose de PRODUCTION pour GitOps diff --git a/docs/deploy.md b/docs/deploy.md index ea4d1af..bf4561a 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -1,148 +1,133 @@ # Déploiement CHLOVA (Phase 1 — lecture seule, GitOps Portainer) -Procédure de mise en production réelle sur le homelab. Cible : -environnement Portainer **`vps-pogoo-002`** (endpoint 11), aux côtés de -`proxy` (Traefik), `n8n`, `jenkins`. +Mise en production réelle sur le homelab. **Tout sur l'hôte `local`** +(environnement Portainer endpoint 3), aux côtés du serveur Portainer et de +Traefik. Pile dédiée à CHLOVA : + +| Stack | État | Rôle | +|---|---|---| +| `gitea` (endpoint 3) | **déployé** | dépôt git source du GitOps (`git.pogoo.app`) | +| `n8n-chlova` (endpoint 3) | **déployé** | n8n dédié à CHLOVA (`n8n-chlova.pogoo.app`) | +| `chlova` (endpoint 3) | à déployer | backend + ollama + sidecars (`chlova.pogoo.app`) | > **Modèle de risque.** Phase 1 = lecture seule : `CHLOVA_PHASE=1`, > `PORTAINER_READ_ONLY=true`, MCP read-only filtré. Aucune écriture branchée. -> Les secrets ne transitent **jamais** par l'agent : ils sont saisis par -> l'opérateur dans les variables de stack Portainer (UI) — voir §4. +> Les secrets de CHLOVA ne transitent **jamais** par l'agent : ils sont saisis +> par l'opérateur dans les variables de stack Portainer (UI) — voir §4. -## Vue d'ensemble - -| Brique | Réseau | Exposé ? | -|---|---|---| -| `backend` (API + SPA) | `chlova-internal` + `proxy` | **Oui** — `chlova.pogoo.app` via Traefik/TLS | -| `ollama` (proxy cloud) | `chlova-internal` + `chlova-egress` | Non | -| `mcp-portainer` (sidecar) | `chlova-internal` | Non | -| `socket-proxy` (Docker RO) | `chlova-internal` | Non | -| n8n (MCP natif) | `proxy` (existant) | déjà exposé en `n8n.pogoo.app` | +Réseau commun : `proxy` (external) — Traefik, Portainer, gitea, n8n-chlova et le +backend CHLOVA y sont tous attachés, donc joignables **en interne** par nom de +conteneur. Resolver TLS : `letsencrypt`. Entrypoint : `websecure`. Compose de prod : [`infra/docker-compose.prod.yml`](../infra/docker-compose.prod.yml). -Build de l'image fait **par Portainer sur le VPS** (GitOps : clone du dépôt + -`docker compose build`). Contexte de build = racine du dépôt. +Build de l'image : **par Portainer sur `local`** (GitOps : clone du dépôt gitea ++ `docker compose build`). Contexte de build = racine du dépôt. --- -## 1. Prérequis — dépôt git accessible au VPS +## 0. DNS (préalable) -GitOps exige un remote que Portainer peut cloner. Choisir un hébergeur privé -(GitHub privé, Gitea du homelab…), puis : +Faire pointer vers l'IP de l'hôte `local` (la même que `traefik.pogoo.app`) : +`git.pogoo.app`, `n8n-chlova.pogoo.app`, `chlova.pogoo.app`. Sans ça, Traefik ne +peut pas émettre les certificats letsencrypt ni router. + +## 1. Gitea — admin + dépôt (UI, une fois) + +`gitea` est déployé, installeur **ouvert** (tu crées le 1er compte = admin). + +1. Ouvrir `https://git.pogoo.app` → compléter l'install (DB = SQLite déjà + réglée) → créer le compte admin. +2. **New repository** : `chlova` (privé). +3. **Settings → Applications → Generate token** (scope `write:repository`) : + sert à pousser le code (§2) et au clone GitOps par Portainer (§4). + +> L'enregistrement public est désactivé (`DISABLE_REGISTRATION=true`) : seul +> l'admin existe. + +## 2. Pousser le dépôt CHLOVA dans gitea + +Le code est sur le poste de build. Ajouter le remote gitea et pousser `main` : ```bash -git remote add origin -git push -u origin main +git remote add gitea https://git.pogoo.app//chlova.git +git push gitea main # auth : + token de l'étape 1.3 ``` -Noter l'URL de clone (HTTPS) + des identifiants en lecture (PAT/deploy key) pour -Portainer. +## 3. Users restreints `chlova` (UI) -## 2. Prérequis — secrets & login fort (générés, jamais commités) +> Le MCP Portainer n'expose pas la gestion users/tokens : étapes UI. Principe : +> CHLOVA n'accède qu'à ses ressources, tokens à portée minimale. -Générer le hash de mot de passe + secrets TOTP/JWT pour l'admin de l'UI : +### 3a. Portainer — user `chlova` + token +1. **Users → Add user** : `chlova`, non-admin. +2. **Environments → local → Access** : rôle le plus bas suffisant. *(CE = RBAC + par environnement, pas par stack ; le vrai verrou Phase 1 reste + `PORTAINER_READ_ONLY=true`.)* +3. Connecté en `chlova` → **My account → Access tokens → Add token** → + `PORTAINER_MCP_AUTH_TOKEN`. + +### 3b. n8n-chlova — MCP token +1. `https://n8n-chlova.pogoo.app` → créer le compte propriétaire. +2. Activer le serveur **MCP natif**, générer le **MCP Access Token** → + `MCP_N8N_AUTH_TOKEN`. Endpoint interne : `http://n8n-chlova:5678/mcp-server/http`. + +## 4. Secrets login UI (générés, jamais commités) ```bash cd orchestrator npm run provision-auth -- '' ``` -La commande imprime `CHLOVA_ADMIN_PASSWORD_HASH`, `CHLOVA_TOTP_SECRET`, -`CHLOVA_JWT_SECRET` et un `otpauth://…` à scanner dans une app TOTP. **Conserver -ces valeurs pour l'étape 4 (UI Portainer).** Ne pas les committer. +→ `CHLOVA_ADMIN_PASSWORD_HASH`, `CHLOVA_TOTP_SECRET`, `CHLOVA_JWT_SECRET` + +`otpauth://…` (scanner dans l'app TOTP). À saisir dans les variables de stack (§5). -## 3. Prérequis — users restreints (`chlova`) +## 5. Déploiement du stack `chlova` -> Le MCP Portainer n'expose pas la gestion d'utilisateurs/tokens : ces étapes se -> font dans les **UI** Portainer et n8n. Principe : CHLOVA n'accède qu'à ses -> ressources, avec des tokens à portée minimale. +`infra/docker-compose.prod.yml` n'a **aucun** `env_file` : toutes les variables +sont des **variables de stack** Portainer (les secrets y sont saisis par +l'opérateur, jamais par l'agent). -### 3a. Portainer — user `chlova` + token - -1. **Users → Add user** : `chlova`, mot de passe fort. **Non-administrateur.** -2. **Environments → vps-pogoo-002 → Access** : donner à `chlova` le rôle le plus - bas suffisant. En CE, la granularité est **par environnement** (pas par - stack) — l'accès se limite donc à ce seul environnement. *(La restriction - par-ressource fine nécessite l'édition Business ; à défaut, le verrou réel - reste `PORTAINER_READ_ONLY=true` côté sidecar.)* -3. Se connecter en tant que `chlova` → **My account → Access tokens → Add token**. - Copier le token → ce sera `PORTAINER_MCP_AUTH_TOKEN`. - -### 3b. n8n — projet + membre `chlova` - -1. **Admin → Users** : inviter/créer un utilisateur `chlova`. -2. **Projects → New project** « CHLOVA » ; ajouter `chlova` comme membre. - Y déplacer les workflows que CHLOVA doit voir (les autres restent invisibles). -3. **Settings → n8n API / MCP** : activer le serveur MCP natif, générer le - **MCP Access Token** scopé → ce sera `MCP_N8N_AUTH_TOKEN`. - Endpoint interne : `http://n8n:5678/mcp-server/http` (backend sur réseau `proxy`). - -## 4. Déploiement du stack (Portainer) - -`infra/docker-compose.prod.yml` n'utilise **aucun** `env_file` : toutes les -variables ci-dessous sont fournies comme **variables d'environnement du stack** -Portainer (les secrets y sont saisis par l'opérateur, jamais par l'agent). - -### Variables de stack à renseigner - -| Variable | Exemple / valeur | Secret ? | +| Variable | Valeur | Secret ? | |---|---|---| | `CHLOVA_DOMAIN` | `chlova.pogoo.app` | non | | `CHLOVA_PHASE` | `1` | non | | `PORTAINER_READ_ONLY` | `true` | non | +| `PORTAINER_URL` | `http://portainer:9000` (défaut, interne) | non | | `OLLAMA_API_KEY` | clé Ollama cloud | **oui** | | `OLLAMA_MODEL` | `qwen3:cloud` | non | -| `MCP_N8N_AUTH_TOKEN` | token MCP n8n (§3b) | **oui** | -| `PORTAINER_URL` | URL interne de l'API Portainer (cf. §4 note) | non | +| `MCP_N8N_AUTH_TOKEN` | token MCP n8n-chlova (§3b) | **oui** | | `PORTAINER_MCP_AUTH_TOKEN` | token user chlova (§3a) | **oui** | | `CHLOVA_ADMIN_USER` | ex. `kantin` | non | -| `CHLOVA_ADMIN_PASSWORD_HASH` | (§2) | **oui** | -| `CHLOVA_TOTP_SECRET` | (§2) | **oui** | -| `CHLOVA_JWT_SECRET` | (§2) | **oui** | - -> **`PORTAINER_URL`** = URL **publique** du serveur Portainer. Topologie réelle : -> le serveur Portainer (`portainer-ce`) tourne sur l'hôte `local`, tandis que -> CHLOVA et son sidecar tournent sur `vps-pogoo-002` (un **autre hôte**). Le -> sidecar ne peut donc pas joindre Portainer en interne : utiliser l'URL publique -> (la même que celle configurée pour le MCP `portainer-pogoo`), p. ex. -> `https://.pogoo.app`. Pas de `:9443` interne ici. - -### Procédure UI (recommandée — l'opérateur saisit les secrets) +| `CHLOVA_ADMIN_PASSWORD_HASH` | (§4) | **oui** | +| `CHLOVA_TOTP_SECRET` | (§4) | **oui** | +| `CHLOVA_JWT_SECRET` | (§4) | **oui** | **Stacks → Add stack → Git repository** : -- Repository URL = remote de l'étape 1 ; Reference = `refs/heads/main` ; - Compose path = `infra/docker-compose.prod.yml`. -- Authentication = identifiants de lecture (§1). +- Repository URL = `http://gitea:3000//chlova.git` (clone interne) ou + `https://git.pogoo.app//chlova.git`. +- Authentication = user gitea + token (§1.3). +- Reference = `refs/heads/main` ; Compose path = `infra/docker-compose.prod.yml`. - Environment variables = tableau ci-dessus. - **Deploy**. Portainer clone, build l'image et lance le stack. -### Procédure assistée (MCP) +## 6. Vérification -CHLOVA peut créer le stack via `StackCreateDockerStandaloneRepository` une fois -le remote prêt — **mais** les valeurs secrètes ne doivent pas transiter par -l'agent. Schéma retenu : l'agent crée le stack avec les variables **non -secrètes** et l'opérateur ajoute/édite les secrètes dans l'UI avant le 1er -déploiement, **ou** l'opérateur fait l'étape UI ci-dessus de bout en bout. - -## 5. Vérification (post-déploiement) - -1. Portainer → le stack `chlova` est *running* ; conteneurs `backend`, - `ollama`, `mcp-portainer`, `socket-proxy` *up*. +1. Stack `chlova` *running* ; conteneurs `backend`, `ollama`, `mcp-portainer`, + `socket-proxy` *up*. 2. Logs `backend` : `API/UI activée (auth configurée)` + `healthcheck interne - prêt`. Pas d'erreur de config fail-closed. -3. `https://chlova.pogoo.app` répond (certificat `letsencrypt` émis) → page de - login (mot de passe + TOTP). -4. Connexion → l'onglet Chat répond ; les outils MCP read-only (n8n/Portainer) - sont listés dans l'état (header UI). + prêt`. Pas d'erreur fail-closed. +3. `https://chlova.pogoo.app` → page login (mot de passe + TOTP). +4. Connexion → Chat répond ; outils MCP read-only (n8n + Portainer) listés. -## 6. Rollback +## 7. Rollback -GitOps : revert du commit compose + redeploy, ou **Stacks → chlova → Stop/Remove**. -Le volume `chlova-data` (SQLite) persiste ; le supprimer pour repartir de zéro. +GitOps : revert du commit compose + redeploy, ou **Stacks → chlova → +Stop/Remove**. Volume `chlova-data` (SQLite) persiste ; le supprimer pour +repartir de zéro. -## 7. Passage en Phase 2 (plus tard, hors de cette procédure) +## 8. Passage Phase 2 (plus tard) Écriture sous gatekeeper + need-review : `CHLOVA_PHASE=2` et -`PORTAINER_READ_ONLY=false` (le sidecar autorise alors les mutations, toujours -filtrées par le gatekeeper + paliers de risque). À ne faire qu'après validation -du cerveau en lecture seule. Voir [`docs/need-review.md`](./need-review.md). +`PORTAINER_READ_ONLY=false` (mutations alors filtrées par gatekeeper + paliers +de risque). Uniquement après validation du cerveau en lecture seule. Voir +[`docs/need-review.md`](./need-review.md). diff --git a/infra/docker-compose.prod.yml b/infra/docker-compose.prod.yml index 5f8b188..4703ad6 100644 --- a/infra/docker-compose.prod.yml +++ b/infra/docker-compose.prod.yml @@ -64,14 +64,15 @@ services: image: portainer/portainer-mcp:0.6.0 restart: unless-stopped environment: - PORTAINER_URL: ${PORTAINER_URL:?PORTAINER_URL requis} # URL PUBLIQUE Portainer (serveur sur autre hôte — cf. deploy.md) + PORTAINER_URL: ${PORTAINER_URL:-http://portainer:9000} # interne : Portainer sur le même hôte (local), réseau proxy PORTAINER_MCP_AUTH_TOKEN: ${PORTAINER_MCP_AUTH_TOKEN:?requis} # token user chlova restreint PORTAINER_READ_ONLY: ${PORTAINER_READ_ONLY:-true} # P1 : NE PAS passer à false DOCKER_HOST: tcp://socket-proxy:2375 depends_on: - socket-proxy networks: - - chlova-internal + - chlova-internal # joignable par le backend + - proxy # joint le serveur Portainer (http://portainer:9000) # AUCUN port publié. # ── Backend CHLOVA : SEULE surface, cerveau (boucle agent) ────────────── @@ -92,7 +93,7 @@ services: OLLAMA_API_KEY: ${OLLAMA_API_KEY:?requis} OLLAMA_MODEL: ${OLLAMA_MODEL:-qwen3:cloud} # — MCP n8n (natif, interne via réseau proxy) — - MCP_N8N_URL: ${MCP_N8N_URL:-http://n8n:5678/mcp-server/http} + MCP_N8N_URL: ${MCP_N8N_URL:-http://n8n-chlova:5678/mcp-server/http} MCP_N8N_AUTH_TOKEN: ${MCP_N8N_AUTH_TOKEN:?requis} # — MCP Portainer (sidecar) — MCP_PORTAINER_URL: ${MCP_PORTAINER_URL:-http://mcp-portainer:3000} diff --git a/infra/gitea/docker-compose.yml b/infra/gitea/docker-compose.yml new file mode 100644 index 0000000..95afb1d --- /dev/null +++ b/infra/gitea/docker-compose.yml @@ -0,0 +1,43 @@ +# Gitea — serveur git auto-hébergé du homelab, source du GitOps de CHLOVA. +# +# Déployé sur l'hôte `local` (Portainer endpoint 3), réseau `proxy`, exposé en +# https://git.pogoo.app via Traefik (resolver letsencrypt). DB = SQLite (volume). +# Installeur laissé ouvert au 1er run : l'opérateur crée le compte admin, puis +# l'enregistrement public reste désactivé. Voir docs/deploy.md §1. +# +# Image épinglée (jamais :latest). Aucune donnée sensible ici (admin créé en UI). + +services: + gitea: + image: gitea/gitea:1.26.4 + container_name: gitea + restart: unless-stopped + environment: + - USER_UID=1000 + - USER_GID=1000 + - TZ=Europe/Paris + - GITEA__server__DOMAIN=git.pogoo.app + - GITEA__server__ROOT_URL=https://git.pogoo.app/ + - GITEA__server__DISABLE_SSH=true # push via HTTPS uniquement + - GITEA__database__DB_TYPE=sqlite3 + - GITEA__service__DISABLE_REGISTRATION=true + volumes: + - gitea-data:/data + networks: + - proxy + labels: + - "traefik.enable=true" + - "traefik.docker.network=proxy" + - "traefik.http.routers.gitea.rule=Host(`git.pogoo.app`)" + - "traefik.http.routers.gitea.entrypoints=websecure" + - "traefik.http.routers.gitea.tls=true" + - "traefik.http.routers.gitea.tls.certresolver=letsencrypt" + - "traefik.http.services.gitea.loadbalancer.server.port=3000" + +networks: + proxy: + name: proxy + external: true + +volumes: + gitea-data: diff --git a/infra/n8n-chlova/docker-compose.yml b/infra/n8n-chlova/docker-compose.yml new file mode 100644 index 0000000..ca63b2e --- /dev/null +++ b/infra/n8n-chlova/docker-compose.yml @@ -0,0 +1,67 @@ +# n8n dédié à CHLOVA — instance séparée du n8n de prospection (qui vit sur le VPS). +# +# Déployé sur l'hôte `local` (Portainer endpoint 3), réseau `proxy`, exposé en +# https://n8n-chlova.pogoo.app via Traefik (resolver letsencrypt). Joignable en +# interne par le backend CHLOVA : http://n8n-chlova:5678 (MCP natif sur +# /mcp-server/http). Voir docs/deploy.md §3b. +# +# Image épinglée (même tag que l'instance existante, connue bonne). La clé de +# chiffrement N8N_ENCRYPTION_KEY n'est PAS fournie ici : n8n l'auto-génère et la +# persiste dans le volume au 1er démarrage (instance mono-nœud). + +services: + n8n: + image: docker.n8n.io/n8nio/n8n:2.20.8 + container_name: n8n-chlova + restart: unless-stopped + volumes: + - n8n_chlova_data:/home/node/.n8n + environment: + - N8N_HOST=n8n-chlova.pogoo.app + - N8N_PORT=5678 + - N8N_PROTOCOL=https + - WEBHOOK_URL=https://n8n-chlova.pogoo.app/ + - N8N_EDITOR_BASE_URL=https://n8n-chlova.pogoo.app/ + - N8N_SECURE_COOKIE=true + - N8N_TRUST_PROXY=true + - N8N_PROXY_HOPS=1 + - NODE_ENV=production + - N8N_DIAGNOSTICS_ENABLED=false + - N8N_VERSION_NOTIFICATIONS_ENABLED=false + - N8N_HIRING_BANNER_ENABLED=false + - N8N_PERSONALIZATION_ENABLED=false + - N8N_PUBLIC_API_DISABLED=true + - N8N_PUBLIC_API_SWAGGER_UI_DISABLED=true + - N8N_BLOCK_ENV_ACCESS_IN_NODE=true + - GENERIC_TIMEZONE=Europe/Paris + - TZ=Europe/Paris + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://localhost:5678/healthz || exit 1"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 30s + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + labels: + - "traefik.enable=true" + - "traefik.docker.network=proxy" + - "traefik.http.routers.n8n-chlova.rule=Host(`n8n-chlova.pogoo.app`)" + - "traefik.http.routers.n8n-chlova.entrypoints=websecure" + - "traefik.http.routers.n8n-chlova.tls=true" + - "traefik.http.routers.n8n-chlova.tls.certresolver=letsencrypt" + - "traefik.http.services.n8n-chlova.loadbalancer.server.port=5678" + networks: + - proxy + +networks: + proxy: + name: proxy + external: true + +volumes: + n8n_chlova_data: + driver: local