Skip to main content

Choisir la meilleure image Docker pour Node.js

hero docker secrets

30 septembre 2022

0 minutes de lecture
How to Choose the Best and Secure Node.js Docker Image

Note de la rédaction :

Mis à jour pour inclure l’image Chainguard Distroless pour Node.js.

(31 août 2023) Mise à jour de la version de Node.js recommandée, des exemples et des résultats d’analyse des vulnérabilités pour tenir compte des dernières versions LTS de Node.js.

Choisir une image Docker pour Node.js peut sembler anodin, mais la taille des images et les vulnérabilités potentielles peuvent avoir des conséquences importantes sur votre pipeline CI/CD et votre posture de sécurité. Alors, comment choisir la meilleure image Docker pour Node.js ?

Il est facile de ne pas voir les risques potentiels liés à l’utilisation de FROM node:latest ou simplement de FROM node (qui est un alias du précédent). C’est d’autant plus vrai si vous n’avez pas conscience des risques de sécurité globaux et de l’espace disque considérable qu’ils mobilisent dans un pipeline CI/CD.

Voici un exemple de Dockerfile Node.js généralement présenté comme référence dans les tutoriels et articles de blog sur les images Docker Node.js — mais ce Dockerfile comporte de graves défauts et n’est pas recommandé :

FROM node
WORKDIR /usr/src/app
COPY . /usr/src/app
RUN npm install
CMD "npm" "start"

J’ai déjà présenté un guide détaillé, étape par étape, sur les 10 bonnes pratiques pour conteneuriser des applications web Node.js avec Docker, qui développe et améliore cet exemple afin d’obtenir une image Docker Node.js prête pour la production.

Dans cet article, nous allons utiliser l’exemple simplifié ci-dessus comme contenu d’un Dockerfile afin de trouver une image Docker Node.js idéale.

Les options d’images Docker pour Node.js

Vous avez en réalité plusieurs options pour créer votre image Node.js. Elles vont de l’image Docker officielle Node.js, maintenue par l’équipe principale de Node.js, aux balises d’image Node.js spécifiques disponibles pour cette image de base, en passant par d’autres possibilités : créer votre application Node.js à partir d’une image distroless de Google ou de Chainguard, ou d’une image scratch minimale fournie par l’équipe Docker.

Parmi toutes ces possibilités, quelle image Docker Node.js vous convient le mieux ?

Examinons-les une par une pour découvrir leurs avantages et les risques potentiels.

Note de l’auteur : Dans cet article, je comparerai les images avec une version de Node.js donnée, publiée pour la dernière fois aux alentours d’avril 2024, à savoir Node.js 22.1.0.

L’image node par défaut

Commençons par l’image node maintenue officiellement par l’équipe Docker de Node.js. Elle comprend plusieurs balises d’image de base Docker, qui correspondent à différentes distributions sous-jacentes (Debian, Ubuntu ou Alpine) et à différentes versions du runtime Node.js. Des balises spécifiques permettent également de cibler des architectures de processeur telles que amd64 ou arm64x8 (le nouvel Apple M1).

Les balises d’image node les plus courantes pour la distribution Debian, comme bullseye ou bookworm, reposent elles-mêmes sur buildpack-deps, maintenu par une autre équipe.

Que se passe-t-il lorsque vous créez une image Docker Node.js à partir de cette image node par défaut, avec pour seule dépendance npm fastify ? Pour simplifier, nous utiliserons cet exemple plutôt qu’un déploiement d’application complet.

FROM node
WORKDIR /app
RUN npm install fastify

Dans le même répertoire, créez l’image avec docker build --no-cache -t mynode . — J’ai également téléchargé l’image node:latest pour montrer la différence de taille. Voici le résultat :

$ docker images
REPOSITORY   TAG       IMAGE ID       CREATED          SIZE
mynode       latest    6978fbd7640d   24 seconds ago   1.13GB
node         latest    2fb0552f149e   11 days ago   1.11GB
  • Nous n’avons pas spécifié la version du runtime Node.js : node est donc un alias de node:latest, qui pointe actuellement vers la version 22.1.0 de Node.js.

  • Notre image Docker fait 1,13 Go.

  • L’image de base node:latest représente 1,11 Go de cette taille.

