📘 Scripter la conteneurisation d’un logiciel

Dans ce dernier chapitre, nous allons voir comment automatiser le processus de crĂ©ation d’une nouvelle image de conteneur Ă  l’aide d’un petit langage de script appelĂ© Containerfile.

1. Problématique et principe

Dans le chapitre prĂ©cĂ©dent nous avons Ă©tudiĂ© comment crĂ©er manuellement une image de conteneur Ă  partir d’un conteneur temporaire. Bien que pratiquable − et trĂšs utile pour bien comprendre le principe de crĂ©ation d’image − cette maniĂšre de procĂ©dĂ© prĂ©sente plusieurs faiblesses :

  • Lors d’une manipulation manuelle il est assez facile de commettre une erreur humaine, que ce soit en copiant le mauvais fichier ou en installant la mauvaise dĂ©pendance. Il faut alors soit corriger "sur le fait", soit repartir de zĂ©ro.

  • Les manipulations manuelles peuvent prendre du temps, surtout si certaines actions Ă  rĂ©aliser sont coĂ»teuses pour la machine (tĂ©lĂ©chargement d’une dĂ©pendance trĂšs volumineuse, copie d’un fichier volumineux, etc.).

  • Enfin, en faisant le lien avec les deux points prĂ©cĂ©dents, un important problĂšme est le manque de reproductibilitĂ© de la crĂ©ation d’une image. En effet, il est frĂ©quent de devoir fabriquer une nouvelle image de conteneur pour chaque nouvelle version d’un logiciel que l’on dĂ©veloppe. Devoir effectuer toutes les actions manuelles de crĂ©ation de conteneur Ă  chaque nouvelle version parait peut pratiquable.

Heureusement, pour rĂ©pondre Ă  ces problĂšmes, la crĂ©ation d’une image de conteneur peut Ă©galement ĂȘtre automatisĂ©e Ă  l’aide d’un script de construction d’image, qui prend la forme d’une sĂ©quence d’actions qui seront automatiquement effectuĂ©es pour produire une image de conteneur. Ainsi, une fois un script de ce type bien Ă©crit (et corrigĂ© si nĂ©cessaire), il devient possible de le rĂ©-exĂ©cuter Ă  volontĂ©, par exemple pour permettre le dĂ©ploiement d’une nouvelle version d’un mĂȘme logiciel.

image creation script.drawio
Figure 1. CrĂ©ation d’une image de conteneur Ă  l’aide d’un script de crĂ©ation d’image (Containerfile)

Un script de construction d’image prend en entrĂ©e :

  • une image de base (bien choisie),

  • un logiciel Ă  dĂ©ployer,

  • d’autres fichiers Ă  ajouter Ă  l’image (ex. un fichier de configuration).

La sĂ©quence d’action que dĂ©crit un script de construction d’image est totalement comparable aux actions que l’on doit rĂ©aliser manuellement si on choisissait de crĂ©er manuellement une image de conteneur. Ainsi, un tel script peut contenir notamment les actions suivantes :

  • La copie d’un fichier de l’environnement d’accueil vers le conteneur (par exemple pour dĂ©poser le logiciel qu’on souhaite y exĂ©cuter),

  • L’exĂ©cution d’une commande au sein du conteneur (par exemple pour exĂ©cuter un installeur de paquets comme apt, dnf ou pip afin d’installer une dĂ©pendance nĂ©cessaire Ă  notre logiciel),

  • La dĂ©finition d’une mĂ©tadonnĂ©e de l’image de conteneur, par exemple le point d’entrĂ©e de l’image.

2. Containerfile, le langage de construction d’images

2.1. Présentation

Le langage de loin le plus utilisĂ© pour Ă©crire un script de construction d’image de conteneur est le langage informatique Containerfile.

C’est un langage informatique initialement dĂ©veloppĂ© pour le moteur de conteneurisation Docker (sous le nom Dockerfile), mais qui dĂ©sormais est utilisĂ© par une grande partie des outils de conteneurisation − y compris Podman.

On nomme communĂ©ment Containerfile un fichier contenant un script de construction Ă©crit avec le langage Containerfile. Un tel fichier est situĂ© la plupart du temps Ă  la racine du rĂ©pertoire contenant le code source du logiciel qu’on souhaite conteneuriser.

Pour des raisons d’inter-compĂątibilitĂ© avec Docker, il est Ă©galement possible de nommer ce fichier Dockerfile.

Un Containerfile est une sĂ©quence d'instructions. Chaque instruction commence par un mot-clĂ© (FROM, RUN, COPY, etc.) et est suivie par une liste d’arguments. Par convention, de maniĂšre analogue au langage SQL, on Ă©crit toujours les instructions en majuscule.

On trouve principalement trois sortes d’instructions :

  • L’instruction qui crĂ©e un conteneur temporaire Ă  partir d’une image de base (FROM).

  • Les instructions qui viennent modifier le conteneur temporaire pour y ajouter tout le nĂ©cessaire (COPY, RUN, WORKDIR).

  • Les instructions qui viennent dĂ©clarer des mĂ©tadonnĂ©es pour la future image de conteneur (ENV, CMD, WORKDIR).

On peut voir que WORKDIR fait simultanĂ©ment partie de deux catĂ©gories ci-dessus. En effet, une instruction WORKDIR va Ă  la fois changer le rĂ©pertoire courant au sein du conteneur temporaire (ce qui va influencer les prochaines actions rĂ©alisĂ©es dans le script), et modifier la mĂ©tadonnĂ©e qui dĂ©finit le rĂ©pertoire de dĂ©part de l’image de conteneur produite.

2.2. StratĂ©gies pour l’Ă©criture d’un Containerfile

Lorsqu’on Ă©crit un Containerfile pour crĂ©er une image de conteneur pour un logiciel, il existe au moins deux stratĂ©gies possibles.

  • Un premiĂšre stratĂ©gie est de faire l’hypothĂšse que le logiciel a dĂ©jĂ  Ă©tĂ© compilĂ© et/ou empaquetĂ©, et est donc dĂ©jĂ  disponible dans une forme exĂ©cutable sans travail supplĂ©mentaire nĂ©cessaire. Le Containerfile est donc simplifiĂ© car il peut juste se contenter de prĂ©parer le mini-environnement d’exĂ©cution, et de copier le logiciel dĂ©jĂ  prĂȘt dans cet environnement, sans se prĂ©occuper de la construction du logiciel. En revanche, cela signifie que des commandes devront ĂȘtre rĂ©alisĂ©es (par exemple, appeler un compilateur) avant l’exĂ©cution du Containerfile.

  • Une seconde stratĂ©gie est de compiler/empaqueter le logiciel directement lors de la crĂ©ation de l’image. Le Containerfile est alors plus complexe, car il intĂšgre les actions nĂ©cessaires pour construire le logiciel, en sus de devoir prĂ©parer l’environnement d’exĂ©cution. L’avantage de cette stratĂ©gie est de simplifier la crĂ©ation de l’image, car il suffit alors de juste exĂ©cuter le Containerfile sans autre action prĂ©alable.

