Files
siop2/docs/06-production/runbook-dokploy.md
pr-daaif 59ed6f3952 feat(r5): dictée — audio local (faster-whisper) → note → corpus (ADR-004 §5)
É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>
2026-07-22 11:55:47 +01:00

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é 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)
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 smalltiny/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-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 :

    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-accounts404.
  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).
  • Dictée (D5) : ajouter AI_TRANSCRIPTION=locale dans l'environnement Dokploy puis redéployer siop2-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 par siop2-api ni par siop2-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_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)

# 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.