Docker : découverte des bases

Docker : découverte des bases

Docker est devenu un outil incontournable pour tous les développeurs ou les administrateurs d’infrastructure, mais celui-ci peut paraître complexe au premier abord. On se retrouve donc aujourd’hui pour tenter de clarifier tout ça avec ce premier article de cette série consacré à Docker où l’on va découvrir les bases.

Cette série se compose de trois articles :

  • Découverte des bases de Docker ;
  • Conteneuriser son application Node.js ;
  • Déploiement avec Docker compose

Remontons le temps

La plupart des applications nécessitent un serveur sur lequel s’exécuter. Dans le passé, il n’existait pas de solutions permettant d’isoler efficacement les applications entre elles au sein d’un même serveur. On se retrouvait donc souvent avec une application par serveur. Serveur qui était la plupart du temps surdimensionné pour les besoins de l’application et qui représentait donc un coût important pour les entreprises. De plus il pouvait être difficile de migrer une application vers un autre serveur, car cela nécessitait d’avoir le même système d’exploitation que le serveur d’origine.

L’ère de la virtualisation

L’arrivée de la virtualisation changea la donne. Celle-ci a permis de créer et d’exécuter une ou plusieurs machines virtuelles (virtual machine ou VM) sur une même machine physique. Grossièrement, une machine virtuelle est l’équivalent d’une machine physique, mais gérer de manière logicielle à l’aide de ce que l’on appelle un hyperviseur. L’hyperviseur est une plate-forme de virtualisation qui permet à plusieurs systèmes d’exploitation de travailler sur une même machine physique en même temps.

Principe de la virtualisation
Principe de la virtualisation

Mais la virtualisation n’est pas parfaite. Chaque machine virtuelle dispose de son propre système d’exploitation qui peut consommer beaucoup de ressources (CPU, mémoire, disques, etc.). Nous devons également gérer la maintenance de ces systèmes d’exploitation (mise à jour par exemple) et certains d’entre eux nécessitent même l’achat de licence. Au final, on se retrouve à devoir maintenir des systèmes d’exploitation alors que ce qui nous intéresse ce sont surtout les applications qui vont tourner sur ceux-ci.

La conteneurisation à la rescousse

La conteneurisation ressemble beaucoup à la virtualisation à un détail près, chaque conteneur (pour simplifier, c’est plus ou moins l’équivalent de la machine virtuelle) ne dispose pas de son propre noyau de système d’exploitation, mais va utiliser celui de la machine hôte. Cela permet donc de libérer des ressources, de réduire les opérations de maintenance liées aux systèmes d’exploitation, mais également les coûts de licences de ceux-ci. De plus, les conteneurs sont plus rapides à démarrer qu’une machine virtuelle du fait de l’absence du système d’exploitation, mais également plus facilement transportable d’une infrastructure à une autre.

Principe de la conteneurisation
Principe de la conteneurisation

Pour faire simple, un conteneur virtualise un système d’exploitation et réutilise le noyau du système d’exploitation de la machine hôte alors qu’une machine virtuelle virtualise une machine physique à l’aide de l’hyperviseur.

Et Docker dans tout ça ?

Même si l’histoire sur l’origine de Docker est intéressante, je ne vais pas vous la raconter et plutôt me concentrer sur son fonctionnement et son utilisation (Sinon vous allez vous endormir déjà que l’article est assez long…). Si vous êtes néanmoins intéressé, je vous invite à aller lire la page Wikipédia de Docker.

Concrètement c’est quoi Docker ?

Docker est simplement un moteur de conteneurisation permettant de créer, gérer et orchestrer des conteneurs. Voyons rapidement les différents composants de Docker.

Composants de Docker
Composants de Docker

 

Client

Le client est une interface en ligne de commande (CLI) permettant d’interagir facilement avec le daemon Docker. Nous reviendrons sur l’utilisation du client lorsque nous verrons les différentes commandes.

daemon (dockerd)

Le daemon (dockerd) expose une API (utilisé par le client) permettant d’interagir avec les couches plus basses de Docker (containerd).

containerd

containerd est un runtime de haut niveau permettant de gérer le cycle de vie des conteneurs (démarrage, arrêt, suppression, etc.), mais également de gérer les volumes, le réseau ou encore les images (nous reviendrons sur ces termes dans la suite de l’article). Il s’agit en quelque sorte d’un superviseur de conteneurs.

