Skip to main content

Sélectionner la meilleure image Docker Node.js

wordpress-sync/hero-docker-secrets

30 septembre 2022

0 minutes de lecture

Note du rédacteur :

21 octobre 2022 : cet article a été mis à jour afin de mieux expliquer la comparaison entre des images Node.js Alpine Linux et d’autres images de conteneurs Node.js pour une utilisation en production.

Si le choix d’une image Docker Node.js peut sembler anodin, il faut savoir que la taille des images et les vulnérabilités potentielles peuvent avoir des effets dramatiques sur votre pipeline CI/CD et votre état de sécurité. Alors comment choisir la meilleure image Docker Node.js ?

On peut facilement passer à côté des risques potentiels liés à l’utilisation de FROM node:latest ou simplement de FROM node (qui est un alias du premier). C’est d’autant plus vrai si vous n’avez pas conscience des risques de sécurité globaux et de la taille des fichiers qu’ils introduisent dans un pipeline CI/CD.

Voici un exemple de Dockerfile Node.js généralement donné en référence dans les tutoriels et les articles de blog sur les images Docker Node.js. Attention,ce Dockerfile comporte d’importantes failles et n’est pas recommandé :

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

J’ai précédemment décrit et fourni un guide étape par étape sur les 10 meilleures pratiques pour conteneuriser les applications web Node.js avec Docker, qui s’appuie sur cet exemple et l’améliore afin d’obtenir une image Docker Node.js prête pour la mise en production.

Pour ce post, nous allons utiliser l’exemple inventé ci-dessus comme contenu d’un Dockerfile afin de trouver une image Docker Node.js idéale.

Vos options pour une image Docker Node.js

Vous disposez de plusieurs options pour construire votre image Node.js. Vous avez par exemple le choix entre l’image Docker Node.js officielle gérée par l’équipe principale de Node.js, les balises d’image Node.js spécifiques que vous pouvez choisir dans cette image de base Docker particulière, et même d’autres options telles que la construction de votre application Node.js sur le projet Distroless de Google, ou une image de base brute fournie par l’équipe Docker.

Parmi toutes ces options, quelle image Docker Node.js est idéale pour vous ?

Examinons-les une par une pour en savoir plus sur leurs avantages et risques potentiels.

Note de l’auteur : dans cet article, je vais comparer une version de Node.js qui a été publiée pour la dernière fois aux alentours de juin 2022 et correspond à Node.js 18.2.0.

L’image node par défaut

Commençons par l’image node gérée. Elle est officiellement gérée par l’équipe Docker Node.js et contient plusieurs balises d’image de base Docker, qui correspondent à différentes distributions sous-jacentes (Debian, Ubuntu ou Alpine), ainsi qu’à différentes versions d’exécution de Node.js lui-même. Il existe également des balises de version spécifiques pour cibler les architectures CPU telles qu’amd64 ou arm64x8 (le nouveau M1 d’Apple).

Les balises d’image node les plus courantes pour la distribution Debian, telles que bullseye ou buster, sont elles-mêmes basées sur buildpack-deps, qui sont gérées par une autre équipe.

Que se passe-t-il lorsque vous construisez votre image Docker Node.js basée sur cette image node par défaut, avec seulement la dépendance npm fastify ?

FROM node
WORKDIR /app
RUN npm install fastify

Construisez l’image avec docker build --no-cache -f Dockerfile1 -t dockerfile1 et vous obtenez ce qui suit :

  • Nous n’avons pas spécifié la version d’exécution de Node.js, node est donc un alias de node:latest, qui renvoie actuellement à la version 18.2.0 de Node.js.

  • La taille de l’image Docker Node.js est de 952 Mo.

Quelle est l’empreinte des dépendances et des failles de sécurité pour cette dernière image Node.js ? Nous pouvons exécuter un scan de conteneur alimenté par Snyk avec docker scan dockerfile1, qui révèle ce qui suit :

  • Au total, 409 dépendances : il s’agit de toute bibliothèque open source qui a été détectée à l’aide du gestionnaire de paquets du système d’exploitation, comme curl/libcurl4, git/git-man ou imagemagick/imagemagick-6-common.

  • Au total, 289 problèmes de sécurité, tels que des dépassements de tampon (Buffer Overflows), des erreurs d’utilisation après libération (Use After Free), des écritures hors limites (Out-of-bounds Write), etc., ont été détectés dans ces dépendances.

  • La version d’exécution Node.js 18.2.0 est vulnérable à 7 problèmes de sécurité tels que DNS Rebinding, HTTP Request Smuggling et Configuration Hijacking.

