Docker Init : Initialiser Docker en Moins d'Une Minute !

Docker Init : Initialiser Docker en Moins d'Une Minute !

Docker a récemment ajouté une nouvelle commande permettant d'initialiser Docker au sein d'un projet en quelques secondes. Utile ? Ou pas utile ?

·

7 min read

Aujourd'hui, Docker est indéniablement la plateforme de conteneurisation la plus populaire (désolé Podman et LXD). Les développeurs y sont de plus en plus familiarisés, mais il peut être pénible d'avoir à réécrire un Dockerfile et un docker-compose.yaml à la création de chaque projet (notamment avec la multiplication des micro-services). Même s'il est possible de réutiliser ces fichiers entre les différents services, certains de ces derniers peuvent avoir des particularités qui rendent ce partage impossible (par exemple, s'ils sont utilisent des langages différents).

Il y a quelques mois, Docker a ajouté une nouvelle commande à sa CLI permettant d'initialiser un environnement Docker au sein d'un projet en moins d'une minute (littéralement). Regardons ensemble comment utiliser cette nouvelle commande, et analysons les différents fichiers qu'elle génère. Docker init : la baguette magique

Cette commande est disponible en beta depuis la version 4.18.0 de Docker Desktop (4.19.0 selon l'article de blog 🤔), et elle est très simple d'utilisation :

docker init

Plusieurs questions vont alors vous être posées, comme le langage à utiliser, la version de ce dernier, le dossier dans lequel trouver les fichiers à copier, la commande à lancer pour démarrer l'application, etc.

💡
Ces questions vont dépendre du langage utilisé. Par exemple, dans le cas de Node, il vous sera demandé la version à utiliser, le nom du gestionnaire de paquets (yarn, npm ou pnpm), la commande de démarrage et le port d'écoute.

On peut voir que 3 fichiers sont alors créés : .dockerignore, Dockerfile et compose.yaml. Regardons de plus près ce qu'ils contiennent et analysons leur pertinence.

.dockerignore

Ce fichier permet d'indiquer à Docker les dossiers et fichiers qu'il n'a pas le droit de copier vers l'image finale avec les instructions ADD et COPY du Dockerfile en les excluant du contexte. Il a notamment pour but d'éviter d'encombrer l'image de fichiers inutiles pour limiter sa taille, et surtout d'éviter d'y inclure des fichiers sensibles (coucou .env). C'est ce qui arrive souvent lorsque l'on utilise l'instruction COPY . . pour littéralement tout copier vers l'image.

💡
Cela permet également de réduire le temps de build. Par exemple, dans le cas d'un projet Node, si ce fichier n'existe pas, le dossier node_modules (autrement dit la terre entière) est inclus dans le contexte qui est envoyé au démon Docker. Ce transfert peut prendre plusieurs secondes.

Si l'on utilise Node avec Yarn comme package manager, voici ce que contient le fichier .dockerignore :

Comme prévu, il contient bien les types de fichiers qui n'ont pas leur place dans l'image finale : cache, logs, documentation, dossiers d'IDEs, dossier git, etc.

😳
Si vous aussi vous venez de découvrir l'extension .jfm, lachez un like à cet article.

D'ailleurs, il contient même les fichiers relatifs à Docker (Dockerfile, compose.yaml, et même .dockerignore). Ce qui est logique, car à quoi bon les envoyer vers l'image finale ?

Le fichier est donc assez complet et couvre à priori 80% des fichiers et dossiers que l'on souhaite ignorer. Il contient même quelques commentaires expliquant son contenu, avec un lien vers de la documentation.

D'un point de vue personnel, je préfère cependant faire de l'exclusion par défaut pour ignorer les fichiers. Je m'explique : plutôt que de lister un à un les motifs à ignorer, je préfère tout ignorer et empêcher la copie de tous les fichiers vers l'image, puis ajouter un par un les motifs de dossiers et fichiers que je veux copier. Par exemple, voici le contenu d'un fichier .dockerignore sur l'un de mes projets Node :

*
!dist

J'exclus tous les fichiers par défaut, sauf le dossier dist qui contient les fichiers finaux de l'application (générés avec la commande npm run build).

Selon moi, cette approche est plus judicieuse dans la quasi-totalité des cas puisqu'elle permet d'éviter de copier par erreur des fichiers que l'on aurait oublié de spécifier. Par exemple, si je crée un fichier .env.dev pour stocker les variables sensibles de l'environnement partagé de développement, il ne sera pas ignoré par le fichier .dockerignore généré par docker init.

Dockerfile

Vous l'attendiez tous, c'est le fichier le plus important des trois : celui qui permet de construire l'image de notre application.

Toujours dans le cas de notre exemple en Node avec Yarn, voici le contenu du fichier Dockerfile généré par docker init :

Dockerfile généré par docker init

Je le trouve dans l'ensemble très propre et optimisé :

  • Utilisation d'un argument de build NODE_VERSION pour pouvoir builder sur une autre version de Node sans réécrire le Dockerfile.

  • Utilisation d'une image alpine par défaut, car pourquoi utiliser une image classique si on n'en a pas besoin ?

  • Distinction entre les fichiers de dépendances et le code source + utilisation de montage de type cache dans le but d'optimiser le temps de build en tirant profit des layers.

  • Utilisation de l'instruction USER pour ne pas faire tourner le conteneur en root.

Toutefois, je me questionne sur la syntaxe utilisée pour l'instruction CMD. En effet, dans la grande majorité des cas, il est recommandé de passer par la syntaxe exec (CMD ["yarn", "start"]) plutôt que par la syntaxe shell (CMD yarn start).

Sans trop rentrer dans les détails, cela permet de stopper le conteneur de manière propre avec la commande docker stop. En effet, avec la syntaxe shell, la commande yarn start est transformée en /bin/sh -c "yarn start", ce qui a pour conséquence de la démarrer dans un processus enfant. Le processus principal du conteneur (celui dont le PID est 1) n'est donc pas yarn mais /bin/sh, ce qui impacte la transmission des signaux.

💡
Si ça vous intéresse de comprendre en détail le problème de cette approche, je vous invite à consulter l'article que j'ai dédié à ce sujet.

Revenons à nos moutons (et à notre Dockerfile). Dans le cas de notre projet Node, je ne vois pas de raison qui pourrait justifier l'utilisation de la syntaxe shell, et je ne la comprends donc pas. Un détail me direz-vous, mais qui a pourtant une importance majeure (pensez aux potentielles fonctions de clean-up qui sont censées s'exécuter à la terminaison du conteneur, comme la déconnexion de la base de données).

