Sélectionner la meilleure image Docker Node.js
30 septembre 2022
0 minutes de lectureNote 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 denode: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
ouimagemagick/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.
Corrigez automatiquement les vulnérabilités des conteneurs
Détectez et corrigez les vulnérabilités des conteneurs gratuitement avec Snyk