runc

runc est un runtime de bas niveau permettant de démarrer les conteneurs en interagissant avec le système d’exploitation. L’Open Container Initiative (OCI), un projet de la Fondation Linux, a conçu des standards pour les conteneurs notamment pour le format des images et l’exécution des conteneurs (runtime). runc est donc une l’implémentation de ce dernier.

Il existe de nombreux autres runtimes sur le marché, le monde de la conteneurisation est un joyeux bordel et il est souvent difficile de s’y retrouver. Si vous voulez en savoir plus sur les runtimes je vous conseille cet article de la grotte du barbu ou encore cette vidéo de xavki.

Installation de Docker

Avant de continuer, nous allons installer Docker. Il existe une version pour Linux, Windows et Mac.

Linux

On va s’intéresser ici à la procédure d’installation sur Ubuntu, si vous utilisez une autre distribution, je vous invite à aller lire la documentation officielle.

On commence par mettre à jour la liste des paquets existants :

On installe toutes les dépendances requises pour l’installation :

Puis on ajoute la clé GPG du dépôt de Docker :

On indique ensuite que l’on souhaite utiliser la version stable du dépôt de Docker :

On met à jour une nouvelle fois la liste des paquets existants :

Pour finir, nous pouvons installer Docker :

Vérifions maintenant que tout fonctionne :

Si vous voulez exécuter les commandes Docker sans être root, il suffit de créer un groupe docker:

Et d’ajouter votre nom d’utilisateur à ce groupe :

Windows

Concernant l’installation sous Windows, celle-ci est très simple. Il suffit de télécharger Docker Desktop que vous trouverez sur le site officiel et d’installer celui-ci en suivant les instructions.

Mac

L’installation sur Mac est similaire à celle de Windows. Il suffit de télécharger Docker Desktop et de suivre les instructions. Rendez-vous sur le site officiel pour le télécharger.

Play with Docker

Si vous souhaitez tester Docker sans l’installer, vous pouvez utiliser le site “Play with Docker”. Il vous faudra juste créer un compte sur le site de Docker, et une fois connecté vous aurez accès à une sandbox d’une durée de 4 heures.

Play With Docker (PWD)
Play With Docker (PWD)

Passons aux choses sérieuses

J’ai parlé tout à l’heure d’images, de conteneurs, de volumes ou encore de réseaux, tout ce petit monde forme ce que l’on appelle les objets Docker. Il est temps de s’intéresser à chacun d’eux et de passer à la pratique.

Les images

Une image contient tout ce qui est nécessaire au fonctionnement d’une application c’est-à-dire le code de celle-ci, les dépendances, la configuration, les variables d’environnement, etc. Si vous êtes familiers avec l’utilisation des machines virtuelles, c’est plus au moins l’équivalent des templates. C’est à partir de ces images que nous pouvons créer les conteneurs, si l’on veut faire un parallèle avec la programmation, on peut considérer qu’une image est une classe et un conteneur une instance de celle-ci.

Une image Docker permet de créer un conteneur
Une image Docker permet de créer un conteneur

Je vous rappelle que la conteneurisation permet de virtualiser un système d’exploitation et réutilise le noyau du système d’exploitation de la machine hôte, contrairement aux machines virtuelles qui elles virtualisent une machine physique. De ce fait les images sont généralement très petites puisque celles-ci embarquent uniquement le strict minimum pour faire tourner les applications.

Récupérer des images

Lorsque l’on installe Docker, aucune image n’est présente. Nous pouvons le vérifier avec la commande suivante permettant de lister les images :

On remarque qu’il n’y a aucune image de présente :

Pour récupérer une image (ou parle de pulling), il suffit d’utiliser la commande suivante :

Par exemple, si l’on souhaite récupérer l’image de la dernière version de Node.js, il suffit d’utiliser la commande suivante :

Comme vous pouvez le voir, plusieurs éléments ont été téléchargés (on reviendra dessus un peu plus tard) :

Vérifions maintenant que l’image est bien présente sur notre machine :

C’est bon ! On a une image Docker de Node.js sur notre machine. Maintenant vous vous demandez sûrement où a-t-on récupéré cette image ?

