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
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)
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
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 |