Avez-vous vraiment besoin que wget, git ou curl soient disponibles dans l’image Node.js pour votre application ? Dans l’ensemble, le tableau n’est pas très reluisant : la présence de centaines de dépendances et d’outils dans l’image Docker Node.js, avec autant de vulnérabilités comptabilisées, et l’environnement d’exécution de Node.js présentant 7 types de vulnérabilités de sécurité différents, tout cela laisse beaucoup de place aux attaques potentielles.

Options de Hub Docker Node.js : node:buster ou node:bullseye

Si vous parcourez les balises disponibles sur le référentiel de Hub Docker Node.js, vous trouverez deux options de balises d’images Node.js alternatives : node:buster et node:bullseye.

Ces deux balises d’image Docker sont basées sur les versions de la distribution Debian. La balise d’image buster correspond à Debian 10, dont la date de fin de vie se situe entre août 2022 et 2024, ce qui n’en fait pas un bon choix. La balise d’image bullseye correspond à Debian 11, qui est considérée comme la version stable actuelle de Debian et dont la date de fin de vie est estimée à juin 2026.

Note de l’auteur : pour cette raison, nous vous encourageons vivement à déplacer toutes les images Docker Node.js nouvelles et existantes des balises d’image node:buster vers node:bullseye ou d’autres alternatives appropriées.

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

FROM node:bookworm

Si vous construisez cette balise d’image Docker Node.js et que vous la comparez aux résultats ci-dessus, vous obtiendrez exactement la même taille, le même nombre de dépendances et les mêmes vulnérabilités détectées. La raison en est que node, node:latest et node:bullseye renvoient tous vers la même balise d’image Node.js en cours de construction.

Balise d’image Node.js pour des images plus fines

L’équipe officielle Docker Node.js gère également une balise d’image qui cible explicitement l’outillage nécessaire à un environnement Node.js fonctionnel, et rien d’autre.

Ces balises d’image Node.js sont désignées par une variante de la balise d’image slim telle que node:bullseye-slim ou par une version spécifique de Node.js, telle que node:14.19.2-slim.

Construisons une image Node.js slim basée sur la version stable actuelle de Debian, bullseye :

FROM node:bookworm-slim

La taille de l’image a déjà considérablement diminué, passant de près d’un gigaoctet d’image de conteneur à 246 Mo. L’analyse de son contenu montre également une grande diminution de l’empreinte logicielle globale, avec 97 dépendances et seulement 56 vulnérabilités.

node:bullseye-slim est déjà un meilleur point de départ en termes de taille d’image de conteneur et de niveau de sécurité.

Une image Docker Node.js LTS

Jusqu’à présent, nos images Docker Node.js étaient basées sur la version actuelle de Node.js, qui est Node.js 18. Mais selon le calendrier des versions de Node.js, cette version n’aura pas de statut officiel Active LTS avant octobre 2022.

Et si nous nous reposions toujours sur les versions LTS (long-term support) dans les images Docker Node.js que nous construisons ? Mettons à jour la balise d’image Docker en conséquence et construisons une nouvelle image Node.js :

FROM node:lts-bookworm-slim

La version LTS de Node.js la plus fine (16.15.0) apporte un nombre similaire de dépendances et de vulnérabilités de sécurité sur l’image, pour une taille d’image légèrement plus petite (188 Mo).

Ainsi, il s’avère que, même si vous avez des exigences spécifiques pour choisir entre les versions LTS et Current de l’environnement d’exécution Node.js, aucune d’entre elles n’a d’impact significatif sur l’empreinte logicielle de l’image Node.js.

node:alpine est-il un meilleur choix pour une image Node.js ?

L’équipe Docker Node.js gère une balise d’image node:alpine et des variantes de celle-ci pour faire correspondre des versions spécifiques des distributions Linux Alpine avec celles de l’environnement d’exécution Node.js.

Le projet Alpine Linux est souvent cité en exemple pour la taille incroyablement réduite de ses images, ce qui est précieux car cela signifie une empreinte logicielle plus limitée, et par conséquent, une plus petite surface de vulnérabilité. La commande suivante demande au Dockerfile de construire un nœud, ce qui augmentera la taille de l’image non compressée :

FROM node:alpine
...

Cela aboutira à une image Docker de 178 Mo, ce qui est relativement identique aux images Node.js Slim de 188 Mo, à cette différence près toutefois que dans la balise d’image Alpine, seules 16 dépendances de système d’exploitation et 2 vulnérabilités de sécurité ont été détectées au total. Cela peut indiquer que la balise d’image alpine constitue un bon choix pour une taille d’image et un nombre de vulnérabilités faibles.

node:alpine est-il le meilleur choix pour une image Docker Node.js ?

