mirror of
https://github.com/siop-spelev/siop2.git
synced 2026-08-08 12:41:54 +00:00
Recette (mode extractif, aucune clé) — elle a invalidé le modèle R5.1 : - MiniLM-384 classait la page-réponse DERRIÈRE des passages sans rapport (0,24 vs 0,41 sur la question type du plan) → bascule mesurée vers paraphrase-multilingual-mpnet-base-v2 (768 d, local/CPU), ADR-004 amendé avec le banc comparatif (e5-large écarté : 2,2 Go, scores compressés). - Migration r5_embeddings_mpnet : pgvector 384 → 768, index vidé (re-dérivable par « Réindexer tout »). - Découpage affiné (~350 caractères) : la phrase-réponse ne se noie plus, l'extrait cité est lisible ; seuils par défaut recalés 0,45/0,40/0,55. - Rejouée après bascule : réponse sourcée p. 2 en tête, refus honnête chiffré, suggestions étagées — 16/16 Playwright, 78 API, 23 pytest. - Revue pixel publiée (6 écrans réels vs maquettes, 3 arbitrages). Durcissement : - apps/ai/Dockerfile : uv, modèle ONNX téléchargé AU BUILD (ADR-004 §1), non-root, healthcheck ; répétition locale conteneurisée validée (healthz, reindex via MinIO/pgvector, 401 sans jeton, refus de boot api-sans-clé, réponse sourcée depuis le conteneur). - Compose Dokploy : siop2-ai interne (jamais sur dokploy-network, AI_SERVICE_TOKEN requis, génération opt-in, seuils par env) ; siop2-api branché (AI_SERVICE_URL). - Runbook §5-6 : service IA en production, calibrage des seuils sur le corpus client, réindexation post-déploiement. Le tag release/r5 attend la validation de la revue pixel par le référent. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
168 lines
9.4 KiB
Markdown
168 lines
9.4 KiB
Markdown
# Runbook — déploiement Dokploy
|
|
|
|
> **Rôle de ce document (playbook)** : la procédure REJOUABLE de mise en production
|
|
> sur Dokploy, écrite en R0.13 et répétée à chaque release (principe « déployer tôt »).
|
|
>
|
|
> **Instances** : démonstration/cours → `https://siop2.apps.enset.top` (Dokploy ENSET,
|
|
> projet créé le 15/07/2026) ; production client SPELEV → en attente des accès au
|
|
> serveur du partenaire (même procédure, profil d'environnement « production client »).
|
|
|
|
## 1. Topologie
|
|
|
|
Un projet Dokploy = un service « Compose » pointant sur ce dépôt,
|
|
fichier [`infra/docker-compose.dokploy.yml`](../../infra/docker-compose.dokploy.yml) :
|
|
|
|
```text
|
|
domaine (Traefik/Dokploy) ──▶ siop2-web :80 (nginx, statique)
|
|
│ /api/** (préfixe retiré)
|
|
▼
|
|
siop2-api :3000 (NestJS)
|
|
│── siop2-postgres :5432 (pgvector + PostGIS)
|
|
│── siop2-redis :6379
|
|
│── siop2-minio :9000
|
|
└── siop2-ai :8000 (FastAPI — R5, interne)
|
|
```
|
|
|
|
- **Convention `siop2-`** (leçon v1) : le réseau Dokploy est partagé entre projets ;
|
|
un service nommé `postgres` ou `api` collisionne. Tout est préfixé, sans exception.
|
|
- Seul `siop2-web` rejoint `dokploy-network` (réseau externe du reverse-proxy) :
|
|
l'API n'est **jamais** exposée directement, elle est servie via le proxy `/api` de nginx
|
|
— même topologie que le proxy Vite en dev, donc mêmes chemins partout.
|
|
- Les migrations Prisma s'appliquent **au démarrage du conteneur API**
|
|
(`prisma migrate deploy`, idempotent) : la base est toujours au niveau du code déployé.
|
|
|
|
## 2. Variables d'environnement (à saisir dans Dokploy, jamais dans le dépôt)
|
|
|
|
| Variable | Obligatoire | Rôle |
|
|
| --- | --- | --- |
|
|
| `POSTGRES_PASSWORD` | ✅ | mot de passe PostgreSQL (générer : `openssl rand -hex 24`) |
|
|
| `MINIO_ROOT_PASSWORD` | ✅ | secret MinIO |
|
|
| `JWT_SECRET` | ✅ | signature des jetons (générer : `openssl rand -hex 32`) |
|
|
| `POSTGRES_USER` / `POSTGRES_DB` | — | défaut `siop` / `siop` |
|
|
| `DEMO_MODE` | — | **absent en production client.** `true` uniquement sur l'instance de démonstration |
|
|
| `DEMO_MODE_I_KNOW` | — | double verrou ADR-002 : requis si `DEMO_MODE=true` en production, sinon l'API **refuse de démarrer** |
|
|
| `SEED_ON_START` | — | `true` sur l'instance de démonstration : rôles, matrice et comptes démo au boot (idempotent) |
|
|
| `SEED_DEMO_PASSWORD` | — | mot de passe commun des comptes démo (défaut `Demo!2026`) |
|
|
| `AI_SERVICE_TOKEN` | ✅ (R5) | secret partagé api ↔ service IA (générer : `openssl rand -hex 32`) — le service IA refuse tout appel sans lui |
|
|
| `AI_GENERATION` | — | `off` (défaut, mode extractif — la recette R5 passe ainsi) ou `api` (rédaction LLM, exige `AI_API_KEY`) |
|
|
| `AI_API_KEY` | — | clé API Anthropic — **uniquement** si `AI_GENERATION=api` ; le service refuse de démarrer si elle manque en mode api |
|
|
| `AI_MODEL` | — | défaut `claude-opus-4-8` |
|
|
| `AI_SEUIL_PERTINENCE` / `AI_SEUIL_SUGGESTION` / `AI_SEUIL_CONFIANCE_FORTE` | — | défauts 0,45 / 0,40 / 0,55 — **à calibrer sur le corpus client** (voir §7) |
|
|
|
|
Profils types :
|
|
|
|
- **Production client** : les 4 dernières variables **absentes**. Les routes
|
|
`/auth/demo-accounts` et `/auth/demo-login` n'existent pas (404, testé en CI).
|
|
- **Instance de démonstration** : `DEMO_MODE=true`, `DEMO_MODE_I_KNOW=true`,
|
|
`SEED_ON_START=true`.
|
|
|
|
## 3. Première mise en production — déploiement manuel depuis GitHub (checklist)
|
|
|
|
Dans le service **Compose** du projet Dokploy (déjà créé pour l'instance ENSET) :
|
|
|
|
1. **Onglet General — source GitHub** : Provider `GitHub` (application GitHub
|
|
Dokploy autorisée sur `siop-spelev/siop2`, dépôt privé), Branch `main`,
|
|
**Compose Path** `infra/docker-compose.dokploy.yml`.
|
|
2. **Onglet Environment** : coller les variables du §2. Pour l'instance de
|
|
démonstration ENSET :
|
|
|
|
```env
|
|
POSTGRES_PASSWORD=<openssl rand -hex 24>
|
|
MINIO_ROOT_PASSWORD=<openssl rand -hex 24>
|
|
JWT_SECRET=<openssl rand -hex 32>
|
|
AI_SERVICE_TOKEN=<openssl rand -hex 32>
|
|
DEMO_MODE=true
|
|
DEMO_MODE_I_KNOW=true
|
|
SEED_ON_START=true
|
|
```
|
|
|
|
3. **Deploy** (bouton). Dokploy clone le dépôt, construit les images (contexte =
|
|
racine, Dockerfiles `apps/api` et `apps/web` — ~5-10 min au premier build) puis
|
|
démarre les 5 services ; l'API attend PostgreSQL/Redis/MinIO sains et migre la base.
|
|
Suivi : onglet **Deployments** (logs de build) puis **Logs** par service.
|
|
4. **Onglet Domains** : Add Domain → Host `siop2.apps.enset.top`,
|
|
**Service Name `siop2-web`**, **Container Port `80`**, HTTPS activé
|
|
(certificat Let's Encrypt géré par Dokploy/Traefik).
|
|
5. Vérifications :
|
|
- `https://siop2.apps.enset.top/api/health` → `{"status":"ok", ...}` (3 services `up`) ;
|
|
- écran de connexion : 7 comptes démo, bascule de rôle < 3 s, bi-thème ;
|
|
- production client (quand elle existera) : `/api/auth/demo-accounts` → **404**.
|
|
6. Consigner la mise en production dans `docs/journal/journal.md` (date, commit, domaine).
|
|
|
|
## 4. Releases suivantes — déploiement continu par GitHub Actions
|
|
|
|
Le job `deploy` de `.github/workflows/ci.yml` appelle le **webhook Dokploy**
|
|
à chaque push sur `main`, **uniquement si les 5 jobs (lint, contrat, api, web, e2e)
|
|
sont verts** — c'est la CI qui garde la porte, pas l'inverse. Mise en place (une fois) :
|
|
|
|
1. Dokploy → service compose → onglet **Deployments** → copier la **Webhook URL**.
|
|
2. Dépôt GitHub → Settings → Secrets and variables → Actions →
|
|
**New repository secret** : nom `DOKPLOY_WEBHOOK_URL`, valeur = l'URL copiée.
|
|
(Ou en CLI : `gh secret set DOKPLOY_WEBHOOK_URL`.)
|
|
3. C'est tout : le prochain push vert sur `main` déclenche le build + redéploiement
|
|
côté Dokploy. Tant que le secret n'existe pas, le job `deploy` se termine en
|
|
« skip » explicite sans faire échouer le pipeline.
|
|
|
|
> **Ne pas activer** l'« Auto Deploy » natif de Dokploy (webhook GitHub direct) :
|
|
> il déploierait aussi les commits dont la CI est rouge.
|
|
|
|
Les migrations de la release s'appliquent au démarrage du conteneur API ; en cas
|
|
d'échec de migration, le conteneur s'arrête **sans** servir de trafic (l'ancienne
|
|
version reste visible côté web).
|
|
|
|
## 5. Le service IA en production (R5)
|
|
|
|
- `siop2-ai` **ne rejoint jamais** `dokploy-network` : il n'a pas de domaine, pas de
|
|
port publié — seul `siop2-api` le contacte, avec `AI_SERVICE_TOKEN`. S'il est
|
|
éteint, l'assistant répond « indisponible » (503 propre) et **tout le reste de
|
|
l'application fonctionne**.
|
|
- Le modèle d'embeddings (mpnet multilingue, ~1 Go) est **dans l'image** : premier
|
|
build long (téléchargement au build), démarrages rapides ensuite, aucun accès à
|
|
Hugging Face requis en production.
|
|
- **Après chaque déploiement qui change le modèle ou le découpage** (ex. migration
|
|
`r5_embeddings_mpnet`) : l'index est vide — cliquer **« Réindexer tout »** dans
|
|
Bibliothèque (ou `POST /assistant/reindex`) pour reconstruire le corpus.
|
|
- Mode génératif : ajouter `AI_GENERATION=api` + `AI_API_KEY` dans l'environnement
|
|
Dokploy puis redéployer le service `siop2-ai` seul. La clé ne transite jamais par
|
|
le dépôt ni par les journaux (`/healthz` n'expose que le mode).
|
|
|
|
## 6. Calibrage des seuils sur le corpus client
|
|
|
|
Les seuils par défaut ont été mesurés sur un banc synthétique (journal 17/07/2026).
|
|
Sur le vrai corpus SPELEV (notices réelles), rejouer une dizaine de questions métier
|
|
et quelques questions hors corpus via l'écran Assistant, puis ajuster :
|
|
|
|
- trop de refus → baisser `AI_SEUIL_PERTINENCE` par pas de 0,03 ;
|
|
- réponses « à côté » sur des questions hors corpus → le monter ;
|
|
- suggestions de bilan trop rares/trop bruyantes → jouer sur `AI_SEUIL_SUGGESTION`.
|
|
|
|
Chaque ajustement = variable d'environnement Dokploy + redéploiement de `siop2-ai`
|
|
(pas de rebuild, pas de réindexation).
|
|
|
|
## 7. Incidents & retours arrière
|
|
|
|
- **Rollback applicatif** : Dokploy → Deployments → redéployer le commit précédent.
|
|
Les migrations Prisma étant additives (convention projet : jamais de `DROP` sans
|
|
tâche dédiée validée), l'ancienne API fonctionne sur le schéma plus récent.
|
|
- **API en échec au boot** : `docker logs siop2-api`. Causes classiques :
|
|
`DEMO_MODE=true` sans `DEMO_MODE_I_KNOW` (verrou ADR-002 — c'est voulu),
|
|
`DATABASE_URL` erronée, migration échouée.
|
|
- **Sauvegardes** : volumes `pg-data` (base) et `minio-data` (fichiers).
|
|
Sauvegarde PostgreSQL planifiée côté Dokploy (onglet Backups) — à configurer
|
|
à la première mise en production réelle.
|
|
|
|
## 8. Répétition locale (sans Dokploy)
|
|
|
|
```bash
|
|
# depuis la racine — construit et lance les 5 services comme en production
|
|
docker network create dokploy-network 2>/dev/null || true
|
|
POSTGRES_PASSWORD=repetition MINIO_ROOT_PASSWORD=repetition-minio \
|
|
JWT_SECRET=$(openssl rand -hex 32) DEMO_MODE=true DEMO_MODE_I_KNOW=true SEED_ON_START=true \
|
|
docker compose -f infra/docker-compose.dokploy.yml up -d --build
|
|
# web : http://localhost via un port publié à ajouter ponctuellement, ou :
|
|
docker compose -f infra/docker-compose.dokploy.yml exec siop2-web wget -qO- http://localhost/api/health
|
|
```
|
|
|
|
C'est cette répétition (images construites, migrations au boot, parcours démo via
|
|
nginx) qui a validé R0.13 en local — voir le journal du 15/07/2026.
|