object.title
5 bonnes pratiques pour construire des fichiers Dockerfile efficaces

Par Jordan A. - Expert DevOps

Durant la DockerCon 2021 qui a eu lieu le 27 mai 2021, nous avons pu assister à un certain nombre de conférences très intéressantes. L’une d’entre elle a retenu notre attention, car elle présente des concepts de base pour la rédaction de fichiers Dockerfile et ainsi créer des conteneurs efficaces et efficients.

Cette conférence a été réalisée par Aaron Kalin, Technical Evangelist chez Datadog : « Lessons Learned With Dockerfiles and Docker Builds » et propose 7 leçons à retenir, que je vais vous détailler et reprendre à l’aide d’exemples concrets.

 

Lesson 1 : Attention à l’image de base que vous utilisez

Les images alpines ont beaucoup de succès ces dernières années, du fait de leur faible taille et du nombre restreint de vulnérabilités. Il s’agit donc d’une base idéale pour construire votre propre image Docker ?

Oui mais… À force d’utilisation, les images alpines ne font plus du tout l’unanimité auprès des développeurs. Une des premières problématiques, concerne l’utilisation de musl plutôt que glibc (alors que les distributions les plus populaires utilisent plutôt glibc). Cela signifie que les éléments qui seront compilés sur les distributions alpines, peuvent ne pas être utilisables sur Ubuntu (et vice versa).

De plus, quid des paquets qui ne sont pas encore disponibles sur Alpine alors qu’ils le sont sur d’autres distributions, et indispensables pour traiter les dépendances de votre code ?

Aaron Kalin nous invite à utiliser plutôt les images dans des versions « slim » qui sont de tailles réduites, parfois assez proches des tailles des alpines comme ici :

$ docker image ls | grep python
python     3.9.1-slim-buster   8c84baace4b3    3 months ago   114MB
python     3.7.4-alpine3.9     32a1b98d0495   19 months ago   98.5MB

Lesson 2 : Enchaînez vos commandes RUN

Le principe d’enchaîner vos commandes RUN pour l’installation des dépendances permet d’avoir qu’une seule couche de créée (car pour chaque commande dans le fichier Dockerfile, une nouvelle couche est créée) pour vos dépendances.

Aaron Kalin conseille également d’organiser les noms de paquets à installer par ordre alphabétique avec un seul paquet par ligne (plus facile à maintenir et à réorganiser).

Par exemple, prenons un fichier Dockerfile avec les paquets à installer sur une seule ligne :

FROM ubuntu:bionic

RUN apt-get update && apt-get install -y --no-install-recommends \
    curl \
    git \
    nginx \
    python

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]

On obtient l’history suivant :

$ docker history docker_1_layer:latest 
IMAGE               CREATED         CREATED BY                                      SIZE                COMMENT
6892a0a503de        18 seconds ago  /bin/sh -c #(nop)     CMD 
["nginx" "-g" "daemon…   0B                  
374bdfdad2b2        18 seconds ago  /bin/sh -c #(nop)     EXPOSE 
80                    0B                  
3f7201caacaa        20 seconds ago  /bin/sh -c apt-get update
&& apt-get install…   189MB               
81bcf752ac3d        8 days ago      /bin/sh -c #(nop)     CMD ["/bin/bash"]            0B                  
<missing>           8 days ago      /bin/sh -c mkdir -p 
/run/systemd && echo 'do…   7B                  
<missing>           8 days ago      /bin/sh -c [ -z "$(apt-get indextargets)" ]     0B                  
<missing>           8 days ago      /bin/sh -c set -xe    && 
echo '#!/bin/sh' > /…   745B                
<missing>           8 days ago    /bin/sh -c #(nop)       ADD file:e05689b5b0d51a231…   63.1MB  

Maintenant, effectuons la même expérience avec les éléments dispatchés ligne par ligne dans son fichier Dockerfile :

FROM ubuntu:bionic

RUN apt-get update && apt-get install -y --no-install-recommends curl
RUN apt-get install -y git
RUN apt-get install -y nginx
RUN apt-get install -y python3

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]

On obtient alors l’history suivant :