La variante de l’image Alpine pour Node.js peut offrir une image de petite taille et un nombre de vulnérabilités encore plus faible. Il est toutefois important de reconnaître que le projet Alpine utilise musl comme implémentation de la bibliothèque standard C, alors que les images Node.js de Debian telles que bullseye ou slim reposent sur l’implémentation glibc. Ces différences peuvent être à l’origine de problèmes de performances, de bogues fonctionnels ou de plantages d’applications potentiels dus aux différences entre les bibliothèques C sous-jacentes. Itamar Turner-Trauring a également écrit sur son expérience relative à des problèmes d’exécution inattendus liés aux balises d’image Alpine pour les images Docker Python.

Choisir une balise d’image Alpine signifie que vous choisissez effectivement un environnement d’exécution Node.js non officiel. L’équipe Docker Node.js ne prend pas officiellement en charge les constructions d’images de conteneurs basées sur Alpine. En tant que telle, elle précise que les balises d’image basées sur Alpine sont expérimentales, peuvent ne pas être cohérentes, et les rend disponibles à partir du site unofficial builds (Builds non officiels) suivant. Pour citer le référentiel de balises d’image du projet Unofficial Builds :

Ce site propose des binaires Node.js de base pour certaines plateformes qui ne sont pas prises en charge ou seulement partiellement prises en charge par Node.js. Ce projet ne fournit aucune garantie et ses résultats n’ont pas fait l’objet de tests rigoureux. Les builds mis à disposition sur nodejs.org répondent à des normes de qualité très élevées en termes de qualité du code, de support sur les plateformes concernées et de délais et méthodes de livraison. Les builds mis à disposition par unofficial-builds ont fait l’objet de peu de tests, voire aucun ; les plateformes peuvent ne pas être incluses dans l’infrastructure de test officielle de Node.js. Ces builds sont mis à disposition de la communauté des utilisateurs à des fins de commodité, mais ces derniers sont censés contribuer à leur maintenance.

Voici quelques observations notables concernant la compatibilité des balises d’image alpine de Node.js :

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

  • Si vous avez besoin de node-gyp pour la compilation croisée des liaisons C natives, alors Python (qui est une dépendance de ce processus) n’est pas disponible dans l’image Alpine et vous devrez vous en occuper vous-même (problème n° 1706).

Si vous choisissez d’utiliser une image Docker Node.js basée sur Alpine, gardez à l’esprit que les outils de sécurité Docker (tels que Trivy ou Snyk) ne peuvent actuellement pas détecter les vulnérabilités liées à l’exécution dans les images de base Alpine. Bien que cela puisse changer dans le futur, vous n’avez pas actuellement la possibilité de détecter des vulnérabilités de sécurité pour la balise d’image de base alpine Node.js 18.2.0 où l’environnement d’exécution Node.js 18.2.0 lui-même est réellement vulnérable. Cette remarque concerne l’outil de sécurité lui-même, non l’image de base Alpine. Il est toutefois important d’en tenir compte.

Images Docker Distroless pour Node.js

Dernier élément de comparaison pour notre benchmark : les images de conteneurs Distroless de Google.

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

Ces images sont encore plus fines (Slim) que l’image Node.js Slim, car elles ciblent uniquement l’application et ses dépendances d’exécution. Ainsi, une image Docker Distroless ne comporte pas de gestionnaire de paquets de conteneurs, d’interpréteur de commandes ou d’autres dépendances d’outils d’usage général, ce qui leur confère une petite taille et une empreinte de vulnérabilité réduite.

Heureusement pour nous, le projet Distroless maintient une image Docker Distroless spécifique au temps d’exécution pour Node.js, identifiée comme gcr.io/distroless/nodejs-debian11 par son espace de nom complet et disponible dans le registre de conteneurs de Google (c’est la partie gcr.io).

Les images de conteneur Distroless ne comportant pas de logiciel, nous pouvons utiliser un flux de travail Docker en plusieurs étapes afin d’installer les dépendances pour notre conteneur et les copier sur 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 construction de cette image Docker Distroless aboutit à un fichier de 112 Mo, ce qui représente une réduction significative de la taille du fichier par rapport aux variantes de balises d’image Slim et Alpine.

Si vous envisagez d’utiliser des images Docker Distroless, voici quelques considérations importantes à prendre en compte :

  • Elles sont basées sur les versions stables actuelles de Debian. Elles sont donc à jour avec une date d’expiration de fin de vie très éloignée, ce qui est une bonne chose.

  • Étant basées sur Debian, elles reposent sur l’implémentation de la glibc et sont moins susceptibles de vous surprendre avec des problèmes en production.

  • Vous vous rendrez compte rapidement que l’équipe Distroless ne maintient pas les versions d’exécution de Node.js. Cela signifie que vous devez vous fier à la balise générale nodejs:16 qui sera fréquemment mise à jour, ou effectuer une installation basée sur le hachage SHA256 de l’image à un moment donné.