Quel est le nombre de dépendances et de vulnérabilités de sécurité de cette dernière image Node.js ? Nous pouvons le découvrir en analysant le conteneur. Si vous souhaitez suivre les étapes et effectuer les mêmes analyses sur vos builds, vous aurez besoin d’un compte Snyk gratuit et de la CLI Snyk. Vous pouvez l’installer avec la simple commande `npm install -g snyk` ou en suivant les instructions de notre documentation.

Nous allons exécuter snyk container test mynode --file=Dockerfile --exclude-app-vulns, ce qui donne les résultats suivants :

  • Au total, 413 dépendances ont été détectées. Il s’agit de bibliothèques open source repérées à l’aide du gestionnaire de paquets du système d’exploitation, comme curl/libcurl4, git/git-man ou imagemagick/imagemagick-6-common.

  • Au total, 179 problèmes de sécurité ont été détectés dans ces dépendances, notamment des dépassements de tampon, des erreurs d’utilisation après libération, des écritures hors limites et bien d’autres.

  • Certaines images Node.js plus anciennes, comme Node.js 20.5.1, se sont révélées vulnérables à différents problèmes de sécurité, notamment le DNS rebinding, le smuggling de requêtes HTTP et le détournement de configuration.

Avez-vous vraiment besoin de wget, git ou curl dans l’image Node.js de votre application ? Dans l’ensemble, le résultat n’est pas réjouissant : des centaines de dépendances et d’outils dans l’image Docker Node.js, auxquels s’ajoutent des centaines de vulnérabilités, offrent de nombreuses possibilités d’attaque.

Options Node.js sur Docker Hub : node:buster, node:bullseye et node:bookworm

En parcourant les balises disponibles dans le dépôt Node.js sur Docker Hub, vous trouverez plusieurs autres options d’images Node.js, notamment node:buster, node:bullseye et node:bookworm.

Ces trois balises d’image Docker reposent sur des versions de la distribution Debian. La balise buster correspond à Debian 10, dont la période de fin de vie s’est achevée entre août 2022 et 2024 : ce n’est donc pas un très bon choix. La balise bullseye correspond à Debian 11, désignée comme l’ancienne version « stable » actuelle de Debian, et dont la fin de vie est estimée à juin 2026. Enfin, bookworm est la version « stable » actuelle. À la date de rédaction de cet article, sa fin de vie n’a pas encore été déterminée, mais au vu du cycle historique, on peut supposer qu’elle aura lieu en 2028.

Note de l’auteur : Par conséquent, nous vous recommandons vivement de remplacer toutes vos images Docker Node.js, nouvelles et existantes, utilisant la balise node:buster par node:bullseye, node:bookworm, ou une autre option adaptée.

Créons une nouvelle image Docker Node.js basée sur :

FROM node:bookworm

Si vous créez cette image Docker Node.js et la comparez aux résultats ci-dessus, vous obtiendrez exactement la même taille, le même nombre de dépendances et le même nombre de vulnérabilités détectées. En effet, node, node:latest et node:bookworm pointent tous vers la même balise d’image Node.js.

Une balise d’image Node.js plus légère

L’équipe Docker officielle de Node.js maintient également une balise d’image qui cible explicitement les outils nécessaires à un environnement Node.js fonctionnel, et rien de plus.

Ces balises d’image Node.js sont désignées par la variante slim, par exemple node:bookworm-slim, ou par une version spécifique de Node.js, comme node:20-slim.

Créons une image Node.js slim basée sur la version stable actuelle de Debian, bookworm :

FROM node:bookworm-slim

La taille de l’image a déjà considérablement diminué : elle est passée de près d’un gigaoctet à 231 Mo. L’analyse de son contenu révèle aussi une forte baisse de l’empreinte logicielle, avec 89=8 dépendances et seulement 37 vulnérabilités.

L’image node:bookworm-slim constitue déjà un meilleur point de départ en matière de taille de conteneur et de posture de sécurité.

Une image Docker Node.js LTS

Jusqu’ici, nos images Docker Node.js reposaient sur la version actuelle de Node.js, à savoir Node.js 22. Toutefois, d’après le calendrier des versions de Node.js, cette version ne passera officiellement au statut Active LTS qu’en octobre 2024.

