8. Optimiser son image - Docker

Dans le cours précédent, nous avons vu comment construire une image Docker mais sans utiliser les bonnes pratiques pour optimiser une image Docker ! En effet, l'image que nous avons construite ensemble n'est pas optimisée du tout et nous allons voir dans ce cours comment optimiser le build d'une image Docker.

Optimiser les layers

Dans une image Docker, il faut voir les layers comme des couches qui s'empilent.
Chaque nouveau layer contient les modifications qui seront effectuées dans le Filesystem pour obtenir l'image finale.

Chaque layer sera également mis en cache par Docker afin d’accélérer le prochain build de l'image et ainsi ne pas répéter les mêmes actions nécessaires au build.

Vous comprenez l'importance de mesurer le nombre d'instructions présentes dans un Dockerfile ?

La philosophie qu'a amené Docker est la légèreté. Il faut donc à chaque build d'une image, se poser les questions suivantes :

  • Est-ce que mon image n'est pas trop lourde ? Contient-elle uniquement ce qu'il y a d'essentiel ?
  • Les layers sont-ils optimisés afin de gagner en espace de stockage ?
  • Suis-je exposé à de nombreuses des failles de sécurité à cause du nombre d'outils présents dans mon image Docker ?

Si l'on reprend notre Dockerfile suivant, regardons ce qu'il est possible d'optimiser :

FROM python

RUN pip install requests

RUN useradd -d /home/python-user -m -s /bin/bash python-user

USER python-user

WORKDIR /home/python-user    

COPY main.py /home/python-user    

CMD ["main.py"]

ENTRYPOINT ["python3"]

Nous pouvons voir que nous avons deux instructions RUN. Utilisons l'opérateur Bash && afin de n'avoir qu'un seul layer. Ainsi, si la première commande tombe en erreur, la deuxième commande ne s'exécutera pas et le build sera en échec. Vous devrez avoir un Dockerfile comme celui-ci :

FROM python

RUN pip install requests && \
    useradd -d /home/python-user -m -s /bin/bash python-user

USER python-user

WORKDIR /home/python-user    

COPY main.py /home/python-user    

CMD ["main.py"]

ENTRYPOINT ["python3"]

Si vous construisez cette image avec la commande docker build, vous gagnerez en espace de stockage et son build sera plus rapide. Cela représentera une petite quantité car la commande useradd n'est pas lourde, mais si cela aurait été l'installation de paquets via la commande apt par exemple, il aurait été possible de gagner plusieurs centaines de MB !

Limiter l'espace de stockage

Il est aussi possible de limiter l'espace disque consommé par une image Docker avec les arguments des commandes exécutées. Dans notre exemple, la commande pip dispose de l'argument --no-cache-dir qui désactive le cache. Un autre exemple est pour la commande apt lorsque vous installez des paquets, il est possible de supprimer le cache comme ceci :

rm -rf /var/cache/apt/archives /var/lib/apt/lists

Il est aussi possible de limiter la quantité de paquets à installer avec l'argument --no-install-recommends de la commande apt install par exemple.

Il existe un moyen de consulter l'espace disque utilisé par chaque layer ainsi que tout ce qui est exécuté pour l'image finale avec la commande suivante :

docker history

Le cache

Comme dit plus haut, à chaque build, Docker mets en cache chacun des layers.
Si vous construisez deux fois la même image, le deuxième build sera quasiment instantané. Il est possible de désactiver le cache de build avec l'argument --no-cache de la commande docker build.

La position des layers

Si vous souhaitez ajouter une nouvelle instruction à votre image Docker et que cette instruction sera amenée à être modifiée fréquemment, il est plus intéressant de positionner cette instruction au plus bas de votre Dockerfile. En effet, lorsque le build d'un Dockerfile arrive sur un layer qui n'est pas en cache, tous les autres layers plus bas n'utiliseront pas le cache.

Exemple : Si vous avez l'instruction ARG dans votre Dockerfile afin de cibler la version du module Python à installer comme ceci :

ARG requests_version

RUN pip install requests==${requests_version}
docker build --build-arg requests_version=2.28.2

Comme la variable ${pip_version} sera amenée à être modifiée à chaque version du module Python requests, l'idéal serait de positionner ces 2 instructions au plus bas possible de votre Dockerfile pour que les instructions plus haut qui sont mises en cache s'exécutent rapidement, et que celles-ci s'exécutent à la suite.

Les bases

Il est aussi possible d'utiliser des bases d'images Docker plus légères. En effet, certaines distributions Linux sont plus légères car elles contiennent moins de paquets installés. Plutôt que d'utiliser l'image Python qui est sur une base debian, il est possible d'utiliser une base alpine en modifiant le tag comme ceci :

FROM python:alpine

Ainsi, uniquement en modifiant la base de l'image, il est possible de gagner beaucoup d'espace disque.

Nous n'allons pas faire cela dans notre exemple car changer de base implique certaines modifications, comme le changement de Shell car Alpine ne dispose pas de Bash par défaut.

Il est possible d'aller encore plus loin dans l'optimisation du build d'images Docker avec notamment le fichier .dockerignore ou encore le build multi-stage mais nous n'allons pas aller plus loin dans ce cours.

Vous pouvez consulter l'article que j'ai rédigé sur le build multi-stage d'images Docker ici :

Comment effectuer du build multi-stage avec Docker ?
Le principe Le build multi-stage de Docker est une fonctionnalité qui permet de créer des images Docker plus légères et permettant d’effectuer des tests unitaires plus fins en utilisant plusieurs étapes de build. C’est une fonctionnalité qui permet de construire une image Docker à partir de plusie…

Nous aborderons dans le cours suivant comment partager et stocker son image Docker avec les Registrys.