Écran Voix R5 (maquetté, jamais construit) implémenté sur décision du référent : open-source et local, pas d'API externe. - apps/ai : faster-whisper (CTranslate2, CPU, MIT) opt-in (AI_TRANSCRIPTION=off|locale|deterministe, défaut off) ; endpoint /internal/transcrire — l'audio ne survit JAMAIS à l'appel (fichier temporaire supprimé quoi qu'il arrive) ; indexer_bilans inclut désormais InterventionReport.note anonymisée (champ existant depuis R2, jamais eu d'écran jusqu'ici) ; 29 pytest. - Contrat (77 opérations) : POST /assistant/transcribe (multipart). - API : proxy multipart vers siop2-ai (WORK_ORDERS.edit — même droit que la saisie du bilan) ; 2 tests e2e (80 tests API au total). - Mobile : expo-audio + expo-file-system, bouton dicter/terminer sur l'écran de clôture, purge locale après transcription, « Joindre la description à l'OT » (corrige un bug latent : enfilerBilan ignorait silencieusement les mises à jour de note). - Docker : siop2-ai embarque le modèle Whisper au build (1,54→2,19 Go), construit et vérifié (transcription réelle en conteneur, non-root). - Vérifié réellement : transcription fidèle (voix de synthèse française) en direct, bout en bout via l'API, dans le conteneur Docker construit, et chaîne corpus complète (note → clôture → réindexation → recherche sémantique). - Base de dev locale réinitialisée avec accord explicite du référent (prisma migrate reset, bloqué par défaut pour un agent IA) après pollution par les tests manuels de la recette terrain précédente. Reste : test tactile sur iPhone physique (bouton dicter) — bloqué par une connexion USB qui ne s'est pas rétablie malgré câble/port/ redémarrage essayés à plusieurs reprises, reporté comme la recette Android. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
10 KiB
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 :
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épostgresouapicollisionne. Tout est préfixé, sans exception. - Seul
siop2-webrejointdokploy-network(réseau externe du reverse-proxy) : l'API n'est jamais exposée directement, elle est servie via le proxy/apide 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) |
AI_TRANSCRIPTION |
— | off (défaut) ou locale (dictée R5 D5, faster-whisper — le modèle est déjà dans l'image, l'activer ne demande pas de rebuild) |
AI_TRANSCRIPTION_MODEL |
— | défaut small — tiny/base/small/medium/large-v3, compromis qualité/vitesse CPU non calibré sur le darija (voir ADR-004 §5) |
Profils types :
- Production client : les 4 dernières variables absentes. Les routes
/auth/demo-accountset/auth/demo-loginn'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) :
-
Onglet General — source GitHub : Provider
GitHub(application GitHub Dokploy autorisée sursiop-spelev/siop2, dépôt privé), Branchmain, Compose Pathinfra/docker-compose.dokploy.yml. -
Onglet Environment : coller les variables du §2. Pour l'instance de démonstration ENSET :
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 -
Deploy (bouton). Dokploy clone le dépôt, construit les images (contexte = racine, Dockerfiles
apps/apietapps/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. -
Onglet Domains : Add Domain → Host
siop2.apps.enset.top, Service Namesiop2-web, Container Port80, HTTPS activé (certificat Let's Encrypt géré par Dokploy/Traefik). -
Vérifications :
https://siop2.apps.enset.top/api/health→{"status":"ok", ...}(3 servicesup) ;- é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.
-
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) :
- Dokploy → service compose → onglet Deployments → copier la Webhook URL.
- 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.) - C'est tout : le prochain push vert sur
maindéclenche le build + redéploiement côté Dokploy. Tant que le secret n'existe pas, le jobdeployse 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-aine rejoint jamaisdokploy-network: il n'a pas de domaine, pas de port publié — seulsiop2-apile contacte, avecAI_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 (ouPOST /assistant/reindex) pour reconstruire le corpus. - Mode génératif : ajouter
AI_GENERATION=api+AI_API_KEYdans l'environnement Dokploy puis redéployer le servicesiop2-aiseul. La clé ne transite jamais par le dépôt ni par les journaux (/healthzn'expose que le mode). - Dictée (D5) : ajouter
AI_TRANSCRIPTION=localedans l'environnement Dokploy puis redéployersiop2-ai— le modèle Whisper est déjà dans l'image (§ci-dessus), pas de rebuild ni de téléchargement au boot. L'audio n'est jamais stocké, ni parsiop2-apini parsiop2-ai: transcrit à la volée, effacé aussitôt (voir ADR-004 §5). Point de vigilance non calibré : la qualité en darija (fréquent sur le terrain, mélangé au français) — à mesurer sur de vrais enregistrements si la dictée déçoit en recette.
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_PERTINENCEpar 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
DROPsans 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=truesansDEMO_MODE_I_KNOW(verrou ADR-002 — c'est voulu),DATABASE_URLerronée, migration échouée. - Sauvegardes : volumes
pg-data(base) etminio-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)
# 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.