Et si nous utilisions toujours les versions à support à long terme (LTS) pour créer nos images Docker Node.js ? Modifions la balise d’image en conséquence et créons une nouvelle image Node.js :

FROM node:lts-bookworm-slim

La version LTS allégée de Node.js (20.13.1) intègre un nombre similaire de dépendances et de vulnérabilités de sécurité, pour une image légèrement plus petite de 219 Mo.

En fin de compte, même si vous avez des exigences spécifiques qui vous amènent à choisir entre les versions de runtime Node.js LTS et Current, aucune d’elles n’a d’incidence majeure sur l’empreinte logicielle de l’image Node.js.

node:alpine est-elle une meilleure image Node.js ?

L’équipe Docker de Node.js maintient une balise d’image node:alpine, ainsi que des variantes correspondant aux versions spécifiques des distributions Alpine Linux et du runtime Node.js.

Le projet Alpine Linux est souvent cité pour la taille extrêmement réduite de ses images, ce qui est avantageux : l’empreinte logicielle est moindre et, par conséquent, la surface d’exposition aux vulnérabilités est réduite.

FROM node:alpine
...

La taille de l’image Docker passe à 167 Mo, soit 64 Mo de moins que les images Node.js slim. Dans la balise d’image Alpine, au moment où j’écris ces lignes, seules 17 dépendances du système d’exploitation et une vulnérabilité de sécurité ont été détectées. Cela peut indiquer que la balise d’image alpine est un bon choix pour réduire à la fois la taille de l’image et le nombre de vulnérabilités.

node:alpine est-elle la meilleure image Docker pour Node.js ?

La variante Alpine pour Node.js peut offrir une taille d’image réduite et un nombre encore plus faible de vulnérabilités. Il est toutefois important de noter que le projet Alpine utilise musl comme implémentation de la bibliothèque standard C, tandis que les balises d’image Debian pour Node.js, comme bullseye ou slim, utilisent l’implémentation glibc. Ces différences peuvent entraîner des problèmes de performances, des bugs fonctionnels ou des plantages de l’application en raison des différences entre les bibliothèques C sous-jacentes. Itamar Turner-Trauring a également évoqué son expérience de problèmes d’exécution inattendus liés aux balises d’image Alpine pour les images Docker Python.

Choisir une balise d’image Node.js alpine, c’est en pratique choisir un runtime Node.js non officiel. L’équipe Docker de Node.js ne prend pas officiellement en charge les images de conteneurs basées sur Alpine. Elle précise donc que les balises d’image basées sur Alpine sont expérimentales et peuvent manquer de cohérence. Elles sont disponibles dans les builds non officiels suivants. Voici un extrait du dépôt de balises d’image Unofficial Builds :

Unofficial-builds tente de fournir des binaires Node.js de base pour certaines plateformes qui ne sont pas prises en charge, ou ne le sont que partiellement, par Node.js. Ce projet ne fournit aucune garantie et ses résultats ne sont pas rigoureusement testés. Les builds disponibles sur nodejs.org répondent à des normes de qualité très élevées en matière de code, de prise en charge des plateformes concernées, ainsi que de délais et de méthodes de mise à disposition. Les builds disponibles via unofficial-builds font l’objet de peu de tests, voire d’aucun ; certaines plateformes ne sont peut-être pas incluses dans l’infrastructure officielle de tests de Node.js. Ces builds sont proposés pour faciliter la tâche de leur communauté d’utilisateurs, mais celle-ci est censée contribuer à leur maintenance.

Voici quelques observations notables sur la compatibilité de la balise d’image Node.js alpine :

  • Yarn est incompatible (problème n° 1716).

  • Si vous avez besoin de node-gyp pour compiler de manière croisée des liaisons natives en C, vous devrez trouver une solution vous-même : Python, une dépendance de ce processus, n’est pas disponible dans l’image Alpine (problème n° 1706).

Images Docker Distroless pour Node.js

Les dernières images à comparer dans notre benchmark sont les images Distroless. Deux options principales existent : les originales images de conteneur distroless de Google et les plus récentes images de conteneur distroless de Chainguard.

