# Docker Init : initialiser Docker en moins d'une minute !

Aujourd'hui, Docker est indéniablement la plateforme de conteneurisation la plus populaire (désolé [Podman](https://podman.io/) et [LXD](https://ubuntu.com/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](https://www.docker.com/products/docker-desktop/) (4.19.0 selon [l'article de blog](https://www.docker.com/blog/docker-init-initialize-dockerfiles-and-compose-files-with-a-single-cli-command/) 🤔), et elle est très simple d'utilisation :

```plaintext
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.

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Ces questions vont dépendre du langage utilisé. Par exemple, dans le cas de <a target="_blank" rel="noopener noreferrer nofollow" href="https://nodejs.org/en" style="pointer-events: none">Node</a>, il vous sera demandé la version à utiliser, le nom du gestionnaire de paquets (<a target="_blank" rel="noopener noreferrer nofollow" href="https://yarnpkg.com/" style="pointer-events: none">yarn</a>, <a target="_blank" rel="noopener noreferrer nofollow" href="https://www.npmjs.com/" style="pointer-events: none">npm</a> ou <a target="_blank" rel="noopener noreferrer nofollow" href="https://pnpm.io/" style="pointer-events: none">pnpm</a>), la commande de démarrage et le port d'écoute.</div>
</div>

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1694086763906/46330d0f-d0f1-4f2e-abe6-70cf73ec1dac.png align="center")

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.

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Cela permet également de réduire le temps de build. Par exemple, dans le cas d'un projet <code>Node</code>, si ce fichier n'existe pas, le dossier <code>node_modules</code> (autrement dit la terre entière) est inclus dans le contexte qui est envoyé au démon Docker. Ce transfert peut prendre plusieurs secondes.</div>
</div>

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

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1693948617063/b7431352-07b1-48c6-b6c9-efa6bed17476.png align="center")

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.

<div data-node-type="callout">
<div data-node-type="callout-emoji">😳</div>
<div data-node-type="callout-text">Si vous aussi vous venez de découvrir l'extension <code>.jfm</code>, lachez un like à cet article.</div>
</div>

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 :

```plaintext
*
!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](https://cdn.hashnode.com/res/hashnode/image/upload/v1693947470235/26bce82e-4b74-41de-b674-3dd1215de667.png align="center")

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.

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">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.</div>
</div>

%[https://blog.iamludal.fr/docker-shell-vs-exec-quelle-syntaxe-utiliser] 

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.

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Étonnement, ce fichier ne s'appelle pas <code>docker-compose.yaml</code> comme on a plutôt l'habitude de voir. En me renseignant sur <a target="_blank" rel="noopener noreferrer nofollow" href="https://docs.docker.com/compose/compose-file/03-compose-file/" style="pointer-events: none">la documentation Docker</a> à propos du fichier compose, il semble que <code>compose.yaml</code> est désormais le nom préconisé, et que le nom <code>docker-compose.yaml</code> est supporté pour des raisons de rétrocompatibilité avec les versions antérieures.</div>
</div>

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](https://cdn.hashnode.com/res/hashnode/image/upload/v1694085129963/8c20ab78-355c-4474-a9f5-f8f597995ce6.png align="center")

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](https://www.postgresql.org/) 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](https://stackoverflow.com/) ou [GitHub](https://github.com/) 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](https://www.rust-lang.org/) ou [ASP.NET](https://dotnet.microsoft.com/en-us/apps/aspnet).

---

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

* [https://www.docker.com/blog/docker-init-initialize-dockerfiles-and-compose-files-with-a-single-cli-command/](https://www.docker.com/blog/docker-init-initialize-dockerfiles-and-compose-files-with-a-single-cli-command/)
    
* [https://docs.docker.com/engine/reference/commandline/init/](https://docs.docker.com/engine/reference/commandline/init/)
