Aller au contenu

Postmortem — Déploiement Backend ESM Caritas Bénin en Docker

Date : 17 juin 2026
Durée de résolution : ~3h (de l'erreur initiale au backend opérationnel en prod)
Résultat final : ✅ 18 modules, 171 routes, Redis + PostgreSQL opérationnels


1. Contexte

Le backend Node.js/TypeScript (architecture DDD stricte, 5 couches) devait être déployé via Docker sur le VPS Hetzner. Le projet utilise npm workspaces (racine + BackEnd + FrontEnd). Le script deploy.sh buildait une image Docker depuis BackEnd/Dockerfile.


2. Chaîne des problèmes rencontrés et solutions

Problème 1 — npm ci échoue dans Docker

Erreur : npm ci can only install with an existing package-lock.json**Cause :** Le projet utilise npm workspaces. Lepackage-lock.jsonest à la racine, pas dansBackEnd/. Le Dockerfile ne copiait queBackEnd/package.json`.
Solution :*

COPY package*.json ./           # lock file racine
COPY BackEnd/package*.json ./BackEnd/
RUN npm ci --workspace=BackEnd


Problème 2 — dist/server.js introuvable au démarrage

Erreur : Cannot find module '/app/BackEnd/dist/server.js'
Cause : npm --prefix ./BackEnd run build exécutait tsc depuis /app (pas /app/BackEnd). Sans tsconfig.json dans le répertoire courant, tsc compilait rien et sortait avec code 0.
Solution :

ENV PATH="/app/node_modules/.bin:$PATH"   # binaires hoistés par workspaces
RUN cd BackEnd && tsc && tsc-alias


Problème 3 — dist/src/server.js au lieu de dist/server.js

Erreur : Le fichier était créé dans dist/src/server.js et non dist/server.js.
Cause : tsconfig.json incluait à la fois src/**/* et prisma/seeds/**/*. Avec deux répertoires source, TypeScript ne pouvait pas inférer rootDir = ./src et utilisait . (racine BackEnd), préservant le chemin complet dans l'output.
Solution : Ajouter "rootDir": "./src" et retirer prisma/seeds/**/* du include (les seeds ne font pas partie du serveur de prod).


Problème 4 — Cannot find module '@/shared/...' au runtime

Erreur : Les alias @/ TypeScript n'étaient pas résolus dans le JS compilé.
Cause : TypeScript ne résout pas les path aliases à la compilation — il les laisse tels quels dans le JS. tsconfig-paths/register (solution dev) lisait le tsconfig avec paths: { "@/*": ["src/*"] } mais les fichiers compilés sont dans dist/, pas src/.
Solution : Ajouter tsc-alias qui réécrit les @/ en chemins relatifs corrects après compilation.

"build": "tsc && tsc-alias"
"start": "node dist/server.js"   // tsconfig-paths plus nécessaire


Problème 5 — 0 modules chargés (auto-discovery vide)

Erreur : 📦 Modules: 0 — 🔗 Routes: 0
Cause : L'auto-discovery cherchait *.module.routes.ts dans src/ — des fichiers TypeScript qu'on ne peut pas importer avec Node.js en prod. De plus 'dist' était dans les excludePatterns.
Solution : Adapter le discovery selon NODE_ENV :

const isProd = process.env.NODE_ENV === 'production';
modulesBasePath: isProd ? 'dist' : 'src',
filePattern: isProd ? '*.module.routes.js' : '*.module.routes.ts',
excludePatterns: isProd ? excludeBase : [...excludeBase, 'dist'],


Problème 6 — Contrôleur manquant : supports

Erreur : Validation échouée: Contrôleur manquant: supports
Cause : Le fichier sensibilisation.module.routes.ts (dans le module Formations) déclarait 4 sous-modules (supports, distributions, emissions, educateurs) non câblés dans server.ts. SensibilisationModuleController existait dans FormationsModule.getDI() mais n'était pas exposé dans Dependencies.
Solution : - Ajouter Dependencies.getSensibilisationModuleController() via FormationsModule - Enregistrer le controller avec les 4 clés dans configurerRoutesModulaires()


Problème 7 — Module auth absent en prod (17 modules au lieu de 18)

Erreur : auth absent des métriques, /api/auth/connexion retourne 404
Cause : auth.module.routes.ts est dans src/shared/infrastructure/http/routes/, pas dans src/Modules/. En dev, modulesBasePath: 'src' scannait tout src/. En prod, modulesBasePath: 'dist/Modules' ne scannait que les modules métier et ratait shared/.
Solution : Utiliser dist (pas dist/Modules) en prod, pour rester cohérent avec le comportement dev (src).


3. Résultat final

Composant État
Docker build ✅ Réussi en une étape
Modules chargés ✅ 18/18
Routes enregistrées ✅ 171 (10 publiques, 161 privées)
Redis ✅ Connecté
PostgreSQL ✅ Connecté (vérifié via tentative connexion)
JWT middleware ✅ Actif sur toutes les routes privées
Health check /_health ✅ Opérationnel

4. Recommandations — Leçons apprises

R1 — Tester le build Docker localement avant de déployer

docker build -t esm-backend-test -f BackEnd/Dockerfile .
docker run --rm -e NODE_ENV=production -e PORT=4200 ... esm-backend-test
curl http://localhost:4200/_health
Évite les cycles déploiement → erreur → fix → déploiement.

R2 — Ajouter une étape de vérification dans le Dockerfile

RUN cd BackEnd && tsc && tsc-alias
RUN test -f /app/BackEnd/dist/server.js || (echo "ERREUR: dist/server.js absent" && exit 1)
Fait échouer le build immédiatement si la compilation n'a pas produit le fichier attendu, plutôt que de découvrir l'erreur au runtime.

R3 — Documenter les particularités npm workspaces dans CLAUDE.md

Le pattern COPY package*.json racine + npm ci --workspace=X est non-évident. Le noter comme règle technique non-négociable au même titre que prisma db push.

R4 — Toujours utiliser tsc-alias dans les projets TypeScript avec alias @/

tsconfig-paths/register est une solution dev uniquement. En prod, les alias doivent être résolus à la compilation. Ajouter systématiquement tsc-alias au script build.

R5 — Tester en mode dev avant tout déploiement

npm run start:dev
curl http://localhost:4100/_health  # vérifier modules et routes
curl http://localhost:4100/_metrics # vérifier chaque module
Le dev mode détecte des problèmes (contrôleurs manquants, modules non câblés) que le build silencieux ne remonte pas.

R6 — Éviter de stocker les routes auth dans shared/

La route auth.module.routes.ts dans src/shared/ était invisible au discovery prod. Toutes les routes métier devraient être dans src/Modules/<NomModule>/infrastructure/http/routes/. Si une convention différente est voulue, la documenter explicitement et adapter le modulesBasePath en conséquence.

R7 — Standardiser le .env.example

Le .env.example était vide. Il doit contenir toutes les variables avec des valeurs fictives documentées. Voir /home/kanmaber/projets/ProjetESM/BackEnd/.env.example comme référence.


5. État des fichiers modifiés

Fichier Modification
BackEnd/Dockerfile npm workspaces + ENV PATH + tsc-alias
BackEnd/tsconfig.json rootDir + suppression prisma/seeds
BackEnd/package.json build: tsc && tsc-alias / start: node dist/server.js
BackEnd/.env.example Toutes les variables documentées
BackEnd/src/shared/infrastructure/routing/ConfigurationModulaire.ts Discovery prod/dev adaptatif
BackEnd/src/shared/infrastructure/routing/AutoDiscoveryModular.ts Extension dynamique .ts/.js
BackEnd/src/shared/infrastructure/config/dependencies.ts getSensibilisationModuleController()
BackEnd/src/server.ts Import + câblage SensibilisationModuleController (4 clés)
package-lock.json Ajout tsc-alias