Comparaison des balises d’image Docker Node.js

Le tableau suivant récapitule notre comparatif entre les différentes balises d’images Docker Node.js :

Balise d’image

Version d’exécution Node.js

Dépendances du SO

Vulnérabilités de sécurité du SO

Vulnérabilités élevées et critiques

Vulnérabilités moyennes

Vulnérabilités faibles

Vulnérabilités d’exécution Node.js

Taille d’image

Disponibilité Yarn

node

18.2.0

409

289

54

18

217

7

952 Mo

Oui

node:bullseye

18.2.0

409

289

54

18

217

7

952 Mo

Oui

node:bullseye-slim

18.2.0

97

56

4

8

44

7

246 Mo

Oui

node:lts-bullseye-slim

16.15.0

97

55

4

7

44

6

188 Mo

Oui

node:alpine

18.2.0

16

2

2

0

0

0

178 Mo

Oui

gcr.io/distroless/nodejs:16

16.17.0

9

11

0

0

11

0

112 Mo

Non

Passons en revue les données et les connaissances que nous avons acquises grâce à chacune des différentes balises d’image Node.js et décidons laquelle est la meilleure.

Développement-parité

Si vous souhaitez opter pour une parité exacte entre environnement de développement et environnement de production, c’est sans doute une bataille perdue d’avance. Dans la plupart des cas, les trois principaux systèmes d’exploitation utilisent une implémentation de bibliothèque C différente. Linux s’appuie sur glibc, Alpine sur musl, et macOS a sa propre implémentation BSD libc.

Taille d’image Docker

Parfois, la taille est importante. Pour être précis, l’objectif n’est pas tant d’avoir la plus petite taille qu’une moindre empreinte logicielle. Dans ce cas, les balises d’image Slim ne sont pas très différentes en taille par rapport à leurs homologues Alpine ; toutes deux font une moyenne d’environ 200 Mo pour une image de conteneur. Certes, l’empreinte logicielle des images Slim est encore assez élevée (97 contre 16 pour les images Alpine), et en tant que telle, elle représente une plus grande surface de vulnérabilité (56 pour les `premières` contre `2` pour les `secondes`).

Vulnérabilités de sécurité

Les vulnérabilités, qui constituent une préoccupation récurrente, ont fait l’objet de nombreux articles sur les raisons pour lesquelles vous devriez réduire la taille de vos images de conteneurs. Cependant, la sémantique des questions de sécurité est très importante.

Si nous laissons de côté les images node et node:bullseye en raison de leur empreinte logicielle plus importante et de leur vulnérabilité de sécurité accrue, nous pouvons nous concentrer sur un ensemble plus restreint de types d’images. En comparant les images Slim, Alpine et Distroless, la variation entre les vulnérabilités de sécurité élevées et critiques n’est pas élevée en chiffres absolus et se situe entre 0 et 4 - un risque gérable qui pourrait potentiellement être sans rapport avec le cas d’utilisation de votre application.

Support et résilience

S’assurer que l’équipe Docker Node.js est capable de donner la priorité et de répondre aux préoccupations concernant la construction de vos images de conteneurs, et que les problèmes sont résolus en temps voulu, est un grand pas en avant. Les balises d’images officielles basées sur Debian sont un point à ne pas négliger dans votre liste de vérification.

Lorsque vous utilisez les balises d’image node ou node:bullseye-slim, que vous optiez pour une image complète du système d’exploitation ou que vous utilisiez la version allégée des dépendances, vous obtenez toujours la dernière version d’exécution de Node.js. Bien qu’il s’agisse d’un nombre pair(Node.js 18.2.0), à l’heure où nous écrivons ces lignes, il n’a pas encore été inclus dans le cycle de vie du support à long terme, ce qui signifie qu’il sera accompagné de nouvelles versions d’autres composants dépendants tels que les dernières versions de npm (qui est connu pour son nouveau comportement bogué et nécessite du temps pour se stabiliser).

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 d’exploitation Debian moderne, avec une version stable et active du support à long terme de Node.js.

Cela revient à choisir la balise node:lts-bullseye-slim pour l’image Node.js. Étant en faveur de l’utilisation de balises d’image déterministes, je ferais le léger changement suivant : utiliser le numéro de version sous-jacent réel au lieu de l’alias lts.

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

Si vous travaillez dans une équipe DevOps expérimentée pouvant prendre en charge des images de base personnalisées, ma deuxième meilleure recommandation serait d’utiliser la balise d’image Distroless de Google qui maintient la compatibilité glibc pour les versions d’exécution officielles de Node.js. Toutefois, ce flux de travail nécessitant de la maintenance, je le recommande seulement aux équipes à même de le faire.