Aller au contenu

Déploiement Backend Docker — postmortem (7 problèmes résolus)

Contexte

Premier déploiement Docker du backend ESM (Node 20 + Express + TypeScript + Prisma + PostgreSQL natif, npm workspaces) sur le VPS Hetzner, le 2026-06-17. ~3h de résolution avant un backend opérationnel (18 modules, 171 routes). Conteneur en --network host pour accéder au PostgreSQL natif du VPS (non conteneurisé) via 127.0.0.1:5432, Caddy en reverse proxy HTTPS sur esm.tekdhi.dev.

Contenu

Les 7 problèmes, dans l'ordre où ils sont apparus

1. npm ci échoue dans Docker — projet en npm workspaces, package-lock.json à la racine, pas dans BackEnd/. Le Dockerfile ne copiait que BackEnd/package*.json. Fix : copier aussi le lock file racine, npm ci --workspace=BackEnd.

2. dist/server.js introuvable au démarragenpm --prefix ./BackEnd run build exécutait tsc depuis /app, pas /app/BackEnd ; sans tsconfig.json dans le répertoire courant, tsc ne compilait rien et sortait en code 0 (silencieux). Fix : ENV PATH avec les binaires hoistés workspaces + cd BackEnd && tsc && tsc-alias.

3. dist/src/server.js au lieu de dist/server.jstsconfig.json incluait à la fois src/**/* et prisma/seeds/**/*, empêchant TypeScript d'inférer rootDir. Fix : "rootDir": "./src" + retirer les seeds du include (pas partie du serveur de prod).

4. Cannot find module '@/shared/...' au runtimetsconfig-paths/register (solution dev) lit le tsconfig avec paths: {"@/*": ["src/*"]} mais les fichiers compilés sont dans dist/, pas src/. Fix : tsc-alias dans le script build, réécrit les alias en chemins relatifs après compilation — tsconfig-paths devient inutile en prod.

5. 0 modules chargés (auto-discovery vide) — le discovery cherchait des *.module.routes.ts (TypeScript, non importables par Node en prod), et dist était dans les patterns d'exclusion. Fix : adapter le discovery selon NODE_ENV (modulesBasePath/filePattern différents dev vs prod).

6. Contrôleur manquant : supportssensibilisation.module.routes.ts déclarait 4 sous-modules (supports/distributions/emissions/educateurs) non câblés dans server.ts — le controller existait dans FormationsModule.getDI() mais n'était pas exposé dans Dependencies. Fix : ajouter getSensibilisationModuleController() + enregistrer les 4 clés.

7. Module auth absent en prod (17/18 modules)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' ratait shared/. Fix : utiliser dist (pas dist/Modules) en prod, cohérent avec le dev.

Résultat final

18/18 modules chargés, 171 routes (10 publiques, 161 privées), Redis et PostgreSQL connectés, JWT actif sur toutes les routes privées, health check opérationnel.

Décision / Conclusion — 7 leçons génériques à réappliquer

  1. Toujours tester le build Docker en local avant de déployer (docker build + docker run + curl /_health) — évite les cycles déploiement→erreur→fix→redéploiement.
  2. Ajouter une vérification explicite dans le Dockerfile : test -f dist/server.js || exit 1 après le build — fait échouer immédiatement plutôt que de découvrir l'erreur au runtime.
  3. Documenter le pattern npm workspaces + Docker (COPY package*.json racine + npm ci --workspace=X) comme règle technique non-évidente.
  4. tsc-alias systématique pour tout projet TypeScript avec alias @/tsconfig-paths/register est dev-only, jamais suffisant en prod.
  5. Toujours tester en mode dev avant déploiement (/_health, /_metrics) — révèle les contrôleurs manquants/modules non câblés qu'un build silencieux ne signale pas.
  6. Ne jamais placer des routes de module dans shared/ — invisible au discovery prod selon la config ; toutes les routes métier dans src/Modules/<Module>/infrastructure/http/routes/.
  7. .env.example toujours renseigné avec toutes les variables et des valeurs fictives documentées — jamais vide.

Liens connexes