Comprendre les couches Docker


27

Nous avons le bloc suivant dans notre Dockerfile:

RUN yum -y update
RUN yum -y install epel-release
RUN yum -y groupinstall "Development Tools"
RUN yum -y install python-pip git mysql-devel libxml2-devel libxslt-devel python-devel openldap-devel libffi-devel openssl-devel

On m'a dit que nous devrions unir ces RUNcommandes pour réduire les couches de docker créées:

RUN yum -y update \
    && yum -y install epel-release \
    && yum -y groupinstall "Development Tools" \
    && yum -y install python-pip git mysql-devel libxml2-devel libxslt-devel python-devel openldap-devel libffi-devel openssl-devel

Je suis très nouveau dans Docker et je ne suis pas sûr de bien comprendre les différences entre ces deux versions de spécification de plusieurs commandes RUN. Quand unirait-on des RUNcommandes en une seule et quand est-il logique d'avoir plusieurs RUNcommandes?


Réponses:


35

Une image Docker est en fait une liste liée de couches de système de fichiers. Chaque instruction dans un Dockerfile crée une couche de système de fichiers qui décrit les différences dans le système de fichiers avant et après l'exécution de l'instruction correspondante. La docker inspectsous-commande peut être utilisée sur une image Docker pour révéler sa nature d'être une liste liée de couches de système de fichiers.

Le nombre de couches utilisées dans une image est important

  • lorsque vous poussez ou tirez des images, car cela affecte le nombre de téléchargements simultanés ou de téléchargements en cours.
  • lors du démarrage d'un conteneur, car les couches sont combinées ensemble pour produire le système de fichiers utilisé dans le conteneur; plus il y a de couches, plus les performances sont mauvaises, mais les différents backends du système de fichiers en sont affectés différemment.

Cela a plusieurs conséquences sur la façon dont les images doivent être construites. Le premier et le plus important conseil que je puisse donner est:

Conseil # 1 Assurez-vous que les étapes de génération où votre code source est impliqué arrivent aussi tard que possible dans le Dockerfile et ne sont pas liées aux commandes précédentes à l'aide de a &&ou de a ;.

La raison en est que toutes les étapes précédentes seront mises en cache et que les couches correspondantes n'auront pas besoin d'être téléchargées encore et encore. Cela signifie des versions plus rapides et des versions plus rapides, ce qui est probablement ce que vous voulez. Chose intéressante, il est étonnamment difficile de faire un usage optimal du cache docker.

Mon deuxième conseil est moins important mais je le trouve très utile du point de vue de la maintenance:

Conseil n ° 2 N'écrivez pas de commandes complexes dans le Dockerfile mais utilisez plutôt des scripts qui doivent être copiés et exécutés.

Un Dockerfile suivant ce conseil ressemblerait à

COPY apt_setup.sh /root/
RUN sh -x /root/apt_setup.sh
COPY install_pacakges.sh /root/
RUN sh -x /root/install_packages.sh

etc. L'avis de lier plusieurs commandes avec &&n'a qu'une portée limitée. Il est beaucoup plus facile d'écrire avec des scripts, où vous pouvez utiliser des fonctions, etc. pour éviter la redondance ou à des fins de documentation.

Les personnes intéressées par les pré-processeurs et désireuses d'éviter les petits frais généraux causés par les COPYétapes et génèrent en fait à la volée un Dockerfile où le

COPY apt_setup.sh /root/
RUN sh -x /root/apt_setup.sh

les séquences sont remplacées par

RUN base64 --decode … | sh -x

est la version encodée en base64 de apt_setup.sh.

Mon troisième conseil s'adresse aux personnes qui souhaitent limiter la taille et le nombre de couches au prix possible de constructions plus longues.

Conseil # 3 Utilisez le with-idiom pour éviter les fichiers présents dans les couches intermédiaires mais pas dans le système de fichiers résultant.

Un fichier ajouté par une instruction docker et supprimé par une instruction ultérieure n'est pas présent dans le système de fichiers résultant mais il est mentionné deux fois dans les couches docker constituant l'image docker en construction. Une fois, avec le nom et le contenu complet dans la couche résultant de l'instruction qui l'ajoute, et une fois comme avis de suppression dans la couche résultant de la suppression de l'instruction.

Par exemple, supposons que nous ayons temporairement besoin d'un compilateur C et d'une image et considérons le

# !!! THIS DISPLAYS SOME PROBLEM --- DO NOT USE !!!
RUN apt-get install -y gcc
RUN gcc --version
RUN apt-get --purge autoremove -y gcc

