{{indexmenu_n>30}} # Bonnes pratiques pour les Dockerfiles On pourra se référer à la [documentation officielle sur les bonnes pratiques](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/). ## Import d'images stables Lors de l'import d'une image dans un `Dockerfile` à l'aide de la directive ''FROM'', il est important de spécifier la version de l'image importée ; sinon la dernière version (//latest//) est importée, ce qui peut poser des problèmes de stabilité. ## Réaliser des images minimales Chaque élément d'une image peut être un facteur de vulnérabilité ; même un paquet en apparence inoffensif peut dépendre d'une librairie contenant une vulnérabilité. Pour cette raison, il convient de réaliser des images aussi légères que possibles en évitant d'installer des paquets superflus ou en supprimant les paquets installés pour un besoin temporaire (par exemple pour compiler un programme) une fois qu'ils sont obsolètes. Préférez vous baser sur une image Alpine (peu de paquets, donc peu de vulnérabilités) ou Debian. Pour Debian, préférez la version stable la plus récente possible (Buster plutôt que Stretch) pour mitiger d'emblée un grand nombre de vulnérabilités. Préférez également les versions `slim`, qui contiennent moins de paquets et suffisent souvent. Pour les services basés sur un langage interprété comme Python, utilisez l'image officielle en appliquant les principes précédents. Il peut être intéressant d'avoir recours à des [[https://docs.docker.com/develop/develop-images/multistage-build/|multi-stage builds]]. L'idée est par exemple d'utiliser une image pour la compilation, et une image qui recevra uniquement l'exécutable et les dépendances nécessaires à l'exécution. Cette dernière sera utilisée en production. ## Rendre les build reproductibles L'idée derrière un build "reproductible", c'est que si je lance le build d'un même `Dockerfile` à deux moments différents, l'image finale doit être la même quel que soit le temps écoulé entre les deux. Ce ne sont pas de **vrais** builds reproductibles, car la version des paquets installés peut avoir changé, par exemple. Mais l'idée est d'essayer de s'en rapprocher. Pourquoi des builds reproductibles ? Pour pouvoir relancer le build d'une ancienne version et la remettre en production en cas de problème. Si on a l'ancien `Dockerfile`, alors on devrait pouvoir avoir quasiment la même ancienne image. Souvent, un `Dockerfile` va récupérer du code sur un dépôt Git, ou encore un binaire sur un site de téléchargement de releases. Il est fortement déconseillé de faire un `git clone` ou un `wget` de la dernière version (`latest`, `master`...), ce qui rend le build non reproductible et dépendant de cette dernière version. Il est donc important de gérer la version du service en question, par exemple avec une variable `ARG SERVICE_VERSION=1.0.1` qui sera ré-utilisée dans l'URL de téléchargement. En outre il existe deux solutions pour récupérer du code existant, versionné sur un dépôt Git distant : * Utiliser un `wget` sur une release particulière (Gitlab, Github), permettant de récupérer une archive contenant le code source d'une version spécifique. * Installer Git dans le `Dockerfile`, utiliser un `git clone` puis un `git checkout ` sur la version souhaitée et copier le code dans l'image. En général, on préférera télécharger une release du code que d'utiliser Git. ## Exécution de commandes au démarrage Pour lancer directement le binaire d'un service au démarrage, on utilisera la directive [CMD](https://docs.docker.com/engine/reference/builder/#cmd) avec la forme tableau (`[ "commande", "arg1", "arg2" ]`). En effet, cette forme permet de démarrer le processus directement, tandis que la forme sans tableau est exécutée par `/bin/sh`, ce qui pose des problèmes ([explications ici](https://engineeringblog.yelp.com/2016/01/dumb-init-an-init-for-docker.html)). Lorsque l'on a besoin d'exécuter des instructions préalables (injection de secrets, test du contact avec la base de données, etc...), on utilisera la directive [ENTRYPOINT](https://docs.docker.com/engine/reference/builder/#entrypoint) en conjonction avec `CMD`, toujours sous la forme tableau. En général, l'`entrypoint` sera un script shell, qui recevra en argument le tableau passé à `CMD`. ## Installation de paquets par ordre alphabétique Pour éviter d'installer deux fois un paquet, lors de l'installation de plusieurs paquets en une instruction, il convient de les installer dans l'ordre alphabétique. ## Réduction du nombre de couches Un `Dockerfile` est composé de différentes couches (*layers*). Une couche correspond à une instruction exécutée par le `Dockerfile`. Lorsqu'un conteneur est lancé, il va accéder aux commandes du conteneur en remontant les différents niveaux. À la manière de l'héritage en programmation, si un conteneur cherche a appeler une commande qu'il ne connaît pas, il va remonter à sa classe mère et ainsi de suite. Or, ces accès réduisent la vitesse d'exécution de la commande. Il faut donc veiller à réduire au maximum le nombre de couches d'un conteneur et privilégier les instructions avec plusieurs commandes. Les couches ont pour intérêt de pouvoir être partagées entre les conteneurs et utilisent un système de cache. Ainsi, la modification d'une instruction n'influencera pas les couches précédentes, qui n'auront pas besoin d'être reconstruites. Il faut donc trouver le juste milieu entre trop de couches et pas assez de couches. En général, une couche est une sorte d'unité sémantique. L'exemple qui suit décrit l'installation de dépendances, qui n'ont pas lieu d'être séparées en trois instructions : ```bash RUN echo "deb http://packages.dotdeb.org jessie all" > /etc/apt/sources.list.d/dotdeb.list && \ wget -O- https://www.dotdeb.org/dotdeb.gpg | apt-key add - && \ apt-get update -y && apt-get install -y php7.0 php7.0-fpm php7.0-gd php7.0-xml nginx supervisor curl tar ``` Dans cet exemple, il n'y a qu'une seule instruction `RUN`, donc une seule couche est créée. Par contre, la moindre modification de cette instruction entraînera son ré-exécution complète lors de la reconstruction de l'image. ## Contrôle de la santé d'un conteneur Les `HEALTHCHECK`, au sens Docker, permettent de vérifier qu'un conteneur est en "bonne santé", par exemple en lançant une commande de type `curl`. Souvent, les commandes comme `curl` et `wget` ne sont pas installées, il faut y penser. Référence pour les `HEALTHCHECK` : https://docs.docker.com/engine/reference/builder/#healthcheck ## Ne pas utiliser les directives VOLUME Lors de la rédaction d'images, on peut être tenté d'exposer des volumes sur des images "temporaires". Par exemple, une image `nginx` dans laquelle on va indiquer que `/var/www/html` doit être persistent grâce à la directive `VOLUME`. Le problème de cette pratique est que si l'on veut créer une image Dokuwiki basée sur cette image nginx, il ne sera pas possible d'écrire par dessus le volume ''/var/www/html''. Même si au sein du `Dockerfile` on spécifie des commandes pour remplacer le contenu de l'image, au final, le contenu du dossier restera celui de l'image mère. On évitera donc les directives `VOLUME` au sein des `Dockerfile`, et on spécifiera les volumes au lancement du conteneur. ## Spécifier un utilisateur lors de l'exécution de commandes Dans un `Dockerfile`, une commande exécutée par une directive ''RUN'' est exécutée par le compte root du conteneur par défaut. Il convient d'exécuter aussi peu de commandes que possible avec le compte root ; pour se faire, il est possible d'utiliser la directive ''USER''. Un conteneur lancé en tant que `root` pose des risques de sécurité en cas de compromission, bien plus que quand il est lancé en tant que simple utilisateur. Pour certains services, c'est difficile, mais il faut essayer de s'en rapprocher le plus possible. La dernière directive `USER` utilisée correspondra à l'utilisateur qui exécutera les commandes quand le conteneur sera lancé. Il est préférable de démarrer le conteneur avec un utilisateur non-privilégié. On s'assurera qu'il a bien les droits sur les fichiers qu'il doit utiliser, par exemple avec des instructions `chown` lors de la construction de l'image.