Les images sont centralisées dans ce qu’on appelle des image registries qui sont en quelque sorte des dépôts permettant de partager et de stocker les images Docker à l’instar de npm ou packagist qui eux permettent de stocker et de partager respectivement des librairies JavaScript et PHP.

Le registry par défaut est le Docker Hub, mais il en existe bien entendu d’autres. On peut d’ailleurs voir les registries utilisés par Docker avec la commande suivante :

On a tout un tas d’informations, mais celle qui nous intéresse est Registry :

Un registry peut contenir plusieurs repositories qui peuvent contenir plusieurs versions d’images… Ok vous avez du mal à suivre ? Je vais vous faire un schéma ça sera beaucoup plus simple :

Un registry contient plusieurs repositories qui contiennent plusieurs versions d'images
Un registry contient plusieurs repositories qui contiennent plusieurs versions d’images

Si l’on reprend l’image de Node.js, celle-ci se trouve sur le registry https://index.docker.io/v1/ (le Docker Hub), le repository c’est simplement le nom de l’image ici node, la version latest est quant à elle, un tag correspondant à la dernière version de l’image de Node.js (la version 16 au moment de l’écriture de cet article).

Repository Node.js
Repository Node.js

Comme on peut le voir pour une même image, il peut y avoir plusieurs tags. Si par exemple nous récupérons l’image de Node.js ayant le tag 16 :

Et qu’on liste les images :

On pourrait penser que l’on a deux images différentes, mais si l’on regarde de plus près on se rend compte que les ids des images sont les mêmes, preuve qu’il s’agit bien de la même image.

Une image est découpée en plusieurs couches

Avant de voir les principales commandes concernant les images, il faut que je vous parle d’un concept très important les concernant, il s’agit des couches (layer). Rappelez-vous lorsque l’on a récupéré l’image de Node.js, plusieurs éléments ont été téléchargés :

Chaque élément est en fait ce que l’on appelle une couche (ou layer), chaque couche est en lecture seule et l’ensemble de celles-ci forme une image.

L'image Node est composée de plusieurs couches
L’image Node est composée de plusieurs couches

Pour faire simple, chaque couche correspond à une étape de création de l’image et contient un ensemble de fichiers créés lors de cette étape. 

Chaque couche contient des fichiers
Chaque couche contient des fichiers

Docker utilise un système de fichier permettant de fusionner ces différentes couches et de présenter une vue unifiée, c’est ce qu’on appelle un “Union File System“.

Docker fusionne les différentes couches afin de présenter une vue unifiée
Docker fusionne les différentes couches afin de présenter une vue unifiée

Sachez également que les couches peuvent être partagées entre plusieurs images, mais on en reparlera dans le prochain article, où nous nous intéresserons à la création d’images.

Les principales commandes

Documentation officielle : https://docs.docker.com/engine/reference/commandline/image

Voyons maintenant les principales commandes concernant les images.

Récupérer une image

Documentation officielle : https://docs.docker.com/engine/reference/commandline/image_pull

Pour rappel, pour récupérer (pull) une image il suffit d’utiliser la commande suivante :

Si l’on souhaite par exemple télécharger la version 16 de Node.js utilisant Alpine, qui est une distribution Linux ultra légère, il suffit d’utiliser la commande suivante :

Il est également possible de ne pas préciser de tag :

Ce qui a pour effet de récupérer l’image ayant comme tag latest, si celui-ci existe.

Note : Je vous déconseille d’utiliser le tag latest qui ne garantit pas qu’il s’agisse de la dernière version ! Préférez un tag avec un numéro de version.

Vous pouvez également télécharger toutes les images d’un repository avec l’option -a ou --all-tags, mais pour des raisons évidentes, éviter de le faire !

Lister les images

Documentation officielle : https://docs.docker.com/engine/reference/commandline/image_ls

On a également vu la commande permettant de lister les images :

Quelques options utiles :

  • Lister toutes les images: --all , -a
  • Appliquer un filtre : --filter. Par exemple afficher les images sans tags : docker image ls --filter dangling=true
Supprimer les images

Documentation officielle : https://docs.docker.com/engine/reference/commandline/image_rm

Supprimer les images non taguées