Qu’est-ce qu’une image Docker distroless ?

Ces images sont encore plus légères que la balise d’image Node.js slim , car elles ne comprennent que l’application et les dépendances de son runtime. Une image Docker distroless ne comporte donc ni gestionnaire de paquets, ni shell, ni autres outils généralistes, ce qui réduit sa taille et son empreinte en matière de vulnérabilités.

Image Google Distroless

Le projet Google Distroless maintient une image Docker distroless spécifique au runtime Node.js, identifiée par le nom complet gcr.io/distroless/nodejs22-debian12 et disponible dans le registre de conteneurs de Google (la partie gcr.io).

Remarque : Il existe également d’anciennes combinaisons de Debian et de Node.js, mais veillez à choisir une version encore maintenue. Pour consulter les images prises en charge les plus récentes, reportez-vous au tableau du dépôt GitHub de Distroless.

Comme les images de conteneur Distroless ne contiennent aucun logiciel, nous pouvons utiliser un workflow Docker à plusieurs étapes pour installer les dépendances de notre conteneur et les copier dans les images Distroless :

FROM node:22-bookworm-slim AS build
WORKDIR /app
COPY . /app
RUN npm install

FROM gcr.io/distroless/nodejs22-debian12
COPY --from=build /app /usr/src/app
WORKDIR /usr/src/app
CMD ["server.js"]

La création de cette image Docker Distroless produit un fichier de 177 Mo, dont la taille est inférieure à celle des variantes des balises d’image slim et alpine .

Si vous envisagez d’utiliser des images Docker Distroless, voici quelques points importants à prendre en compte :

  • Elles reposent sur les versions stables actuelles de Debian, ce qui signifie qu’elles sont à jour et que leur fin de vie est encore lointaine. C’est un avantage considérable.

  • Comme elles sont basées sur Debian, elles s’appuient sur l’implémentation glibc et risquent moins de vous réserver des surprises en production.

  • Vous constaterez rapidement que l’équipe Distroless ne maintient pas de versions précises du runtime Node.js. Vous devez donc vous fier à la balise généraliste latest, qui sera fréquemment mise à jour, ou installer l’image en utilisant son hachage SHA256 à un moment donné.

Image Distroless de Chainguard

L’autre option pour les images Distroless est Chainguard. Chainguard fournit plusieurs images, notamment pour les anciennes versions de Node et des versions compatibles FIPS. L’offre gratuite pour les développeurs comprend deux images : « latest », qui exécute Node 22.1.0 au moment de la rédaction, et « latest-dev », qui ajoute à l’image latest un gestionnaire de paquets et un shell, utiles pour le développement et le débogage. 

Nous pouvons reprendre un exemple très similaire à celui de Google Distroless présenté précédemment :

FROM cgr.dev/chainguard/node:latest-dev AS build
WORKDIR /app
COPY . /app
USER root
RUN npm install

FROM cgr.dev/chainguard/node:latest
COPY --from=build /app /usr/src/app
WORKDIR /usr/src/app
CMD ["server.js"]

On obtient ainsi une image de 142 Mo, similaire à l’image Google Distroless. Aucune vulnérabilité n’est signalée. 

Il convient de noter que toutes les images Chainguard sont basées sur la distribution Linux Wolfi de Chainguard. Wolfi est compilée avec glibc : il n’y a donc aucun problème de compatibilité avec musl. 

Comparaison des balises d’image Docker Node.js

Le tableau suivant récapitule notre comparaison des différentes balises d’image Docker Node.js, selon les données mises à jour pour la dernière fois le 15 mai 2024 :

Balise d’image

Version du runtime Node.js

Dépendances du système d’exploitation

Vulnérabilités de sécurité du système d’exploitation

Vulnérabilités élevées et critiques

Vulnérabilités moyennes

Vulnérabilités faibles

Vulnérabilités du runtime Node.js

Taille de l’image

Yarn disponible

node:latest

22.1.0

413

179

3

0

176

0

1135 Mo

Oui

node:bookworm

22.1.0

413

179

3

0

176

0

1135 Mo

Oui

node:bookworm-slim

22.1.0

88

37

2

0

35

0

233 Mo

Oui