IMAGE               CREATED            CREATED BY                       SIZE                COMMENT
0d25db122b31        16 seconds ago     /bin/sh -c #(nop)   CMD
["nginx" "-g" "daemon…   0B                  
3cf4fb051b11        17 seconds ago     /bin/sh -c #(nop)   EXPOSE
80                    0B                  
f736c0e7e9e6        18 seconds ago     /bin/sh -c apt-get  install
-y python3           29.4MB              
c6c35fc73cad        28 seconds ago     /bin/sh -c apt-get  install
-y nginx             53.3MB              
53e8b93b739a        39 seconds ago     /bin/sh -c apt-get  install
-y git               83.4MB              
57e76bf1ae81        52 seconds ago     /bin/sh -c apt-get  update
&& apt-get install…   48.9MB              
81bcf752ac3d        8 days ago         /bin/sh -c #(nop)   CMD
["/bin/bash"]        0B                  
<missing>           8 days ago         /bin/sh -c mkdir -p
/run/systemd && echo 'do…   7B                  
<missing>           8 days ago         /bin/sh -c [ -z "$(apt-get indextargets)" ]     0B                  
<missing>           8 days ago         /bin/sh -c set -xe  && 
echo '#!/bin/sh' > /…   745B                
<missing>           8 days ago         /bin/sh -c #(nop)   ADD file:e05689b5b0d51a231…   63.1MB

On obtient deux images qui ont des tailles différentes, et un niveau de complexité plus important pour la deuxième :

$ docker image ls | grep docker
docker_4_layers            latest              0d25db122b31
About a minute ago   278MB
docker_1_layer             latest              6892a0a503de
4 minutes ago        252MB
$

Lesson 3 : Clean après l’installation de paquets

Reprenons notre exemple suivant :

FROM ubuntu:bionic

RUN apt-get update && apt-get install -y --no-install-recommends \
    curl \
    git \
    nginx \
    python

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]

Ici, après avoir installé les paquets grâce à apt, nous n’avons procédé à aucun clear. Cependant, pour réduire encore la taille de l’image, et donc son délai de build et de chargement, on peut ajouter les commandes suivantes :

rm -rf /var/lib/apt/lists/* && apt clean

Cela nous donnerait donc :

FROM ubuntu:bionic

RUN apt-get update && apt-get install -y --no-install-recommends \
    curl \
    git \
    nginx \
    python \
    && rm -rf /var/lib/apt/lists/* \
    && apt clean

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]

On peut ainsi comparer la taille de l’image sans avoir effectué le clean : docker_1_layer, et de l’image ayant effectuée le clean : docker_1_layer_clean :

$ docker image ls | grep docker
docker_1_layer_clean       latest              494fb62a6e8c        
16 seconds ago      216MB
docker_4_layers            latest              0d25db122b31        
7 minutes ago       278MB
docker_1_layer             latest              6892a0a503de        
10 minutes ago      252MB

On voit donc que l’image où le clean a été effectué, est de taille réduite par rapport à l’image où le clean n’a pas été fait. On a donc encore réussi à réduire la taille de notre image.

Lesson 4 : Lancer l’installation des dépendances applicatives séparément à la fin du Dockerfile

En effet, comme ces dépendances sont amenées à changer de temps en temps avec l’évolution de votre code, il convient de les faire figurer en priorité dans les parties les plus basses du Dockerfile. Ainsi, on évite de reconstruire toutes les couches suivantes en cas de modifications.

On n’oublie pas non plus de spécifier à l’outil de ne pas conserver de données en cache (un peu comme pour apt).

Voici un exemple pour l’installation de librairies Python :

RUN pip install --no-cache-dir -r requirements.txt

Lesson 5 : Ne pas oublier d’utiliser le .dockerignore

Aaron Kalin nous rappelle à très juste titre, d’utiliser le fichier .dockerignore de manière intelligente. En effet, il permet d’exclure des répertoires et des fichiers de toutes copies qui pourraient être effectuées à l’intérieur de l’image Docker.

Parmi les fichiers et répertoires que l’on oublie souvent de ne pas inclure figurent en pôle position : .git

En effet, si vos fichiers liés à votre code sont versionnés grâce à l’outil Git, vous avez nécessairement un répertoire caché .git qui est créé à l’intérieur de votre répertoire de travail.

Quel dommage qu’il soit téléchargé à l’intérieur de votre image docker ?!

D’autres fichiers que l’on a tendance à oublier correspondent à tous les fichiers Dockerfile dans notre répertoire de travail.

Voici donc à quoi ressemblerait notre fichier .dockerignore :

.git
Dockerfile*

Les « lessons » 6 et 7 seront traitées dans le prochain article. Elles concernent l’utilisation de la construction des images par la fonctionnalité de multi-stage et l’utilisation des labels.