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 :
Nous aborderons dans le cours suivant comment partager et stocker son image Docker avec les Registrys.