Cette distinction entre deux stratĂ©gies a surtout du sens lorsque le logiciel qu’on souhaite conteneuriser est Ă©crit dans un langage qui nĂ©cessite une Ă©tape de compilation (par exemple Java, C, ou C++) avant qu’on puisse exĂ©cuter le logiciel. Si le langage utilisĂ© ne nĂ©cessite pas de compilation (par exemple Python ou R), alors cette distinction n’a pas lieu d’ĂȘtre.

Passons maintenant en revue les instructions les plus importantes qu’on puisse trouver dans un Containerfile.

2.3. CrĂ©er un conteneur temporaire Ă  partir d’une image de base

La premiĂšre instruction d’un Containerfile est obligatoirement une instruction FROM, qui permet de dĂ©finir l'image de base utilisĂ©e comme pour de dĂ©part pour crĂ©er notre propre image.

La syntaxe est la suivante :

FROM reference_image

avec reference_image une rĂ©fĂ©rence d’image valide.

Dans les rares cas oĂč on souhaite construire une image en partant de zĂ©ro, sans image de base, alors on peut Ă©crire :

FROM scratch

Lorsqu’une instruction FROM est exĂ©cutĂ©e, l’image rĂ©fĂ©rencĂ©e est rĂ©cupĂ©rĂ©e depuis un registre distant si nĂ©cessaire, puis un conteneur temporaire est créé Ă  partir de cette image. Toutes les instructions qui suivent FROM agissent sur ce conteneur temporaire afin de l’enrichir pour construire l’environnement souhaitĂ©.

Exemple 1. Instruction FROM pour Simplestblog

Simplestblog Ă©tant une application web dĂ©veloppĂ©e en Python, il nous faut donc Ă  minima un environnement fournissant la commande python. On choisit d’utiliser l’image officielle Python comme image de base. L’image en question est disponible sur le Docker Hub et on se rend sur sa page de description. On souhaite une version de l’image qui contienne Python version 3, et qui soit aussi lĂ©gĂšre (slim) que possible. On se dĂ©cide Ă  utiliser l’Ă©tiquette 3-slim.

On Ă©crit donc en premiĂšre ligne de notre Containerfile :

FROM docker.io/python:3-slim 
Exemple 2. Instruction FROM pour Automatic-Image-Converter

Automatic-Image-Converter est un logiciel développé en Java, il nous faut donc à minima un environnement fournissant la commande java.

Si on fait l’hypothĂšse que la compilation du logiciel est effectuĂ©e avant de construire l’image, alors un environnement avec la commande java permettant d’exĂ©cuter un fichier .jar nous suffirait. On se rend sur la page de l’image de Eclipse Temurin, un des principaux exĂ©cuteurs Java disponibles, et on regarde les versions. On souhaite Java en version 17, en version JRE (c’est Ă  dire sans le compilateur Java), et dans une image aussi lĂ©gĂšre que possible. On choisit donc l’Ă©tiquette 17-jre-alpine, et on Ă©crit l’instruction FROM suivante :

FROM docker.io/eclipse-temurin:17-jre-alpine

Si on fait l’hypothĂšse que la compilation du logiciel est effectuĂ©e pendant la construction de l’image, alors nous avons besoin d’un environnement avec les commandes nĂ©cessaires pour cette compilation. Le code de Automatic-Image-Converter repose sur l’outil Maven pour la compilation, il nous faut donc une image nous fournissant la commande maven. On se rend sur la page de l’image officielle Maven, et on regarde les versions. On souhaite Java en version 17, et on souhaite utiliser Eclipse Temurin comme distribution Java Ă  la fois pour la compilation et l’exĂ©cution. On choisit donc l’Ă©tiquette 3-eclipse-temurin-17, et on Ă©crit l’instruction FROM suivante :

FROM docker.io/maven:3-eclipse-temurin-17

2.4. Ajouter du contenu Ă  l’image

L’instruction WORKDIR a deux effets :

  • change de rĂ©pertoire courant au sein du conteneur, et crĂ©e ce rĂ©pertoire s’il n’existe pas  ,

  • dĂ©finir la mĂ©tadonnĂ©e rĂ©pertoire de dĂ©part de l’image de conteneur produite.

WORKDIR répertoire_conteneur

oĂč rĂ©pertoire_conteneur est un chemin vers le rĂ©pertoire ciblĂ© dans le conteneur.

Changer le rĂ©pertoire courant influence toutes les instructions suivantes, qui prendront ce rĂ©pertoire comme contexte d’exĂ©cution, jusqu’Ă  la prochaine instruction WORKDIR.

Cette instruction équivaut, sur un systÚme Linux classique, à utiliser la commande cd.

L’instruction COPY copie un rĂ©pertoire ou un fichier depuis l’environnement d’accueil, vers le conteneur.

COPY chemin_environnement_hote chemin_conteneur

oĂč chemin_environnement_hote est un chemin vers un fichier/rĂ©pertoire de l’environnement d’accueil, et chemin_conteneur un chemin vers un emplacement dans le conteneur.

Au delĂ  de son apparente simplicitĂ©, il faut faire attention Ă  quelques particularitĂ©s peu intuitives concernant COPY :

  • Le chemin_environnement_hote est forcĂ©ment un chemin relatif Ă  partir du rĂ©pertoire contexte donnĂ© lors de la crĂ©ation de l’image (notion expliquĂ©e plus tard en Section 3). En outre, ce chemin relatif ne peux pas accĂ©der Ă  du contenu situĂ© en dehors du rĂ©pertoire contexte.

  • Si le chemin_environnement_hote pointe vers un rĂ©pertoire, alors le contenu de ce rĂ©pertoire sera copiĂ©, mais pas le rĂ©pertoire lui-mĂȘme.

  • Si le chemin_conteneur pointe vers une rĂ©pertoire, ce chemin doit forcĂ©ment se terminer avec un /.

L’instruction RUN exĂ©cute une commande Ă  l’intĂ©rieur du conteneur.

RUN commande argument1 argument2 argument3


Pour que cette commande puisse ĂȘtre exĂ©cutĂ©e, il faut que l’outil exĂ©cutĂ© soit au prĂ©alable installĂ© Ă  l’intĂ©rieur du conteneur. Cette instruction permet par exemple d’installer de nouveaux logiciels Ă  l’aide d’un installeur de paquets (comme apt ou apk), ou bien de manipuler des fichiers Ă  l’aide de commandes comme cp, mv, rm (si ces derniĂšres sont disponibles dans le conteneur), ou mĂȘme de compiler le logiciel qu’on souhaite avoir dans le conteneur, si cela est nĂ©cessaire.