compose.yaml

Enfin, le dernier fichier que nous a généré la commande docker init est le fichier compose.yaml. Il permet de lancer une stack de plusieurs conteneurs avec la commande docker compose, comme par exemple notre application et sa base de données.

💡
Étonnement, ce fichier ne s'appelle pas docker-compose.yaml comme on a plutôt l'habitude de voir. En me renseignant sur la documentation Docker à propos du fichier compose, il semble que compose.yaml est désormais le nom préconisé, et que le nom docker-compose.yaml est supporté pour des raisons de rétrocompatibilité avec les versions antérieures.

Voici ce qu'on trouve donc dans le fichier compose.yaml (j'ai supprimé quelques commentaires par souci de lisibilité) :

Contenu du fichier compose.yaml

Globalement, rien d'exceptionnel : de quoi démarrer l'application, comme on l'aurait fait avec la commande docker run. Je ne vois pas trop pourquoi la variable d'environnement NODE_ENV est répétée (souvenez-vous, elle est déjà définie dans le Dockerfile), peut-être pour s'assurer que l'environnement depuis lequel on lance docker run ne vienne pas écraser cette variable avec une autre valeur.

Ceci dit, on trouve également un exemple pour démarrer une base de données Postgres dans un conteneur, utilisant un volume pour persister les données et un secret pour définir les variables d'environnement. La condition depends_on permet de s'assurer que notre application ne démarre que lorsque la commande pg_isready renvoie 0 (c'est-à-dire que la base de données est prête à recevoir des requêtes).

Verdict : utiliser, ou ne pas utiliser ?

Cette commande est selon moi assez utile pour créer les différents fichiers permettant de conteneuriser notre application, en particulier pour les novices de Docker. Elle est d'ailleurs simple à utiliser et très intuitive. Je trouve que les fichiers générés sont propres et définissent le strict minimum nécessaire pour démarrer avec un environnement Docker. Cela peut permettre d'éviter de copier/coller des exemples trouvés sur Stack Overflow ou GitHub qui ne respecteraient pas forcément les bonnes pratiques de sécurité et d'optimisation des layers.

Bien sûr, docker init est prévue pour être générique, non pas pour générer automatiquement ces fichiers pour des projets complexes avec des cas particuliers. Il n'existe toujours pas de solution magique, néanmoins si des fonctionnalités spécifiques sont nécessaires, les équipes pourront se référer à la documentation de Docker pour ajuster les fichiers au besoin.

Elle me semble être un bon point de départ pour tout projet qui n'utilise pas encore Docker. À titre personnel, je ne pense pas m'en servir au quotidien, mais il pourrait m'arriver d'en avoir besoin si l'on me demande de conteneuriser une application dans un langage qui m'est inconnu comme Rust ou ASP.NET.


J'espère vous avoir appris des choses au travers de cet article, et si vous avez des questions ou des remarques, n'hésitez-pas à vous exprimer dans l'espace commentaires ou à me contacter ! 😉

Ressources

Did you find this article valuable?

Support Ludal by becoming a sponsor. Any amount is appreciated!