Documentation officielle : https://docs.docker.com/engine/reference/commandline/image_prune

Il est possible de supprimer les images non taguées et inutilisées par des conteneurs via l’option -a (ou -all).

Rechercher une image

Documentation officielle : https://docs.docker.com/engine/reference/commandline/search/

Il est également possible de rechercher une image via la commande suivante :

Par exemple si on cherche l’image de Redis :

On obtient le résultat suivant :

Personnellement j’utilise que très rarement cette commande, je vais plutôt rechercher directement sur le Docker Hub.

Les conteneurs

Un conteneur est simplement un environnement d’exécution de processus isolé du reste du système, il n’est donc pas possible pour un conteneur d’accéder aux ressources d’un autre conteneur ou de la machine hôte, tout du moins si on ne l’a pas explicitement autorisé.

Démarrer un conteneur

J’ai fait précédemment le parallèle avec la programmation en disant qu’une image était une classe et un conteneur l’instance de cette classe. Créons donc un conteneur à partir d’une image. Nous allons commencer par le traditionnel hello world, et utiliser la commande docker run, qui comme son nom l’indique, permet d’exécuter un nouveau conteneur :

Vous devriez avoir le résultat suivant :

Comme le message l’indique, voici ce qu’il s’est passé :

  • Le client Docker a envoyé l’instruction de démarrer un nouveau conteneur de l’image hello-world au daemon ;
  • Le daemon Docker à récupérer l’image hello-world depuis le Docker Hub (si celle-ci n’était pas déjà présente en local) ;
  • Le daemon a créé un nouveau conteneur (à l’aide de containerd et runc) depuis l’image hello-world et exécute le programme contenu dans celle-ci ;
  • Le daemon a ensuite transféré la sortie du conteneur vers le client Docker pour ensuite l’afficher sur le terminal.

Oui c’est tout ce n’est pas très compliqué ! Bon, comme le dit le message faisons quelque chose de plus ambitieux et démarrons un conteneur d’Ubuntu :

On utilise les options -it, permettant d’activer le mode interactif (l’option i) en attachant le flux entrant (STDIN) du conteneur à la machine hôte pour pouvoir interagir avec celui-ci et de connecter un terminal (l’option t) au conteneur. On indique également le nom de l’image ici ubuntu suivi du programme à exécuter ici bash. On obtient la sortie suivante :

