Optimiser la taille de ses images Docker
Le projet est terminé et fonctionnel. Cette partie va au-delà de l'essentiel, pour des projets plus exigeants.
Pourquoi la taille d'une image n'est pas un détail
Une image plus petite se télécharge plus vite, occupe moins d'espace disque, et surtout, contient statistiquement moins d'éléments susceptibles de comporter une faille de sécurité connue : moins de logiciels installés, c'est mécaniquement moins de surface exposée. Comparez vous-même la taille de vos images accumulées au fil de ce parcours :
Remarquez déjà l'écart entre mini-carnet, basée sur node:22-alpine depuis le guide 11, et une image node:22 complète, non allégée : rien qu'en choisissant une variante alpine dès le départ, ce parcours avait déjà appliqué la première des bonnes pratiques.
Choisir une image de base minimale
Une variante alpine, construite sur une distribution Linux légère, pèse généralement plusieurs centaines de mégaoctets de moins qu'une image complète équivalente. Ce n'est pas propre à Node.js : la plupart des images officielles proposent une variante alpine ou équivalente. Le compromis à connaître : une image alpine utilise une bibliothèque système différente (musl plutôt que glibc), ce qui provoque très rarement, mais parfois, des incompatibilités avec certaines dépendances natives. Pour l'immense majorité des projets, y compris Mini-Carnet, ce risque reste négligeable face au gain de taille.
Le multi-stage build
Un multi-stage build permet d'utiliser plusieurs instructions FROM dans un même Dockerfile, chacune démarrant une nouvelle « étape ». Seule la dernière étape compte pour l'image finale ; les précédentes ne servent qu'à préparer du contenu, dont on ne récupère que ce qui est explicitement nécessaire. C'est particulièrement utile quand une étape de préparation (compilation, installation d'outils de construction) n'a aucune utilité une fois le résultat obtenu.
Voici une version multi-stage du Dockerfile de Mini-Carnet, qui sépare l'installation des dépendances de l'image finale réellement exécutée :
AS dependencies donne un nom à la première étape. COPY --from=dependencies, dans la seconde étape, récupère uniquement le dossier node_modules déjà installé, sans rien conserver d'autre de cette première étape, comme le cache interne de npm. --omit=dev exclut en plus les dépendances de développement, si package.json en déclarait.
Pour une application aussi simple que Mini-Carnet, sans étape de compilation, le multi-stage build n'apporte qu'un gain limité : il évite surtout le cache npm résiduel. La technique montre tout son intérêt sur des projets avec une vraie étape de construction, par exemple une application dont le code doit être compilé ou empaqueté avant d'être servi : l'image finale ne conserve alors que le résultat de cette construction, jamais les outils qui ont servi à la produire.
Vérifiez que vous avez compris
Dans un Dockerfile multi-stage à deux étapes, une instruction RUN placée dans la première étape a-t-elle un impact sur la taille de l'image finale ?
Pas directement. Seul ce que la seconde étape copie explicitement depuis la première, via COPY --from, se retrouve dans l'image finale. Tout ce que la première étape a installé ou téléchargé sans être ensuite copié reste dans cette étape intermédiaire, qui n'existe plus dans l'image livrée à la fin.