Il n’est pas possible de changer le rĂ©pertoire courant Ă  l’aide d’une instruction RUN cd mon_repertoire, car c’est toujours la derniĂšre commande WORKDIR qui impose le rĂ©pertoire courant. Il faut donc toujours utiliser WORKDIR et non RUN cd.
Exemple 3. Ajout de contenu Ă  l’image Simplestblog

On poursuit l’Ă©criture du Containerfile pour Simplestblog, et on obtient les lignes suivantes :

FROM docker.io/python:3-slim 

WORKDIR /install
COPY . .
RUN pip install -r ./requirements.txt
  • On commence par se positionner dans le rĂ©pertoire /install Ă  l’aide de la commande WORKDIR.

  • On utilise ensuite COPY pour copier tout le contenu du code source (situĂ© dans le rĂ©pertoire courant de l’environnement d’accueil, donc accessible avec le simple chemin “.”) Ă  l’intĂ©rieur du rĂ©pertoire /install (qui est le contexte courant au sein du conteneur, donc accessible avec le chemin “.”).

  • On utilise RUN pour exĂ©cuter l’utilitaire pip et qui se charge de tĂ©lĂ©charger les dĂ©pendances dont nous avons besoin pour exĂ©cuter le logiciel. Ces dĂ©pendances sont Ă©crites aux cĂŽtĂ©s du code source de Simplestblog dans un fichier requirements.txt dont le contenu est le suivant :

    flask
    mysql-connector-python
    waitress

    avec flask la dĂ©pendance principale du logiciel, mysql-connector-python une dĂ©pendance pour permettre la connexion Ă  une base de donnĂ©es compatible MySQL, et waitress un logiciel permettant de dĂ©marrer le serveur web de l’application.

Exemple 4. Ajout de contenu aux images Automatic-Image-Converter

On poursuit l’Ă©criture des deux variantes de Containerfile pour Automatic-Image-Converter.

Si on considĂšre que le logiciel est dĂ©jĂ  compilĂ©, il suffit de faire COPY pour placer le rĂ©sultat de cette compilation (Ă  savoir un fichier .jar situĂ© dans le rĂ©pertoire target) :

