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émarrage — npm --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.js — tsconfig.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 runtime — tsconfig-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 : supports — sensibilisation.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¶
- 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. - Ajouter une vérification explicite dans le Dockerfile :
test -f dist/server.js || exit 1après le build — fait échouer immédiatement plutôt que de découvrir l'erreur au runtime. - Documenter le pattern npm workspaces + Docker (
COPY package*.jsonracine +npm ci --workspace=X) comme règle technique non-évidente. tsc-aliassystématique pour tout projet TypeScript avec alias@/—tsconfig-paths/registerest dev-only, jamais suffisant en prod.- 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. - Ne jamais placer des routes de module dans
shared/— invisible au discovery prod selon la config ; toutes les routes métier danssrc/Modules/<Module>/infrastructure/http/routes/. .env.exampletoujours renseigné avec toutes les variables et des valeurs fictives documentées — jamais vide.
Liens connexes¶
- jobPipeline/debug/lecons-cablage-auth-package — même piège de routes auth mal placées (shared/ vs Modules/), rencontré indépendamment sur un autre projet
- ProjetESM/index — index du projet