On remarque que nous avons le prompt (root@d947d5b99b48:/#) qui indique que nous sommes actuellement “connecté” au conteneur. Testons une commande, ls -al par exemple :

Nous avons donc un conteneur Ubuntu qui est totalement isolé du reste ! Continuons et lançons la commande suivante :

On remarque que l’on a un seul programme en cours d’exécution (si l’on ignore la commande ps) qui est bash, celui que l’on a indiqué lors du démarrage du conteneur. Son PID est donc 1 ce qui indique qu’il s’agit du processus principal (aussi appelé init), lorsque celui-ci se termine, le conteneur s’arrête.

Sortons donc du conteneur via la commande exit :

On se retrouve donc à nouveau sur la machine hôte. Vérifions maintenant que le conteneur est bien arrêté :

C’est bien le cas !

Faisons un autre test, mais cette fois-ci avec Apache. Lançons la commande suivante :

On a avons plusieurs nouvelles options :

  • -d (ou --detach) : Permets de démarrer le conteneur en arrière-plan ;
  • --name : Permets de donner un nom au conteneur (par défaut Docker génère un nom aléatoire);
  • -p (ou --publish) : Permets de transférer le trafic du port 8080 de la machine hôte vers le port 80 du conteneur (-p port_hote:port_conteneur).

On obtient l’affiche suivant : 

Qui est simplement l’identifiant du conteneur.

Rendez-vous à l’adresse suivante http://localhost:8080/ et vous devriez avoir la page suivante qui s’ouvre :

Apache est accessible depuis la machine hôte
Apache est accessible depuis la machine hôte

Se “connecter” à conteneur en cours d’exécution

Notre conteneur Apache est exécuté en arrière-plan (via l’option -d), mais il est possible de se “connecter” à celui-ci via la commande suivante :

Cette commande permet d’exécuter une commande (ou un programme) au sein d’un conteneur. Dans notre cas, nous souhaitons exécuter le programme bash, d’activer le mode interactif (l’option -i) et de connecter un terminal (l’option -t).

Une fois à l’intérieur du conteneur, ajoutons une page HTML :

Vérifions ensuite que tout fonctionne http://localhost:8080/docker.html :

Notre nouvelle page est accessible depuis la machine hôte
Notre nouvelle page est accessible depuis la machine hôte

Le cycle de vie d’un conteneur

Il est possible de stopper, mettre en pause ou même supprimer un conteneur. Par exemple, pour stopper notre conteneur Apache, il suffit d’utiliser la commande suivante :

Si nous vérifions les conteneurs en cours d’exécution via la commande docker container ls (vous pouvez également utiliser docker ps) :

Nous remarquons qu’il n’y a aucun conteneur en cours d’exécution. Pour voir l’ensemble des conteneurs présent, même ceux stoppés, il suffit d’ajouter l’option -a (ou --all) :

On remarque que le statut de notre conteneur est Exited, le conteneur est donc bien stoppé.

Il est également possible de supprimer un conteneur via la commande docker container rm :

Vérifions maintenant que le conteneur est bien supprimé :

C’est bien le cas ! Petite information, le conteneur doit d’abord être stoppé pour pouvoir le supprimer, dans le cas contraire vous aurez le message suivant :

Vous pouvez néanmoins forcer la suppression via la commande -f :

Mais je vous le déconseille, car le conteneur ne pourra pas se terminer proprement puisque l’option -f envoi le signal SIGKILL au processus principal du conteneur (celui avec le PID 1) qui ne laisse donc aucun délai à celui-ci pour se terminer proprement contrairement à la commande docker container stop qui, elle, envoie le signal SIGTERM et laisse 10 secondes au conteneur de se terminer, et envoie le signal SIGKILL si le conteneur ne s’est pas terminé avant.

Il est également possible de mettre en pause un conteneur via la commande docker container pause :

Et de le sortir de cet état de pause via la commande docker container unpause :

Le cycle de vie d'un conteneur
Le cycle de vie d’un conteneur

Les politiques de redémarrage

Il est possible de spécifier à la commande docker container run, via l’option --restart, une politique de redémarrage du conteneur. Il en existe quatre :

  • no : Ne redémarre pas le container automatiquement. Il s’agit de l’option par défaut ;
  • always : Redémarre le container quand il est stoppé. Si le conteneur est arrêté manuellement (via la commande docker container stop), le container redémarrera si le daemon redémarre ;
  • unless-stopped : Idem que always, mais le container ne redémarrera pas si le daemon redémarre ;
  • on-failure : Redémarre lorsque le container s’est arrêté suite à une erreur (le conteneur s’est terminé avec un code de retour différent de 0).

Reprenons notre exemple précédent avec Ubuntu et rajoutons la politique de redémarrage always :

On a vu tout à l’heure que le processus bash avait le PID 1, lorsque nous utilisons la commande exit, nous quittons donc ce processus. Comme il s’agit du processus principal, le conteneur s’arrête également. Mais avec la politique de redémarrage always le conteneur s’arrête bien, mais redémarre. 

On peut s’en assurer avec la commande docker container ls :

Le conteneur est en cours d’exécution depuis 2 secondes, donc celui-ci a bien redemarré.

Voyons maintenant la différence entre always et unless-stopped. Créons un premier conteneur avec la politique always :

Et un autre avec la politique unless-stopped :

On vérifie que nos deux conteneurs sont en cours d’exécution :

Puis on les arrête :

Vérifions :

Redémarrons le daemon Docker :

Une fois le daemon Docker redémarré, vérifions nos deux conteneurs :

On remarque que le conteneur always-container a bien redémarré contrairement au conteneur unless-stopped-container.

Les principales commandes

Documentation officielle : https://docs.docker.com/engine/reference/commandline/container

Voyons maintenant les principales commandes concernant les conteneurs.

Démarrer un conteneur

Documentation officielle : https://docs.docker.com/engine/reference/commandline/container_run/

Quelques options utiles :

  • Binder les ports : --publish , -p port_hote:port_conteneur
  • Mode interactive : --interactive , -i
  • Ouvrir un terminal : --tty , -t
  • Supprimer après arrêt : --rm
  • Associer un nom : --name
  • Démarrer en arrière-plan : --detach , -d
  • Passer des variables d’environnement : --env , -e
Arrêter un conteneur

Documentation officielle : https://docs.docker.com/engine/reference/commandline/container_stop

Cette commande envoie le signal SIGTERM au processus principal (PID 1) et attend 10 secondes. Si le processus ne s’est pas terminé, elle envoie le signal SIGKILL.

Il est tout de fois possible de modifier ce délai de 10 secondes via l’option -t (ou --time) en spécifiant le nombre de secondes avant d’envoyer le signal SIGKILL :

Vous pouvez soit spécifier le nom du conteneur soit son identifiant.

Supprimer les conteneurs

Documentation officielle : https://docs.docker.com/engine/reference/commandline/container_rm/

Les conteneurs doivent être arrêtés pour pouvoir être supprimés, dans le cas contraire il faudra utiliser l’option -f (ou --force) pour forcer l’arrêt, mais cela a pour conséquence d’envoyer le signal SIGKILL sans laisser le temps aux conteneurs de se terminer proprement.

Supprimer tous les conteneurs à l’arrêt

Documentation officielle : https://docs.docker.com/engine/reference/commandline/container_prune

Exécuter une commande dans un conteneur

Documentation officielle : https://docs.docker.com/engine/reference/commandline/container_exec

Quelques options utiles :

  • Mode interactive : --interactive , -i
  • Ouvrir un terminal : --tty , -t
Lister les conteneurs

Documentation officielle : https://docs.docker.com/engine/reference/commandline/container_ls

Si vous souhaitez afficher tous les conteneurs (même ceux à l’arrêt) il suffit d’ajouter l’option -a (ou --all).

La persistance des données

On a vu que les images comprenaient plusieurs couches (layers) en lecture seule, par contre ce que je ne vous ai pas dit c’est que lors de la création d’un conteneur, une couche en lecture/écriture est ajoutée.

Chaque conteneur dipose d'une couche en lecture/écriture
Chaque conteneur dispose d’une couche en lecture/écriture

Malheureusement cette couche est éphémère, lorsque l’on supprime un conteneur cette couche est également supprimée, cela signifie que toutes les modifications seront perdues. Voyons deux solutions que Docker nous offre pour répondre à cette problématique à savoir :

  • Les montages liés (bind mount) ;
  • Les volumes nommés.

Les montages liés (bind mount)

La première solution est très simple, il s’agit de lier un répertoire de la machine hôte à un répertoire du conteneur. Pour cela il suffit d’utiliser l’option -v de la commande docker container run et de spécifier le répertoire de la machine hôte ainsi que celui du conteneur.

Reprenons l’exemple de notre conteneur Apache :

Ici nous avons lié le répertoire /home/arkerone/docker/apache-test de la machine hôte au répertoire /usr/local/apache2/htdocs du conteneur.

Il est également possible d’utiliser l’option --mount comme ceci :

Celle-ci est un peu plus verbeuse, nous définissons le type de montage (ici bind) à l’aide de la clé type ainsi que le répertoire de la machine hôte à l’aide de la clé src et le répertoire du conteneur à l’aide de la clé dst.

Créons un fichier index.html dans le répertoire /home/arkerone/docker/apache-test de la machine hôte et ajoutons ce code :

Rendons-nous maintenant à l’adresse http://localhost:8080 :

Notre fichier index.html est accessible dans le conteneur
Notre fichier index.html est accessible dans le conteneur

Ça fonctionne bien, mais, il y a tout de même un problème avec cette solution. Les montages liés sont dépendants de la structure du système de fichiers de la machine hôte, vous devez donc faire attention à ne pas modifier cette structure par erreur. De plus cela peut poser des problèmes de sécurité puisque vous autorisez un conteneur, censé être isolé de la machine hôte, à pouvoir modifier la structure du système de fichiers.

Les volumes nommés

La solution au problème précédent est d’utiliser des volumes nommés. Les volumes nommés sont contrairement aux montages liés, totalement gérés par Docker et sont, par conséquent, indépendants de la structure du système de fichier de la machine hôte. Pour créer un volume nommé, nous devons utiliser la commande suivante :

Créons donc un volume test-apache2-vol :

Vérifions que celui-ci est bien créé à l’aide de la commande docker volume ls :

Reprenons notre précédent exemple et créons un conteneur utilisant ce volume :

On utilise, tout comme les montages liés, l’option -v, mais cette fois-ci en spécifiant le nom du volume plutôt qu’un répertoire de la machine hôte. À noter que si le volume n’existe pas, Docker se charge de la créer. 

Il est également possible d’utiliser l’option --mount comme ceci, mais attention cette fois-ci le volume doit d’abord avoir été créé :

Contrairement aux montages liés, il n’est pas possible de modifier les fichiers depuis la machine hôte (Je vous mens un peu puisque c’est possible, mais ce n’est pas du tout recommandé).

Allons donc modifier la page d’accueil d’Apache directement depuis le conteneur. Pour cela connectons-nous au conteneur :

Puis modifions la page d’accueil :

Vérifions ensuite depuis la machine hôte http://localhost:8080 :

Cette page est maintenant stockée dans un volume Docker
Cette page est maintenant stockée dans un volume Docker

Créons un second conteneur utilisant le même volume :

Puis rendons-nous à l’adresse suivante depuis la machine hôte http://localhost:8081. Nous avons bien la même page web qui s’ouvre, puisque celle-ci est stockée dans le volume test-apache2-vol qui est partagé entre les deux conteneurs. Lorsque vous supprimez vos conteneurs, les données elles ne le sont pas tant que vous ne supprimez pas le volume.

Docker utilise des drivers pour gérer les différents types de volumes, par défaut il utilise le driver local qui stocke les données sur le disque de la machine hôte (var/lib/docker/volumes/ sur Linux), mais il est possible d’utiliser d’autres types de drivers permettant par exemple de stocker les données directement sur le cloud.

Montage lié ou volume nommé ?

Vous vous demandez sûrement quand utiliser un montage lié et quand utiliser un volume nommé. Comme nous l’avons vu les montages liés permettent comme leurs noms l’indiquent de lié un répertoire de la machine hôte à un répertoire du conteneur. Personnellement j’utilise les montages liés uniquement en développement. Par exemple, comme nous l’avons vu il est très simple de monter un répertoire pouvant contenir du HTML ou du PHP dans un conteneur Apache et ainsi tester son code facilement. Dans tous les autres cas, les volumes nommés sont à privilégier et c’est d’ailleurs ce que recommande la documentation officielle de Docker.

Les principales commandes

Documentation officielle : https://docs.docker.com/engine/reference/commandline/volume

Voyons maintenant les principales commandes concernant les volumes.

Créer un volume

Documentation officielle : https://docs.docker.com/engine/reference/commandline/volume_create

Lister les volumes

Documentation officielle : https://docs.docker.com/engine/reference/commandline/volume_ls

Supprimer un volume

Documentation officielle : https://docs.docker.com/engine/reference/commandline/volume_rm

Supprimer les volumes inutilisés

Documentation officielle : https://docs.docker.com/engine/reference/commandline/volume_prune

Les réseaux

Petit message aux développeurs avant de continuer, je sais que beaucoup d’entre vous n’y comprennent rien aux réseaux ou du moins déteste ça, c’est pourquoi je vais tenter d’être le plus simple possible, je vais donc volontairement ignorer certains détails techniques pour ne pas vous perdre. Bref, commençons !

Les conteneurs ont souvent besoin de communiquer entre eux, par exemple une application avec une base de données comme nous le verrons dans un prochain article, ou avec le monde extérieur, c’est pourquoi une couche réseau est nécessaire.

Docker permet la création de plusieurs types de réseaux à l’aide de drivers :

  • bridge : Il s’agit du type de driver par défaut. Celui-ci permet aux conteneurs connectés dans le même réseau de communiquer entre eux, mais ne sont pas accessibles de l’extérieur à moins de mapper les ports comme nous l’avons vu précédemment. Par défaut Docker créer un réseau bridge nommé bridge, et tous les conteneurs créés sont automatiquement connectés à celui-ci, à moins de spécifier un autre réseau ;
  • host : Ce type de driver permet de supprimer l’isolation réseau entre les conteneurs et la machine hôte. Ceux-ci seront donc accessibles depuis l’extérieur, puisqu’ils auront l’IP de la machine hôte (ils utilisent directement l’interface réseau de la machine hôte) ;
  • overlay : Ce type de driver permet de créer un réseau distribué entre plusieurs machines exécutant le moteur Docker ;
  • macvlan : Ce type de driver permet d’assigner à un conteneur une adresse MAC faisant apparaître celui-ci comme un périphérique physique sur le réseau ;
  • none : Ce type de driver permet simplement de désactiver toute couche réseau d’un conteneur.

Docker permet également d’installer d’autres drivers réseau.

Mise en pratique

Pour cet article nous allons uniquement nous intéresser au premier type de réseaux à savoir le réseau bridge. Commençons par créer un nouveau réseau bridge que l’on va appeler localnetwork :

L’option -d permet de spécifier le driver à utiliser.

Vérifions que celui-ci est bien créé à l’aide de la commande docker network ls :

Vous remarquerez que Docker crée trois réseaux par défaut : bridge, none et host.

Créons ensuite un conteneur en mode interactif et connectons-le au réseau localnetwork :

Pour choisir à quel réseau le conteneur doit se connecter, nous utilisons l’option --network en spécifiant le nom du réseau.

Créons ensuite un deuxième conteneur également en mode interactif en le connectant également au réseau localnetwork :

Maintenant depuis le premier conteneur, envoyons un ping vers le deuxième conteneur :

Les conteneurs, faisant partie d’un même réseau, peuvent communiquer ensemble au travers de leurs noms. Tout cela est possible grâce au “service discovery”.

Fonctionnement du service discovery
Fonctionnement du service discovery

Voici le fonctionnement :

  1. La commande ping appelle le résolveur DNS local (chaque conteneur à un résolveur DNS) pour résoudre le nom container-2 en une adresse IP ;
  2. Si l’adresse IP du nom container-2 ne se trouve pas dans le cache du résolveur, celui-ci la demande au serveur DNS de Docker ;
  3. Le serveur DNS renvoie l’adresse IP de container-2 au résolveur DNS du conteneur container-1. Cela fonctionne, car les deux conteneurs se trouvent dans le même réseau, j’insiste, mais c’est important, dans le cas contraire cela ne fonctionnerait pas ;
  4. La commande ping peut maintenant envoyer la requête à l’adresse IP de container-2.

Les principales commandes

Documentation officielle : https://docs.docker.com/engine/reference/commandline/network

Voyons maintenant les principales commandes concernant les réseaux.

Créer un réseau

Documentation officielle : https://docs.docker.com/engine/reference/commandline/network_create

Lister les réseaux

Documentation officielle : https://docs.docker.com/engine/reference/commandline/network_ls

Supprimer un réseau

Documentation officielle : https://docs.docker.com/engine/reference/commandline/network_rm

Supprimer les réseaux inutilisés

Documentation officielle : https://docs.docker.com/engine/reference/commandline/network_prune

Connecter un conteneur à un réseau

Documentation officielle : https://docs.docker.com/engine/reference/commandline/network_connect

Déconnecter un conteneur d’un réseau

Documentation officielle : https://docs.docker.com/engine/reference/commandline/network_disconnect

Pour finir…

Cet article est déjà bien trop long, je vais donc m’arrêter là. Nous venons de voir les bases de Docker, mais il reste encore pas mal de choses à découvrir. Dans le prochain article, nous allons apprendre à conteneuriser notre propre application. En attendant, je vous invite à jouer un peu avec Docker en utilisant des images provenant du Docker hub, vous avez le choix il y a un peu près des images pour tout ! Prenez le temps de comprendre chaque commande et n’ayez pas peur de faire n’importe quoi, n’oubliez pas qu’un conteneur est isolé de la machine hôte.


Annonces partenaire

Je suis lead developer dans une boîte spécialisée dans l'univers du streaming/gaming, et en parallèle, je m'éclate en tant que freelance. Passionné par l'écosystème JavaScript, je suis un inconditionnel de Node.js depuis 2011. J'adore échanger sur les nouvelles tendances et partager mon expérience avec les autres développeurs. Si vous avez envie de papoter, n'hésitez pas à me retrouver sur Twitter, m'envoyer un petit email ou même laisser un commentaire.

7 commentaires

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.