Docker : des images Node.js légères, rapides à builder et sécurisées
Une image Node.js de 1,2 Go qui met six minutes à se construire en CI et embarque un compilateur, le cache npm et un shell root n’a rien d’exceptionnel. Elle coûte pourtant cher : déploiements lents, surface d’attaque élargie et alertes de vulnérabilités sur des paquets que l’application n’utilise jamais. Quelques réglages bien choisis suffisent à diviser la taille par cinq et le temps de build par trois, sans changer une ligne de code applicatif.
Le multi-stage build : séparer ce qui construit de ce qui tourne
Le principe est simple : un premier stage build installe toutes les dépendances, compile le TypeScript et produit le dossier dist/ ; un stage final repart d’une image vierge et ne copie que ce qui est nécessaire à l’exécution. Dans ce dernier stage, installez uniquement les dépendances de production avec npm ci --omit=dev : TypeScript, ESLint, Vitest et les types disparaissent de l’image livrée. Choisissez une base adaptée : node:22-slim (Debian) est un bon compromis compatible avec la plupart des modules natifs, node:22-alpine est plus petite mais repose sur musl, ce qui peut poser problème avec certaines dépendances binaires comme sharp ou des drivers de base de données. Épinglez toujours une version précise, voire un digest, plutôt que le tag latest qui rend les builds non reproductibles.
Exploiter le cache des couches pour des builds en secondes
Docker réutilise une couche tant que ses entrées n’ont pas changé. Copiez donc d’abord package.json et package-lock.json, lancez l’installation, puis seulement ensuite le reste du code source : modifier un composant ne relancera plus le téléchargement de 800 paquets. Avec BuildKit, l’instruction RUN --mount=type=cache,target=/root/.npm npm ci conserve le cache npm entre deux builds sans jamais l’inclure dans l’image. Un fichier .dockerignore rigoureux (node_modules, .git, dist, fichiers .env, rapports de couverture) réduit le contexte envoyé au daemon et évite qu’un changement anodin invalide le cache. En CI, activez l’export du cache vers le registre ou le cache GitHub Actions (cache-from / cache-to) pour profiter de ces gains même sur des runners éphémères.
Durcir le conteneur : non-root, signaux et secrets
Les images officielles fournissent un utilisateur node : terminez votre Dockerfile par USER node et copiez les fichiers avec --chown=node:node, afin qu’une faille applicative ne donne pas d’emblée les droits root dans le conteneur. Lancez le processus directement avec CMD ["node", "dist/main.js"] plutôt que via npm start : npm ne relaie pas correctement SIGTERM, ce qui empêche l’arrêt gracieux et provoque des coupures de requêtes lors des déploiements. Ajoutez l’option --init ou tini si l’application lance des processus enfants. Enfin, ne passez jamais un token npm privé via ARG ou ENV, qui restent lisibles dans l’historique de l’image : utilisez RUN --mount=type=secret, qui expose le secret le temps d’une seule instruction.
Mesurer, scanner et aller plus loin avec distroless
Ce qui n’est pas mesuré finit par dériver : docker image ls et l’outil dive montrent couche par couche ce qui pèse dans l’image, et révèlent souvent un dossier de tests ou un cache oublié. Intégrez un scanner comme Trivy ou Docker Scout dans la pipeline, avec un seuil bloquant sur les vulnérabilités critiques corrigeables, et reconstruisez régulièrement l’image pour récupérer les correctifs de la base même sans changement de code. Pour les services exposés, les images distroless (par exemple gcr.io/distroless/nodejs22-debian12) suppriment shell et gestionnaire de paquets : moins de 150 Mo, quasiment aucune CVE système, au prix d’un débogage qui passe par des conteneurs éphémères ou docker debug. Ajoutez un HEALTHCHECK ou une sonde de readiness côté orchestrateur pour que les nouvelles versions ne reçoivent du trafic qu’une fois réellement prêtes.
Un argument concret pour les freelances
Pour un développeur indépendant, livrer un Dockerfile propre est un marqueur de professionnalisme immédiatement visible par l’équipe technique du client. Documentez les choix dans le README (base retenue, utilisateur, variables attendues, commande de healthcheck) et comparez la taille et le temps de build avant et après : ces chiffres parlent aux décideurs, qui y voient des coûts d’infrastructure et des délais de mise en production réduits. C’est aussi une prestation d’audit facile à proposer en début de mission, avec un retour sur investissement mesurable dès la première semaine.
Multi-stage, ordre des couches, cache BuildKit, utilisateur non-root et scan systématique : ces cinq habitudes transforment une image Node.js lourde et fragile en artefact léger, reproductible et sûr. Elles se mettent en place en une demi-journée et profitent à chaque déploiement qui suit.