FROM docker.io/eclipse-temurin:17-jre-alpine
COPY target/*.jar /app.jar

Si on souhaite compiler le logiciel, des Ă©tapes supplĂ©mentaires sont nĂ©cessaires, et on Ă©crit le Containerfile suivant :

FROM docker.io/maven:3-eclipse-temurin-17

# Prepare folders
RUN mkdir /inputs
RUN mkdir /outputs

# Add source code and compile from source folder
WORKDIR /src
COPY . .
  • On utilise WORKDIR pour se positionner dans le rĂ©pertoire /src dans le conteneur.

  • On utilise COPY pour copier tout le code source de l’environnement d’accueil (situĂ© dans le rĂ©pertoire courant, donc “.”) vers le rĂ©pertoire /src (situĂ© dans le contexte courant spĂ©cifiĂ© par WORKDIR, donc “.”).

  • On utilise RUN pour exĂ©cuter la commande mvn package. Cette commande du logiciel Maven va, en une seule traite, tĂ©lĂ©charger toutes les dĂ©pendances requises puis compiler tout le projet Java en un unique fichier .jar au sein du rĂ©pertoire target.

  • Enfin, on utilise Ă  nouveau RUN pour dĂ©placer le fichier produit par Maven Ă  un emplacement un peu plus pratique, Ă  savoir Ă  la racine du conteneur (donc /), en renommant au passage le fichier en app.jar.

2.5. DĂ©finir les mĂ©tadonnĂ©es de l’image de conteneur

Un dernier ensemble d’instructions utiles permet de dĂ©finir les mĂ©tadonnĂ©es de l’image produite pour qu’elle fonctionne de la maniĂšre souhaitĂ©e.

L’instruction WORKDIR, prĂ©sentĂ©e prĂ©cĂ©demment, permet de dĂ©finir le rĂ©pertoire de dĂ©part (c’est Ă  dire le rĂ©pertoire Ă  partir duquel sera exĂ©cutĂ© le point d’entrĂ©e) de l’image de contneur.

L’instruction ENV permet de donner une valeur Ă  une variable d’environnement au sein du conteneur créé Ă  partir de l’image  :

ENV nom_variable valeur

Cette variable d’environnement est ensuite accessible par tous les logiciels exĂ©cutĂ©s au sein du conteneur, par exemple pour les configurer.

Les variables d’environnement spĂ©cifiĂ©es avec ENV pourront toujours ĂȘtre Ă©crasĂ©es par de nouvelles valeurs (avec --env) lors de la crĂ©ation d’un conteneur Ă  partir de l’image. Ce mĂ©canisme peut par exemple servir Ă  dĂ©finir des valeurs par dĂ©faut Ă  l’aide de ENV, et de laisser un utilisateur de l’image mettre d’autres valeurs si nĂ©cessaire.

L’instruction CMD permet de spĂ©cifier le point d’entrĂ©e de l’image, autrement dit la commande qui sera exĂ©cutĂ©e par dĂ©faut lors du dĂ©marrage du conteneur. Elle s’Ă©crit de maniĂšre identique Ă  RUN :

CMD commande argument1 argument2 argument3

Il est possible lors de la crĂ©ation d’un conteneur Ă  partir de l’image de remplacer le point d’entrĂ©e spĂ©cifiĂ© par CMD par une autre commande. Cela peut permettre d’exĂ©cuter ponctuellement une commande diffĂ©rente dans le conteneur, que ce soit par exemple pour inspecter le contenu de ses fichiers (avec ls) ou bien pour ouvrir un interprĂ©teur de commandes interactif dans le conteneur (avec sh).
Exemple 5. DĂ©finition des mĂ©tadonnĂ©es de l’image Simplestblog

On termine enfin l’Ă©criture du Containerfile pour Simplestblog :

FROM docker.io/python:3-slim 

WORKDIR /install
COPY . .
RUN pip install -r ./requirements.txt

ENV SIMPLESTBLOG_SETTINGS_FILE /config.py
CMD waitress-serve --call simplestblog:create_app
  • On utilise WORKDIR pour dĂ©finir /install comme rĂ©pertoire de dĂ©part.

  • On utilise ENV pour donner la valeur /config.py Ă  la variable d’environnement SIMPLESTBLOG_SETTINGS_FILE. Cette variable d’environnement est utilisĂ©e dans le code de Simplestblog pour connaĂźtre l’emplacement du fichier de configuration devant ĂȘtre chargĂ©. Ici, on dĂ©cide de fixer un emplacement pour ce fichier car on attend de l’utilisateur de l’image qu’il effectue un montage d’un fichier de configuration prĂ©parĂ© dans l’environnement d’accueil, vers l’emplacement /config.py dans le conteneur.

  • On utilise CMD pour spĂ©cifier le point d’entrĂ©e, Ă  savoir ici le dĂ©marrage de l’application web. C’est une commande utilisant l’outil waitress qui avait Ă©tĂ© prĂ©cĂ©demment installĂ© avec pip.

Exemple 6. Définition des métadonnées des images Automatic-Image-Converter

On poursuit l’Ă©criture des deux variantes de Containerfile pour Automatic-Image-Converter.

Version avec le logiciel compilĂ© avant de construire l’image :

FROM docker.io/eclipse-temurin:17-jre-alpine
COPY target/*.jar /app.jar
RUN mkdir /inputs
RUN mkdir /outputs

CMD java -jar /app.jar /inputs /outputs $FORMAT $DURATION

Version oĂč le logiciel est compilĂ© durant la construction de l’image :

FROM docker.io/maven:3-eclipse-temurin-17

# Prepare folders
RUN mkdir /inputs
RUN mkdir /outputs

# Add source code and compile from source folder
WORKDIR /src
COPY . .
RUN mvn package

# Copy result of compilation to the root folder
RUN mv /src/target/*.jar /app.jar

CMD java -jar /app.jar /inputs /outputs $FORMAT $DURATION

Dans les deux versions, on ajoute une instruction CMD qui utilise la commande java afin d’exĂ©cuter le fichier compilĂ© placĂ© prĂ©alablement Ă  l’emplacement /app.jar.

Automatic-Image-Converter s’attend Ă  trouver quatre arguments pour s’exĂ©cuter, dans un ordre strict : le rĂ©pertoire d’entrĂ©e, puis le rĂ©pertoire de sortie, puis le format vers lequel convertir, et enfin la durĂ©e entre chaque passe de conversion.

Pour l’image de conteneur, on dĂ©cide de gĂ©rer les arguments de la maniĂšre suivante :

  • On fixe le premier argument (le rĂ©pertoire d’entrĂ©e) Ă  la valeur /inputs, et le second argument (le rĂ©pertoire de sortie) Ă  la valeur /outputs. En effet, on choisit ici d’attendre de l’utilisateur de l’image qu’il effectue deux montages de rĂ©pertoires provenant de l’environnement d’accueil, Ă  la fois vers /inputs et vers /outputs

  • On utilise deux variables d’environnement, FORMAT et DURATION, pour les deux derniers paramĂštres. L’idĂ©e ici est de permettre Ă  l’utilisateur de l’image de spĂ©cifier des valeurs pour ces deux variables au moment de crĂ©er un conteneur Ă  partir de l’image.

2.6. La construction de type "multi-stage"

(20/11/2025) Le contenu de cette section ne sera demandé pour les évaluations.

On a vu qu’il existe au moins deux stratĂ©gies distinctes pour Ă©crire un Containerfile : soit on construit le logiciel avant de construire l’image, soit on construit le logiciel pendant que l’on construit l’image.

Un grand avantage de la premiĂšre stratĂ©gie est ne pas nĂ©cessiter d’avoir dans l’image tout le nĂ©cessaire pour construire le logiciel (compilateur, gestionnaire de dĂ©pendances, etc). Cela permet donc d’obtenir des images plus lĂ©gĂšres, qui se concentrent sur le nĂ©cessaire pour exĂ©cuter le logiciel.

Cependant, il est tout de mĂȘme souvent dĂ©sirable de suivre la deuxiĂšme stratĂ©gie, de maniĂšre Ă  simplifier grandement le processus de crĂ©ation de l’image. Malheureusement, cela signifie gĂ©nĂ©ralement une image rĂ©sultante plus lourde.

Pour rĂ©soudre cela, il existe une possibilitĂ© offerte par le langage Containerfile: la construction d’image multi-stage. Cette fonctionnalitĂ© permet d’utiliser des conteneurs temporaires intermĂ©diaires durant le processus de construction de l’image, par exemple pour exĂ©cuter des commandes pour compiler un logiciel. On peut ensuite copier des fichiers d’un de ces conteneurs temporaires vers un autre conteneur, par exemple le conteneur final qui sera lui transformĂ© en image.

Cette fonctionnalitĂ© s’utilise en combinant deux instructions que l’on a dĂ©jĂ  prĂ©sentĂ©es :

  • L’instruction FROM, qui s’utilise de maniĂšre diffĂ©rente par deux aspects :

    • Il est possible d’utiliser plusieurs fois l’instruction FROM au sein d’un mĂȘme Containerfile. À chaque fois qu’une nouvelle instruction FROM est exĂ©cutĂ©e, un nouveau conteneur temporaire est créé, et toutes les instructions qui suivent s’exĂ©cutent dans ce nouveau conteneur (jusqu’Ă  la prochaine instruction FROM).

    • À chaque nouvelle utilisation de l’instruction FROM, on peut ajouter Ă  la fin de l’instruction la notation AS <nom du conteneur> pour donner un nom au conteneur temporaire créé Ă  partir de ce point.

  • L’instruction COPY, qui est capable de copier des fichiers d’un conteneur temporaire Ă  un autre. On utilise pour cela la notation COPY --from=<nom du conteneur source> <chemin conteneur source> <chemin conteneur actuel>, qui nĂ©cessite de connaĂźtre le nom du conteneur temporaire dans lequel on souhaite rĂ©cupĂ©rer des fichiers.

Ainsi, on peut par exemple compiler un logiciel dans un premier conteneur − qui peut contenir autant d’outils que nĂ©cessaires pour cela − avant de copier le produit de cette compilation dans un second conteneur − qui peut se contenter de possĂ©der le nĂ©cessaire juste pour exĂ©cuter le logiciel.

Exemple 7. Version multi-stage du Containerfile de Automatic-Image-Converter

On a vu dans l'Exemple 4 deux maniĂšres de crĂ©er ou ajouter le contenu nĂ©cessaire pour Automatic-Image-Converter au sein d’une image.

Regardons la deuxiĂšme variante, dans laquelle on compile le code Java au sein du conteneur :

FROM docker.io/maven:3-eclipse-temurin-17

# Prepare folders
RUN mkdir /inputs
RUN mkdir /outputs

# Add source code and compile from source folder
WORKDIR /src
COPY . .

Si on exĂ©cute ce script, l’image rĂ©sultante fait 505 MB.

On peut faire deux observations :

  • L’image de base, maven:3-eclipse-temurin-17 contient tous les outils de compilation Java, y compris l’outil Maven. Cela occupe une certaine place.

  • On copie le code source avec une instruction COPY, or le code source n’aura aucun rĂŽle Ă  jouer au final dans l’exĂ©cution, car cette derniĂšre utilisera le rĂ©sultat de la compilation.

On dĂ©cide donc d’Ă©crire une version multi-stage de ce Containerfile :

# First stage: build with maven
FROM docker.io/maven:3-eclipse-temurin-17 AS build
WORKDIR /src
COPY . /src
RUN mvn package

# Second stage: package with a slim jre
FROM docker.io/eclipse-temurin:17-jre-jammy
COPY --from=build /src/target/*.jar /app.jar

# Prepare folders
RUN mkdir /inputs
RUN mkdir /outputs

# Configure entrypoint
CMD java -jar /app.jar /inputs /outputs $FORMAT $DURATION

Le dĂ©but du script est identique au prĂ©cĂ©dent : on utilise l’image maven:3-eclipse-temurin-17 et on compile le logiciel avec la commande mvn package. On prend soin de faire cela dans un conteneur temporaire nommĂ© build.

Cependant, on utilise ensuite une seconde instruction FROM pour poursuivre dans un nouveau conteneur, avec comme image de base eclipse-temurin:17-jre-jammy qui ne contient que le nĂ©cessaire pour exĂ©cuter un logiciel Java. Il ne reste plus qu’Ă  faire COPY --from=build afin d’aller rĂ©cupĂ©rer le fichier .jar produit dans le premier conteneur, et notre image est dĂ©jĂ  prĂȘte.

La taille de l’image obtenue avec ce script amĂ©liorĂ© est de 274 MB.

2.7. Autres instructions (non utilisées dans ce cours)

MĂȘme si nous n’allons pas les utiliser dans ce cours, il est prĂ©fĂ©rable de mentionner briĂšvement quelques autres instructions que vous risquez de croiser au sein de scripts Ă©crits en Containerfile dans le futur.

  • L’instruction ADD permet, tout comme COPY, d’ajouter des fichiers au sein de l’image. Cette instruction est cependant un peu plus sophistiquĂ©e que COPY, et ajoute notamment la possibilitĂ© de copier dans l’image du contenu provenant du rĂ©seau (par exemple provenant d’Internet), ou bien de dĂ©compresser automatiquement une archive de l’environnement d’accueil Ă  l’intĂ©rieur du conteneur.

  • L’instruction ENTRYPOINT permet tout comme CMD de spĂ©cifier un point d’entrĂ©e pour les images de conteneurs. Cependant elle nĂ©cessite une syntaxe lĂ©gĂšrement diffĂ©rente que nous ne verrons pas en cours, et possĂšde certaines subtilitĂ©s, donc nous privilĂ©gerons CMD.

  • L’instruction USER permet de spĂ©cifier quel compte Linux virtuel est utilisĂ© Ă  l’intĂ©rieur du conteneur. Par dĂ©faut, la plupart des images de base fonctionnent avec un unique compte virtuel root qui possĂšde tous les droits Ă  l’intĂ©rieur du conteneur. Pour des raisons de sĂ©curitĂ© et, il est souvent souhaitable de crĂ©er un compte virtuel autre que root dans un conteneur Ă  l’aide d’une instruction RUN, puis d’utiliser USER pour indiquer au conteneur de toujours utiliser cet compte virtuel pour exĂ©cuter une commande (et en particulier le point d’entrĂ©e).

  • L’instruction EXPOSE permet de dĂ©clarer que le logiciel exĂ©cutĂ© par le point d’entrĂ©e du conteneur va rĂ©aliser une Ă©coute sur un port rĂ©seau particulier du conteneur. Cette dĂ©claration n’est pas nĂ©cessaire pour qu’un port mis en Ă©coute ou soit publiĂ©, et n’est utilise que pour documenter l’image, et pour pouvoir demander au moteur de conteneurisation d’effectuer une publication de ports par dĂ©faut − ce que nous ne ferons pas.

  • L’instruction VOLUME permet de dĂ©clarer un volume au sein de l’image. Dans ce cours nous prĂ©fĂ©rons utiliser uniquement le montage de rĂ©pertoires et de fichiers, et non les volumes, donc nous n’utiliserons pas cette fonctionnalitĂ©.

3. Utiliser Podman pour exécuter un script Containerfile

Nous avons vu dans la partie prĂ©cĂ©dente comment Ă©crire des fichiers Containerfile Ă  l’aide du langage Containerfile. De nombreux outils sont capables de lire un fichier Containerfile afin de produire un image de conteneur, y compris le moteur de conteneurisation Podman.

Pour construire une image Ă  partir d’un Containerfile :

$ podman image build --file <chemin Containerfile> \
                     --tag <nom de l'image>:<étiquette> \
                     <chemin contexte>

avec :

  • <chemin Containerfile> un chemin vers le fichier Containerfile Ă  exĂ©cuter. Si cette clause --file est omise, Podman ira par dĂ©faut exĂ©cuter le Containerfile dans le rĂ©pertoire spĂ©cifiĂ© par <chemin contexte>, s’il existe.

  • <chemin contexte> un chemin vers le rĂ©pertoire dit de contexte depuis lequel sera exĂ©cutĂ© le script de construction. Toutes les instructions du script seront exĂ©cutĂ©es Ă  partir de ce chemin, et seuls les fichiers prĂ©sents dans ce rĂ©pertoire seront accessibles par le Containerfile.

  • <nom de l’image> le nom donnĂ© Ă  l’image.

  • <Ă©tiquette> l’Ă©tiquette donnĂ©e Ă  la version produite de l’image.

La version courte de --file est -f, et la version courte de --tag est -t.
Exemple 8. Construction de l’image Simplestblog

On se rend dans le rĂ©pertoire avec le code source de Simplestblog (qui est Ă©galement le rĂ©pertoire contenant le Containerfile), et on effectue :

$ podman image build --tag simplestblog:v1 .

On obtient la sortie textuelle suivante :

STEP 1/6: FROM docker.io/python:3-slim
Trying to pull docker.io/library/python:3-slim...
Getting image source signatures
Copying blob b65b2f4fad51 done
Copying blob 673a53d7ddde done
Copying blob 189ec909d128 done
Copying blob 4868cf843ed8 done
Copying blob 360eba32fa65 done
Copying config c1c23a6632 done
Writing manifest to image destination
STEP 2/6: WORKDIR /install
--> 7e5c42f47726
STEP 3/6: COPY . .
--> 3a8b64ea6f0d
STEP 4/6: RUN pip install -r ./requirements.txt
Collecting flask (from -r ./requirements.txt (line 1))
  Obtaining dependency information for flask from https://files.pythonhosted.org/packages/fd/56/26f0be8adc2b4257df20c1c4260ddd0aa396cf8e75d90ab2f7ff99bc34f9/flask-2.3.3-py3-none-any.whl.metadata
  Downloading flask-2.3.3-py3-none-any.whl.metadata (3.6 kB)
Collecting mysql-connector-python (from -r ./requirements.txt (line 2))
  Obtaining dependency information for mysql-connector-python from https://files.pythonhosted.org/packages/55/8e/1270601b42d40c4fb0191fd9043430889510bdbc711c482b12e6f66099e2/mysql_connector_python-8.1.0-cp311-cp311-manylinux_2_17_x86_64.whl.metadata
  Downloading mysql_connector_python-8.1.0-cp311-cp311-manylinux_2_17_x86_64.whl.metadata (2.0 kB)
Collecting waitress (from -r ./requirements.txt (line 3))
  Downloading waitress-2.1.2-py3-none-any.whl (57 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 57.7/57.7 kB 3.0 MB/s eta 0:00:00
Collecting Werkzeug>=2.3.7 (from flask->-r ./requirements.txt (line 1))
  Obtaining dependency information for Werkzeug>=2.3.7 from https://files.pythonhosted.org/packages/9b/59/a7c32e3d8d0e546a206e0552a2c04444544f15c1da4a01df8938d20c6ffc/werkzeug-2.3.7-py3-none-any.whl.metadata
  Downloading werkzeug-2.3.7-py3-none-any.whl.metadata (4.1 kB)
Collecting Jinja2>=3.1.2 (from flask->-r ./requirements.txt (line 1))
  Downloading Jinja2-3.1.2-py3-none-any.whl (133 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 133.1/133.1 kB 9.3 MB/s eta 0:00:00
Collecting itsdangerous>=2.1.2 (from flask->-r ./requirements.txt (line 1))
  Downloading itsdangerous-2.1.2-py3-none-any.whl (15 kB)
Collecting click>=8.1.3 (from flask->-r ./requirements.txt (line 1))
  Obtaining dependency information for click>=8.1.3 from https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl.metadata
  Downloading click-8.1.7-py3-none-any.whl.metadata (3.0 kB)
Collecting blinker>=1.6.2 (from flask->-r ./requirements.txt (line 1))
  Downloading blinker-1.6.2-py3-none-any.whl (13 kB)
Collecting protobuf<=4.21.12,>=4.21.1 (from mysql-connector-python->-r ./requirements.txt (line 2))
  Downloading protobuf-4.21.12-cp37-abi3-manylinux2014_x86_64.whl (409 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 409.8/409.8 kB 25.5 MB/s eta 0:00:00
Collecting MarkupSafe>=2.0 (from Jinja2>=3.1.2->flask->-r ./requirements.txt (line 1))
  Obtaining dependency information for MarkupSafe>=2.0 from https://files.pythonhosted.org/packages/fe/21/2eff1de472ca6c99ec3993eab11308787b9879af9ca8bbceb4868cf4f2ca/MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata
  Downloading MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (3.0 kB)
Downloading flask-2.3.3-py3-none-any.whl (96 kB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 96.1/96.1 kB 6.3 MB/s eta 0:00:00
Downloading mysql_connector_python-8.1.0-cp311-cp311-manylinux_2_17_x86_64.whl (27.5 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 27.5/27.5 MB 20.1 MB/s eta 0:00:00
Downloading click-8.1.7-py3-none-any.whl (97 kB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 97.9/97.9 kB 26.1 MB/s eta 0:00:00
Downloading werkzeug-2.3.7-py3-none-any.whl (242 kB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 242.2/242.2 kB 28.0 MB/s eta 0:00:00
Downloading MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (28 kB)
Installing collected packages: waitress, protobuf, MarkupSafe, itsdangerous, click, blinker, Werkzeug, mysql-connector-python, Jinja2, flask
Successfully installed Jinja2-3.1.2 MarkupSafe-2.1.3 Werkzeug-2.3.7 blinker-1.6.2 click-8.1.7 flask-2.3.3 itsdangerous-2.1.2 mysql-connector-python-8.1.0 protobuf-4.21.12 waitress-2.1.2
WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv
--> 43bb9c452599
STEP 5/6: ENV SIMPLESTBLOG_SETTINGS_FILE /config.py
--> f819b13d74c6
STEP 6/6: CMD waitress-serve --call simplestblog:create_app
COMMIT simplestblog:v1
--> 6fb49e42beb6
Successfully tagged localhost/simplestblog:v1
6fb49e42beb6b5e997d3aae2fe9d0cb98fea2e6abd6ff8dedcd053395bfe8f3d

On peut voir que la sortie textuelle nous explique chaque étape avec un intitulé STEP.

  • STEP 1/6: L’image distante est rĂ©cupĂ©rĂ©e puis stockĂ©e dans le stockage local.

  • STEP 2/6: On change de rĂ©pertoire courant.

  • STEP 3/6: La copie du code est effectuĂ©e.

  • STEP 4/6: Les dĂ©pendances sont installĂ©es, avec tout le dĂ©tail de l’exĂ©cution de pip

  • STEP 5/6: La variable d’environnement est configurĂ©e

  • STEP 6/6: Le point d’entrĂ©e est configurĂ©.

On voit ensuite qu’une instruction appelĂ©e COMMIT est effectuĂ©e. C’est cette instruction qui enregistre le conteneur en cours de construction sous la forme d’une image de conteneur.

Puis l’indication Successfully tagged localhost/simplestblog:v1 nous informe que tout a fonctionnĂ©, et nous indique au passage le chemin de l’image produite.

Enfin la derniĂšre chose affichĂ©e est une longue chaĂźne de caractĂšres (6fb49e42be
) qui est l’identifiant unique de l’image construite.

4. Quelques bonnes pratiques pour créer des images de conteneurs

Pour clore ce chapitre, voyons certaines bonnes pratiques utiles pour créer des images de conteneurs.

4.1. CaractĂšre Ă©phĂ©mĂšre d’un conteneur

L’un des objectifs de la conteneurisation est de rendre aussi facile que possible le fait de dĂ©ployer un logiciel dans un environnement donnĂ©. Au delĂ  du simple dĂ©ploiement, on souhaite aussi rendre aussi indolore que possible le fait de mettre Ă  jour un logiciel dĂ©ployĂ© avec un conteneur, ou de migrer un logiciel dĂ©ployĂ© d’un environnement vers un autre.

Pour que tout cela soit favorisĂ©, une caractĂ©ristique recherchĂ©e chez un conteneur est sa capacitĂ© Ă  ĂȘtre Ă©phĂ©mĂšre. Par Ă©phĂ©mĂšre on signifie ici qu’un conteneur ne possĂšde pas d’Ă©tat interne (en anglais, stateless), et qu’il soit anodin de dĂ©truire un conteneur pour le reconstruire Ă  l’identique.

Exemple 9. Scénario avec un Simplestblog non éphémÚre

Imaginons un scĂ©nario avec Simplestblog :

  • On souhaite, toujours, exĂ©cuter Simplestblog au sein d’un conteneur. Simplestblog est une application web de blogging, qui doit donc mĂ©moriser les articles postĂ©s par chaque utilisateur. Supposons que Simplestblog mĂ©morise tous les articles créés au sein d’un unique fichier, et que ce fichier soit stockĂ© au sein-mĂȘme du conteneur.

  • On notera, sans surprise, que le comportement de l’application dĂ©pend de ce qui est mĂ©morisĂ© dans ce fichier : la liste des articles Ă  afficher dĂ©pend entiĂšrement de ce fichier.

  • On dĂ©cide de migrer note instance Simplestblog vers un autre environnement. On entreprend alors de dĂ©truire le conteneur, puis d’en crĂ©er un nouveau dans le nouvel environnement.

  • Malheureusement, on observe alors que tous les articles Ă©crits on Ă©tĂ© perdus, car le fichier qui les mĂ©morisait a Ă©tĂ© dĂ©truit en mĂȘme temps que le premier conteneur.

Le conteneur n’Ă©tait donc pas Ă©phĂ©mĂšre.

Il est par consĂ©quent important de garder deux choses Ă  l’esprit :

  1. Lors de l’exĂ©cution d’un logiciel conteneurisĂ©, les informations Ă  mĂ©moriser ne doivent jamais ĂȘtre gardĂ©es au sein-mĂȘme du conteneur. À la place, un logiciel devant mĂ©moriser des informations devra pouvoir accĂ©der Ă  l’extĂ©rieur du conteneur qui le contient, que ce soit avec des montages de fichiers ou rĂ©pertoires (pour stocker les donnĂ©es au sein de l’environnement d’accueil), ou bien au travers d’un rĂ©seau virtuel (par exemple pour se connecter Ă  une base de donnĂ©es).

  2. Lors de l’Ă©criture d’un Containerfile, on prendra soin de bien rĂ©flĂ©chir Ă  la maniĂšre qu’aura notre logiciel conteneurisĂ© de mĂ©moriser des informations (ni cela est nĂ©cessaire), et on prendra soin d’Ă©crire une documentation prĂ©cise expliquant quelles prĂ©cautions doivent ĂȘtre prises pour qu’un conteneur créé Ă  partir de l’image soit bien Ă©phĂ©mĂšre.

4.2. Une image de conteneur, une seule responsabilité

Un principe souvent rĂ©pĂ©tĂ© en conception logicielle est celui de la sĂ©paration des responsabilitĂ©s (ou sĂ©paration des prĂ©occupations). Ce principe Ă©tablit qu’il est bĂ©nĂ©fique de segmenter un programme informatique en plusieurs parties, afin que chacune des partie traite sĂ©parĂ©ment d’un problĂšme particulier. Ce dĂ©coupage peut s’opĂ©rer en divisant du code en plusieurs fichiers, fonctions, classes, composants, ou mĂȘme diffĂ©rents logiciels.

Ce principe vaut Ă©galement pour la conception d’images de conteneurs, avec l’idĂ©e qu'il est prĂ©fĂ©rable qu’une image se concentre sur une seule responsabilitĂ©.

En effet, on peut ĂȘtre tentĂ© de mettre dans un unique conteneur tout le nĂ©cessaire pour rĂ©pondre au problĂšme que l’on souhaite rĂ©soudre, y compris par exemple plusieurs logiciels devant ĂȘtre exĂ©cutĂ©s en parallĂšle. Cependant, faire cela va rendre plus complexe diffĂ©rentes tĂąches :

  • La prĂ©paration de l’environnement sera plus difficile, car il faudra satisfaire plusieurs prĂ©occupations Ă  la fois.

  • Pour mettre Ă  jour un seul des logiciels conteneurisĂ©s, il sera nĂ©cessaire de prĂ©parer une nouvelle image (et donc possiblement un nouveau Containerfile) et de recrĂ©er un nouveau conteneur, ce qui affectera tous les logiciels conteneurisĂ©s.

  • Par essence, une image ne possĂšde qu’un seul point d’entrĂ©e, qui prend la forme d’une unique commande exĂ©cutĂ©e au dĂ©marrage du conteneur. Si on souhaite exĂ©cuter plusieurs logiciels Ă  l’aide d’un seul point d’entrĂ©e, il faut prĂ©parer un point d’entrĂ©e spĂ©cial − par exemple Ă  l’aide d’un script − qui aura pour charge de dĂ©marrer deux processus dans le conteneur au lieu d’un seul.

Exemple 10. Scénario avec un Simplestblog qui combine les responsabilités

Imaginons que nous souhaitons que Simplestblog puisse fonctionner avec un unique conteneur. Pour rappel, Simplestblog nĂ©cessite un systĂšme de gestion de base de donnĂ©es compatible MySQL pour fonctionner − on peut par exemple utiliser le logiciel MariaDB pour cela.

On dĂ©cide donc de mettre dans le mĂȘme conteneur Ă  la fois Simplestblog et MariaDB. Mais plusieurs problĂšmes vont alors surgir :

  • L’environnement au sein du conteneur devra satisfaire Ă  la fois Simplestblog et le MariaDB. Si les deux ont des besoins qui rentrent en conflit (par exemple chacun nĂ©cessite une version diffĂ©rente d’une mĂȘme bibliothĂšque), alors cela va complexifier fortement la crĂ©ation du conteneur.

  • Si on souhaite mettre Ă  jour Simplestblog, on doit prĂ©parer une nouvelle image et recrĂ©er un nouveau conteneur. Cela va affecter MariaDB alors qu’il n’Ă©tait pas concernĂ© par cette mise Ă  jour. Le mĂȘme problĂšme se produit Ă  l’inverse si on veut mettre Ă  jour MariaDB.

  • Si on souhaite a voir Ă  la fois Simplestblog et MariaDB simultanĂ©ment en exĂ©cution, il nous faut Ă©crire un script qui lance les deux logiciels en parallĂšle.

Pour Ă©viter tous ces problĂšmes, il est prĂ©fĂ©rable de prĂ©parer une image de conteneur avec juste Simplestblog. Puis, lors du dĂ©ploiement, on crĂ©e deux conteneurs − un premier avec Simplestblog, un second avec MariaDB − reliĂ©s entre eux par un rĂ©seau virtuel.

4.3. Limiter la taille d’une image

Une image de conteneur contient un mini-environnement qui peut aller jusqu’Ă  inclure les bases d’un vĂ©ritable systĂšme d’exploitation. En outre, l’image contient toutes les dĂ©pendances du logiciel qu’on souhaite conteneuriser, Ă  savoir d’autre logiciels ou bibliothĂšques.

Tout cela mis bout Ă  bout, on peut facilement se retrouver Ă  construire des images d’une taille assez importante.

4.3.1. Bien choisir l’image de base, et ne garder que le nĂ©cessaire

Comme dĂ©jĂ  expliquĂ©, le premier moyen pour limiter la taille d’une image est de bien sĂ©lectionner une image de base aussi lĂ©gĂšre que possible. Pour cela, on peut privilĂ©gier les images avec des mots-clĂ©s comme slim ou minimal, et on peut privilĂ©gier toutes les images directement ou indirectement basĂ©es sur l’image Alpine Linux.

Exemple 11. Taille de Simplestblog en fonction de l’image de base

Dans l'Exemple 8, on a construit une image Ă  l’aide de l’image de base docker.io/python:3-slim. Cette image propose un environnement Python minimal, avec aussi peu que possible d’outils additionnels de prĂ©sents. Avec cette image de base, la taille de l’image rĂ©sultante est de 278 MB.

Pour la comparaison, on peut rĂ©-exĂ©cuter le mĂȘme Containerfile mais avec comme image de base docker.io/python:3 (sans le suffixe slim). L’image en question contient un environnement Python beaucoup plus riche, et par consĂ©quent l’image rĂ©sultante a pour taille 1.17 GB.

Étant donnĂ© que Simplestblog se satisfait trĂšs bien de l’environnement slim, il n’y a pas Ă  hĂ©siter !

Un deuxiĂšme moyen simple pour limiter la taille d’une image est d’ĂȘtre vigilant lors de l’Ă©criture du Containerfile. Il faut par exemple Ă©viter de copier des donnĂ©es inutiles dans le conteneur par mĂ©garde, et Ă©viter d’installer du contenu superflu. Juste du bon sens, en somme !

4.3.2. Bien maĂźtriser la reprĂ©sentation "en couches" d’une image

Il n’est pas demandĂ© de maĂźtriser cette partie, qui est prĂ©sentĂ©e juste pour votre curiositĂ©.

Pour aller plus loin en ce qui concerne l’optimisation de la taille d’une image de conteneur, deux notions jusqu’ici mises de cĂŽtĂ© sont nĂ©cessaires.

Tout d’abord, il est nĂ©cessaire de comprendre plus prĂ©cisĂ©ment de quelle maniĂšre les donnĂ©es que contient une image sont stockĂ©es. Chaque image est constituĂ©e d’une liste de couches, et chaque couche contient uniquement le delta des changements rĂ©alisĂ©s par rapport Ă  la couche infĂ©rieure. La premiĂšre couche est dĂ©finie par l’image de base, puis une nouvelle couche est construite Ă  chaque fois qu’une instruction de type COPY ou RUN du Containerfile est exĂ©cutĂ©e. Par exemple, lors de l’exĂ©cution d’un ADD, on construit une nouvelle couche qui ne contient que les donnĂ©es ajoutĂ©es lors du ADD, et cette couche est stockĂ©e par dessus la couche infĂ©rieure existante.

Ce mĂ©canisme permet en limiter Ă©normĂ©ment l’espace rĂ©el requis pour manipuler des images au sein d’un stockage local, car plusieurs images diffĂ©rentes peuvent partager les couches identiques qu’elles possĂšdent. Principalement, ce mĂ©canisme permet Ă  une image de base d’ĂȘtre partagĂ©e, comme couche premiĂšre, auprĂšs de toutes les images stockĂ©es localement qui reposent sur cette image de base.

Cependant, cette stratĂ©gie de stockage entraĂźne Ă©galement le comportement peu intuitif suivant. Supposons qu’une commande RUN d’un Containerfile a pour but de supprimer des donnĂ©es, par une dĂ©sinstallation de paquets ou par un usage de la commande rm. On pourrait croire que cela va rĂ©duire taille de l’image, puisque des donnĂ©es sont retirĂ©es de son systĂšme de fichiers. Cependant, cela n’a aucun effet sur la taille finale, car ce RUN va simplement entraĂźner la crĂ©ation d’une nouvelle couche par dessus les couches infĂ©rieures. La taille occupĂ©e par les couches infĂ©rieure ne changeant pas, la taille de l’image ne rĂ©duit pas !

Mais comment faire alors pour supprimer des fichiers d’une image ? La seule maniĂšre, quand cela est possible, est de crĂ©er les fichiers et les supprimer au sein de la mĂȘme commande RUN.

Exemple 12. Supprimer le cache aprĂšs l’installation d’un paquet

On souhaite prĂ©parer une image avec pour base un environnement de type Ubuntu, et offrant la commande git. On souhaite Ă©galement prendre soin de rĂ©duire la taille prise par l’image en supprimant tout le cache tĂ©lĂ©chargĂ© par apt pour l’installation de git. Ce cache est toujours situĂ© Ă  l’emplacement /var/lib/apt/lists/.

On prĂ©pare donc le Containerfile suivant :

FROM docker.io/ubuntu:23.04

RUN apt update
RUN apt install -y git
RUN rm -rf /var/lib/apt/lists/*

On exĂ©cute ensuite podman image build, et tout fonctionne comme prĂ©vu. La taille de l’image produite est de 202 MB.

Cependant, la troisiĂšme instruction RUN est complĂštement inutile pour rĂ©duire la taille de l’image. En effet, lorsqu’elle est exĂ©cutĂ©e, cette instruction va simplement crĂ©er une nouvelle couche qui s’ajoutera aux couches prĂ©cĂ©dentes. Or, le rĂ©pertoire /var/lib/apt/lists/ a Ă©tĂ© rempli suite Ă  l’exĂ©cution des deux RUN prĂ©cĂ©dents. La taille occupĂ©e par /var/lib/apt/lists/ sera donc toujours impactante sur l’image finale construite.

Pour rĂ©soudre cela, on Ă©crit le nouveau Containerfile suivant :

FROM docker.io/ubuntu:23.04

RUN apt update && apt install -y git \
                   && rm -rf /var/lib/apt/lists/*

Cette fois-ci on prend soin de tout faire au sein de la mĂȘme commande RUN, en utilisant l’opĂ©rateur && pour crĂ©er une sĂ©quence de commandes. Comme une seule instruction RUN est prĂ©sente, une seule couche est construite par dessus l’image de base. Or, cette instruction RUN supprime bien le rĂ©pertoire /var/lib/apt/lists/ Ă  sa toute fin, ce rĂ©pertoire n’est donc jamais stockĂ© dans la couche construite. Ainsi, la taille de l’image produite est de 161 MB.

5. Références