node:lts-bookworm-slim

20.13.1

88

37

2

0

35

0

219 Mo

Oui

node:alpine

22.1.0

17

1

0

0

1

0

145 Mo

Oui

22.1.0

8

16

0

0

16

0

186 Mo

Non

cgr.dev/chainguard/node:latest

22.1.0

25

0

0

0

0

0

134 Mo

Non

cgr.dev/chainguard/node:latest-dev

22.1.0

66

0

0

0

0

0

651 Mo

Oui

Passons en revue les données et les enseignements tirés de chacune des balises d’image Node.js pour déterminer laquelle est la plus adaptée.

Parité avec l’environnement de développement

Si vous choisissez une balise d’image Node.js pour garantir la cohérence entre le développement et la production — autrement dit, pour reproduire exactement le même environnement —, la bataille est peut-être déjà perdue. Dans la plupart des cas, les trois principaux systèmes d’exploitation utilisent une implémentation différente de la bibliothèque C. Linux s’appuie sur glibc, Alpine sur musl et macOS possède sa propre implémentation BSD libc.

Taille de l’image Docker

La taille compte parfois. Plus précisément, l’objectif n’est pas d’obtenir la plus petite taille possible, mais de réduire au minimum l’empreinte logicielle globale. Dans ce cas, les balises d’image slim ne sont pas beaucoup plus petites que leurs équivalentes alpine : elles atteignent toutes environ 211 Mo par image de conteneur. Certes, l’empreinte logicielle des images slim reste assez importante (89 contre 17 pour alpine) et expose donc une surface de vulnérabilité plus vaste (28 pour slim, contre 0 pour alpine).

Vulnérabilités de sécurité

Les vulnérabilités sont un enjeu important et ont fait l’objet de nombreux articles expliquant pourquoi vous devriez réduire la taille de vos images de conteneur. Toutefois, la nature des problèmes de sécurité est essentielle.

En écartant les images node et node:bullseye en raison de leur empreinte logicielle plus importante et de leurs vulnérabilités de sécurité plus nombreuses, nous pouvons nous concentrer sur un ensemble plus restreint de types d’images. Entre slim, alpine et distroless, le nombre de vulnérabilités de sécurité élevées et critiques reste faible : entre 0 et 2. Il s’agit d’un risque maîtrisable, potentiellement sans incidence sur votre cas d’usage.

Assistance et résilience

Il est essentiel que l’équipe Docker Node.js puisse donner la priorité aux problèmes liés à vos images de conteneur et les résoudre rapidement. Avec toute balise d’image autre que la balise officielle basée sur Debian, vous ne pouvez tout simplement pas cocher cette case.

Avec les balises d’image node ou node:22.1.0-bookworm-slim, que vous choisissiez une image complète du système d’exploitation ou une version plus légère avec moins de dépendances, vous disposez toujours de la dernière version du runtime Node.js. Même s’il s’agit d’une version paire (Node.js 22.1.0), elle ne faisait pas encore partie du cycle de support à long terme au moment de la rédaction. Elle intègre donc de nouvelles versions d’autres composants dépendants, comme la dernière version de npm (dont le nouveau comportement peut être instable et nécessiter un délai de stabilisation).

En résumé ?

L’image Docker Node.js idéale serait une version allégée du système d’exploitation, basée sur un système Debian moderne et incluant une version stable de Node.js bénéficiant d’un support à long terme actif.

Il s’agit de choisir la balise d’image Node.js node:lts-bookworm-slim. Je privilégie les balises d’image déterministes ; je remplacerais donc l’alias lts par le numéro de version réel sous-jacent.

La balise d’image Docker Node.js idéale est node:20.13.1-bookworm-slim.

Si vous faites partie d’une équipe DevOps expérimentée capable de prendre en charge des images de base personnalisées, mon deuxième choix serait la balise d’image distroless de Google, car elle garantit la compatibilité avec glibc pour les versions officielles du runtime Node.js. Ce workflow nécessitera toutefois une certaine maintenance ; je ne le recommande donc que si vous pouvez l’assurer.

La sécurité des conteneurs pour le DevSecOps

Détectez et corrigez gratuitement les vulnérabilités des conteneurs avec Snyk.