RUN multiple vs RUN à chaîne unique dans Dockerfile, quoi de mieux?

132

Dockerfile.1exécute plusieurs RUN:

FROM busybox
RUN echo This is the A > a
RUN echo This is the B > b
RUN echo This is the C > c

Dockerfile.2 les rejoint:

FROM busybox
RUN echo This is the A > a &&\
    echo This is the B > b &&\
    echo This is the C > c

Chacun RUNcrée un calque, donc j'ai toujours supposé que moins de calques était mieux et donc Dockerfile.2mieux.

Cela est évidemment vrai lorsqu'un RUNsupprime quelque chose d'ajouté par un précédent RUN(c'est-à-dire yum install nano && yum clean all), mais dans les cas où tout RUNajoute quelque chose, il y a quelques points à considérer:

  1. Les couches sont censées simplement ajouter une différence au-dessus de la précédente, donc si la dernière couche ne supprime pas quelque chose d'ajouté dans une précédente, il ne devrait pas y avoir beaucoup d'avantage d'économie d'espace disque entre les deux méthodes ...

  2. Les couches sont extraites en parallèle de Docker Hub, donc Dockerfile.1, bien que probablement légèrement plus grandes, elles seraient théoriquement téléchargées plus rapidement.

  3. Si l'ajout d'une 4ème phrase (ie echo This is the D > d) et la reconstruction locale, Dockerfile.1construirait plus rapidement grâce au cache, mais Dockerfile.2devrait exécuter à nouveau les 4 commandes.

Alors, la question: quelle est la meilleure façon de faire un Dockerfile?

Yajo
la source
1
Impossible de répondre en général car cela dépend de la situation et de l'utilisation de l'image (optimiser la taille, la vitesse de téléchargement ou la vitesse de construction)
Henry

Réponses:

99

Lorsque cela est possible, je fusionne toujours les commandes qui créent des fichiers avec des commandes qui suppriment ces mêmes fichiers en une seule RUNligne. En effet, chaque RUNligne ajoute un calque à l'image, la sortie est littéralement les modifications du système de fichiers que vous pouvez afficher docker diffsur le conteneur temporaire qu'il crée. Si vous supprimez un fichier qui a été créé dans une couche différente, tout ce que le système de fichiers d'union fait est d'enregistrer le changement de système de fichiers dans une nouvelle couche, le fichier existe toujours dans la couche précédente et est expédié sur le réseau et stocké sur le disque. Donc, si vous téléchargez le code source, extrayez-le, compilez-le dans un binaire, puis supprimez les fichiers tgz et source à la fin, vous voulez vraiment que tout cela soit fait en une seule couche pour réduire la taille de l'image.

Ensuite, je divise personnellement les couches en fonction de leur potentiel de réutilisation dans d'autres images et de l'utilisation prévue de la mise en cache. Si j'ai 4 images, toutes avec la même image de base (par exemple debian), je peux extraire une collection d'utilitaires communs à la plupart de ces images dans la première commande d'exécution afin que les autres images bénéficient de la mise en cache.

L'ordre dans le Dockerfile est important lors de l'examen de la réutilisation du cache d'images. Je regarde tous les composants qui se mettront à jour très rarement, peut-être uniquement lorsque l'image de base se met à jour et les met en haut dans le Dockerfile. Vers la fin du Dockerfile, j'inclus toutes les commandes qui s'exécuteront rapidement et peuvent changer fréquemment, par exemple en ajoutant un utilisateur avec un UID spécifique à l'hôte ou en créant des dossiers et en modifiant les autorisations. Si le conteneur comprend du code interprété (par exemple JavaScript) en cours de développement actif, celui-ci est ajouté le plus tard possible afin qu'une reconstruction n'exécute que cette seule modification.

Dans chacun de ces groupes de changements, je consolide du mieux que je peux pour minimiser les couches. Donc, s'il y a 4 dossiers de code source différents, ceux-ci sont placés dans un seul dossier afin qu'il puisse être ajouté avec une seule commande. Toutes les installations de paquet à partir de quelque chose comme apt-get sont fusionnées en un seul RUN lorsque cela est possible pour minimiser la charge du gestionnaire de paquets (mise à jour et nettoyage).


Mise à jour pour les versions en plusieurs étapes:

Je m'inquiète beaucoup moins de la réduction de la taille de l'image dans les étapes non finales d'une construction en plusieurs étapes. Lorsque ces étapes ne sont pas balisées et expédiées à d'autres nœuds, vous pouvez maximiser la probabilité d'une réutilisation du cache en fractionnant chaque commande sur une RUNligne distincte .

Cependant, ce n'est pas une solution parfaite pour écraser les couches car tout ce que vous copiez entre les étapes sont les fichiers, et non le reste des métadonnées d'image telles que les paramètres de variable d'environnement, le point d'entrée et la commande. Et lorsque vous installez des packages dans une distribution Linux, les bibliothèques et autres dépendances peuvent être dispersées dans le système de fichiers, ce qui rend difficile une copie de toutes les dépendances.

Pour cette raison, j'utilise des versions en plusieurs étapes en remplacement de la création de binaires sur un serveur CI / CD, de sorte que mon serveur CI / CD n'a besoin que de l'outillage pour fonctionner docker build, et non d'un jdk, nodejs, go, et tout autre outil de compilation installé.

BMitch
la source
30

Réponse officielle répertoriée dans leurs meilleures pratiques (les images officielles DOIVENT y adhérer)

Minimiser le nombre de couches

Vous devez trouver l'équilibre entre la lisibilité (et donc la maintenabilité à long terme) du Dockerfile et la minimisation du nombre de couches qu'il utilise. Soyez stratégique et prudent quant au nombre de couches que vous utilisez.

Depuis le docker 1.10 COPY, les instructions , ADDet RUNajoutent un nouveau calque à votre image. Soyez prudent lorsque vous utilisez ces déclarations. Essayez de combiner les commandes en une seule RUNinstruction. Séparez-le uniquement si cela est nécessaire pour la lisibilité.

Plus d'infos: https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/#/minimize-the-number-of-layers

Mise à jour: Multi stage dans le docker> 17.05

Avec les versions en plusieurs étapes, vous pouvez utiliser plusieurs FROMinstructions dans votre Dockerfile. Chaque FROMinstruction est une étape et peut avoir sa propre image de base. Dans la dernière étape, vous utilisez une image de base minimale comme alpine, copiez les artefacts de construction des étapes précédentes et installez les exigences d'exécution. Le résultat final de cette étape est votre image. C'est donc là que vous vous souciez des couches comme décrit précédemment.

Comme d'habitude, docker a d' excellents documents sur les versions en plusieurs étapes. Voici un extrait rapide:

Avec les générations en plusieurs étapes, vous utilisez plusieurs instructions FROM dans votre Dockerfile. Chaque instruction FROM peut utiliser une base différente et chacune d'entre elles commence une nouvelle étape de la construction. Vous pouvez copier sélectivement des artefacts d'une étape à une autre, laissant derrière tout ce que vous ne voulez pas dans l'image finale.

Un excellent article de blog à ce sujet peut être trouvé ici: https://blog.alexellis.io/mutli-stage-docker-builds/

Pour répondre à vos points:

  1. Oui, les calques sont un peu comme des diffs. Je ne pense pas qu'il y ait des couches ajoutées s'il n'y a absolument aucun changement. Le problème est qu'une fois que vous avez installé / téléchargé quelque chose dans la couche # 2, vous ne pouvez pas le supprimer dans la couche # 3. Ainsi, une fois que quelque chose est écrit dans un calque, la taille de l'image ne peut plus être diminuée en supprimant cela.

  2. Bien que les couches puissent être tirées en parallèle, ce qui le rend potentiellement plus rapide, chaque couche augmente sans aucun doute la taille de l'image, même si elles suppriment des fichiers.

  3. Oui, la mise en cache est utile si vous mettez à jour votre fichier docker. Mais cela fonctionne dans un sens. Si vous avez 10 calques et que vous changez le calque n ° 6, vous devrez toujours tout reconstruire à partir du calque n ° 6 - n ° 10. Ce n'est donc pas trop souvent que cela accélérera le processus de création, mais il est garanti d'augmenter inutilement la taille de votre image.


Merci à @Mohan de m'avoir rappelé de mettre à jour cette réponse.

Menzo Wijmenga
la source
1
Ceci est maintenant obsolète - voir la réponse ci-dessous.
Mohan
1
@Mohan merci pour le rappel! J'ai mis à jour le message pour aider les utilisateurs.
Menzo Wijmenga
19

Il semble que les réponses ci-dessus soient dépassées. La documentation note:

Avant Docker 17.05, et même plus, avant Docker 1.10, il était important de minimiser le nombre de couches dans votre image. Les améliorations suivantes ont atténué ce besoin:

[...]

Docker 17.05 et les versions ultérieures ajoutent la prise en charge des builds en plusieurs étapes, qui vous permettent de copier uniquement les artefacts dont vous avez besoin dans l'image finale. Cela vous permet d'inclure des outils et des informations de débogage dans vos étapes de construction intermédiaires sans augmenter la taille de l'image finale.

https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/#minimize-the-number-of-layers

et

Notez que cet exemple compresse également artificiellement deux commandes RUN ensemble à l'aide de l'opérateur Bash &&, pour éviter de créer un calque supplémentaire dans l'image. Ceci est sujet aux pannes et difficile à maintenir.

https://docs.docker.com/engine/userguide/eng-image/multistage-build/

Les meilleures pratiques semblent avoir changé pour utiliser des builds à plusieurs niveaux et garder les Dockerfiles lisibles.

Mohan
la source
Bien que les versions à plusieurs étapes semblent être une bonne option pour garder l'équilibre, la solution réelle à cette question viendra lorsque l' docker image build --squashoption sortira de l'expérimentation.
Yajo
2
@Yajo - Je suis sceptique quant au dépassement de l' squashexpérimentation. Il a de nombreux gadgets et n'a de sens qu'avant les constructions en plusieurs étapes. Avec les versions en plusieurs étapes, il vous suffit d'optimiser la dernière étape, ce qui est très facile.
Menzo Wijmenga
1
@Yajo Pour développer cela, seuls les calques de la dernière étape font une différence sur la taille de l'image finale. Donc, si vous placez tous vos gubbins de générateur dans les étapes précédentes et que l'étape finale consiste simplement à installer les packages et à copier les fichiers des étapes précédentes, tout fonctionne à merveille et le squash n'est pas nécessaire.
Mohan
3

Cela dépend de ce que vous incluez dans vos calques d'image.

Le point clé est de partager autant de couches que possible:

Mauvais exemple:

Dockerfile.1

RUN yum install big-package && yum install package1

Dockerfile.2

RUN yum install big-package && yum install package2

Bon exemple:

Dockerfile.1

RUN yum install big-package
RUN yum install package1

Dockerfile.2

RUN yum install big-package
RUN yum install package2

Une autre suggestion est que la suppression n'est pas si utile que si elle se produit sur la même couche que l'action d'ajout / d'installation.

x jours
la source
Ces 2 partageraient-ils vraiment le RUN yum install big-packagecache?
Yajo
Oui, ils partageraient la même couche, à condition qu'ils partent de la même base.
Ondra Žižka