(Un exemple plus réaliste consisterait à construire des logiciels avec le compilateur au lieu de simplement affirmer sa présence avec l' --versionindicateur.)

L'extrait Dockerfile crée trois couches, la première contient la suite complète de gcc de sorte que même si elle n'est pas présente dans le système de fichiers final, les données correspondantes font toujours partie de l'image de la même manière et doivent être téléchargées, téléchargées et décompressées chaque fois que le l'image finale est.

Le with-idiom est une forme courante dans la programmation fonctionnelle pour isoler la propriété des ressources et la libération des ressources de la logique l'utilisant. Il est facile de transposer cet idiome en script shell, et nous pouvons reformuler les commandes précédentes comme le script suivant, à utiliser avec COPY & RUNcomme dans le conseil # 2.

# with_c_compiler SIMPLE-COMMAND
#  Execute SIMPLE-COMMAND in a sub-shell with gcc being available.

with_c_compiler()
(
    set -e
    apt-get install -y gcc
    "$@"
    trap 'apt-get --purge autoremove -y gcc' EXIT
)

with_c_compiler\
    gcc --version

Les commandes complexes peuvent être transformées en fonction afin de pouvoir être introduites dans le with_c_compiler. Il est également possible de chaîner les appels de plusieurs with_whateverfonctions, mais ce n'est peut-être pas très souhaitable. (En utilisant des fonctionnalités plus ésotériques du shell, il est certainement possible de faire with_c_compileraccepter des commandes complexes, mais il est préférable à tous égards d'envelopper ces commandes complexes dans des fonctions.)

Si nous voulons ignorer le conseil n ° 2, l'extrait de Dockerfile résultant serait

RUN apt-get install -y gcc\
 && gcc --version\
 && apt-get --purge autoremove -y gcc

ce qui n'est pas si facile à lire et à entretenir à cause de l'obscurcissement. Voyez comment la variante shell-script met l'accent sur la partie importante gcc --versiontandis que la &&variante enchaînée enterre cette partie au milieu du bruit.


1
Pourriez-vous inclure le résultat de la taille de la boîte après la construction à l'aide d'un script et à l'aide de plusieurs commandes dans une instruction RUN?
030

1
Cela me semble une mauvaise idée de mélanger la configuration de la base d'image (c'est-à-dire le système d'exploitation) et même des libs avec la configuration de la source que vous avez écrite. Vous dites "Assurez-vous que les étapes de génération où votre code source est impliqué arrivent aussi tard que possible". Y a-t-il un problème à faire de cette partie un artefact complètement indépendant?
JimmyJames

1
@ 030 Que voulez-vous dire par taille de «boîte»? Je ne sais pas de quelle boîte vous parlez.
Michael Le Barbier Grünewald

1
Je voulais dire la taille de l'image docker
030

1
@JimmyJames Cela dépend largement de votre scénario de déploiement. Si nous supposons un programme compilé, la «bonne chose à faire» serait de le conditionner et d'installer les dépendances de ce package et le package lui-même en deux étapes distinctes presque finales. Ceci pour maximiser l'utilité du cache docker et pour éviter de télécharger plusieurs couches avec les mêmes fichiers. Je trouve plus facile de partager des recettes de construction pour construire des images docker que de construire de longues chaînes d'images de dépendance, car cette dernière rend la reconstruction plus difficile.
Michael Le Barbier Grünewald

13

Chaque instruction que vous créez dans votre Dockerfile entraîne la création d'un nouveau calque d'image. Chaque couche apporte des données supplémentaires qui ne font pas toujours partie de l'image résultante. Par exemple, si vous ajoutez un fichier dans un calque, mais que vous le supprimez dans un autre calque plus tard, la taille de l'image finale inclura la taille de fichier ajoutée sous la forme d'un fichier spécial "blanc" bien que vous l'ayez supprimé.

Disons que vous disposez du Dockerfile suivant:

FROM centos:6

RUN yum -y update 
RUN yum -y install epel-release

La taille d'image résultante sera

bigimage     latest        3c5cbfbb4116        2 minutes ago    407MB

Comme ci-contre, avec Dockerfile "similaire":

FROM centos:6

RUN yum -y update  && yum -y install epel-release

La taille d'image résultante sera

smallimage     latest        7edeafc01ffe        3 minutes ago    384MB

Vous obtiendrez une taille encore plus petite si vous nettoyez le cache yum dans une seule instruction RUN.

Vous voulez donc garder un équilibre entre lisibilité / facilité d'entretien et nombre de couches / taille d'image.


4

Les RUNinstructions représentent chaque couche. Imaginez que l'on télécharge un package, l'installe et souhaite le supprimer. Si l'on utilise trois RUNinstructions, la taille de l'image ne diminuera pas car il existe des calques séparés. Si l'on exécute toutes les commandes à l'aide d'une seule RUNinstruction, la taille de l'image disque pourrait être réduite.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.