Docker : la plateforme de conteneurisation

Ça fait un petit moment maintenant que l’on entend un peu partout parler de la plateforme de virtualisation révolutionnaire qu’est Docker. Tellement qu’OVH va même la proposer sur son cloud (ou la propose déjà ?) via sa filiale RunAbove. En tant que passionné je ne pouvais pas passer à côté de cette techno, et je ne pouvais pas non plus m’empêcher de partager tout cela avec vous.

Je vous propose donc de découvrir Docker et ce que nous allons pouvoir faire avec, et on commence aujourd’hui avec un peu de théorie.

A la découverte de Docker

Docker est donc une plateforme de virtualisation par conteneur qui, contrairement à la virtualisation sur hyperviseur où vous devez créer de nouvelles machines complètes pour isoler celles-ci les unes des autres et s’assurer de leur indépendance, va vous permettre de créer des conteneurs qui vont uniquement contenir votre ou vos applications. Empaquetées sous forme de conteneurs, ces applications pourront ainsi être facilement déployées sur tout hôte exécutant Docker, chaque conteneur restant parfaitement indépendant !

Le nom et le vocabulaire de cette technologie n’ont pas été choisis au hasard : tout comme les conteneurs en métal, toujours fabriqués selon les mêmes standards, peuvent être pris en charge par n’importe quel transporteur (et peu importe ce qu’il y a dedans), une application « Dockerisée » doit pouvoir être exécutée sur n’importe quel hôte faisant tourner Docker, peu importe son contenu !

Avec la virtualisation par hyperviseur de type 2 (VMware Workstation, VirtualBox…), chaque hôte invité virtualise un environnement complet, ce qui permet notamment de pouvoir exécuter un système virtualisé différent du système hôte (typiquement du Windows sur une Red Hat par exemple).

La virtualisation par conteneur telle que Docker la propose permet à un système Linux de contenir un ou plusieurs processus dans un environnement d’exécution indépendant : indépendant du système hôte et des conteneurs entre eux.

Dans ce cas, conteneur virtualisé et système hôte sont fortement liés et c’est pour cela que cette technologie, qui existe pourtant depuis longtemps (OpenVZ a déjà presque 10 ans !), était peu répandue jusqu’à maintenant. Certains appellent d’ailleurs ce type de virtualisation la para-virtualisation… Mais alors comment ça marche ?

Docker simplifie l’utilisation d’outils existants

Et c’est sa principale force. S’il est possible d’isoler des conteneurs : processus, interfaces réseau, points de montage, c’est grâce à l’utilisation des namespaces Linux. Un processus qui s’exécute dans un espace de nom correspondant à un conteneur ne sera visible que par ce conteneur et le système hôte exécutant ce conteneur, point. Les deux précédents schémas vous feront comprendre aussi que la virtualisation par conteneur est plus rapide que la virtualisation par hyperviseur car les conteneurs ont directement accès aux ressources matérielles de l’hôte. La consommation de ces ressources pouvant être gérées par l’hôte en question grâce aux control groups (cgroups) du noyau Linux.

Tout ceci fait donc de Docker un chroot dopé aux stéroïdes.

À ses débuts Docker était une couche d’abstraction à LXC car il permettait de s’affranchir de la complexité de ce dernier en gérant pour nous un certain nombre d’opérations, plus facilement. Aujourd’hui Docker va beaucoup plus loin que LXC puisqu’il dispose de sa propre plate-forme : Libcontainer (bien que LXC soit toujours proposé sous forme de plugin).

Et ce qui démarque Docker des autres, c’est la rapidité des flux de travail que vous allez pouvoir mettre en place : tout est fait pour réduire le temps entre le développement, les tests, le déploiement et l’utilisation de votre application en production. Et pour cela, on trouve les images.

Une image est le composant de base de Docker. C’est un instantané d’un système d’exploitation (par exemple Debian). Ces images sont disponibles sur un dépôt public, géré par Docker, appelé le registre. Schématisons ceci de manière plus précise…

Différence entre le fonctionnement de Docker et des VM

L’exemple ci-dessus se compose de deux conteneurs, parfaitement isolés (d’où les couleurs différentes). On trouve à la base les composants nécessaires à Docker, et qui sont fournis par le noyau Linux de l’hôte. On trouve ensuite deux images : Debian et BusyBox.

Une image ne peut être modifiée directement, elle reste toujours en lecture seule.

Remarquez qu’un environnement d’exécution peut nécessiter l’empilement de plusieurs images (ici emacs, puis Nginx), rendu possible par les fonctionnalités de copy-on-write des modules de stockages utilisés par Docker (AuFS, Btrfs).

Enfin, on trouve la couche supérieure, accessible en écriture, qui contiendra toutes les modifications apportées à l’application.

Pour résumer : lorsque nous allons lancer un conteneur pour exécuter une application à partir d’une image, Docker va monter le système de fichier de l’image souhaitée en lecture seule, et ajouter une couche accessible en écriture par dessus, via UnionFS.

On fait donc de sacrées économies de consommation puisque plusieurs conteneurs utilisant la même image de base utiliseront le même système de fichiers !

Le Hub, un GitHub pour vos images Docker

Si vous souhaitez conserver les modifications que vous avez apporté à votre application, vous pouvez enregistrer celles-ci dans un nouvelle image qui sera uniquement composée des différences apportées. Cette image sera identifiée par Docker grâce à un numéro unique, et vous pourrez pousser (push) et stocker cette image vers le dépôt public Docker.

À travers le Docker Hub, vous aller pouvoir gérer vos images de la même manière que vous gérer votre code avec Git. Le vocabulaire est le même…

Pour conclure cette première approche…

J’espère que je n’ai perdu personne… Docker repose sur des concepts qui ne sont pas forcément évidents au premier abord. Vous verrez néanmoins que toutes ces explications seront plus claires lorsque nous commencerons à manipuler images et conteneurs.

Installation de Docker

Ici pas de secret, il suffit de suivre les instructions proposées par le site officiel. On nous informe que Docker a besoin du noyau Linux 3.8 ou supérieur pour fonctionner, et que Debian Stable est toujours sur le noyau 3.2. On va donc devoir activer les dépôts backports afin d’aller y récupérer la dernière version du noyau Linux.

Normalement vous êtes sur une Debian récente donc vous n’avez pas besoin des étapes suivantes

Pour cela il suffit d’éditer le fichier /etc/apt/sources.list, et d’y ajouter :

deb http://http.debian.net/debian wheezy-backports main

On enregistre, puis :

sudo apt-get update sudo apt-get -t wheezy-backports install linux-image-amd64

Redémarrons pour que le nouveau noyau soit chargé. Nous pouvons enfin récupérer le script d’installation de Docker 🙂

$ curl -sSL https://get.docker.com/ | sh

Sous Debian 10, vous n’avez pas besoin de faire cela !

Exécuter Docker sans les droits root

La documentation nous indique que le daemon Docker tourne toujours avec l’utilisateur root, et qu’il se connecte à un socket Unix en lieu et place d’un socket TCP. L’utilisateur root est par défaut propriétaire de ce socket Unix, et ne peut donc être accessible qu’avec la commande sudo.

Pour éviter d’utiliser sudo à tort et à travers, nous allons pouvoir créer un groupe Unix nommé ‘docker’ pour nous y ajouter. Ainsi, les propriétés du daemon docker seront modifiées pour que celui-ci soit accessible en lecture / écriture pour les membres du groupe docker. Certes le daemon doit toujours fonctionner en tant que root, mais si nous utilisons le client docker en tant que membre du groupe docker, alors nous n’aurons plus besoin d’utiliser sudo.

C’est du bonus hein… Cela permet aussi de gérer la surface d’attaque de Docker, mais vous pouvez très bien sauter cette étape et continuer à utiliser Docker avec sudo.

# On ajoute le groupe docker s'il n'existe pas déjà. 
$ sudo groupadd docker   

# On ajoute l'utilisateur courant (nous) "${USER}" au groupe. 
# On peut y mettre n'importe quel utilisateur 
# Il faudra peut-être de reconnecter pour que les modifications prennent effet. 
$ sudo gpasswd -a ${USER} docker
 
# On redémarre le daemon. 
$ sudo service docker restart

Alors, c’était dur ?

docker version
Client:
 Version:           18.09.7
 API version:       1.39
 Go version:        go1.10.1
 Git commit:        2d0083d
 Built:             Wed Jul  3 12:13:59 2019
 OS/Arch:           linux/amd64
 Experimental:      false

Server:
 Engine:
  Version:          18.09.7
  API version:      1.39 (minimum version 1.12)
  Go version:       go1.10.1
  Git commit:       2d0083d
  Built:            Mon Jul  1 19:31:12 2019
  OS/Arch:          linux/amd64
  Experimental:     false

Commençons à jouer avec les images

Avant de pouvoir commencer à jouer avec les conteneurs, il va falloir récupérer une image.
Je vous avais expliqué que les images Docker sont des instantanés de systèmes d’exploitation sur lesquels nous allons pouvoir travailler et manipuler des conteneurs.

Les images Docker

Tout un tas d’images sont maintenues et disponibles sur le registre officiel : le Docker Hub.

Imaginons que je souhaite récupérer une image d’Ubuntu. Je vais alors utiliser la commande search pour retrouver toutes les images Ubuntu du registre :

$ docker search ubuntu
NAME                                DESCRIPTION                                     STARS     OFFICIAL   AUTOMATED
ubuntu                              Official Ubuntu base image                      1159      [OK]       
dockerfile/ubuntu                   Trusted automated Ubuntu (http://www.ubunt...   39                   [OK]
ansible/ubuntu14.04-ansible         Ubuntu 14.04 LTS with ansible                   30                   [OK]
dockerfile/ubuntu-desktop           Trusted automated Ubuntu Desktop (LXDE) (h...   20                   [OK]
ubuntu-upstart                      Upstart is an event-based replacement for ...   17        [OK]       
[...]

Vous verrez qu’un grand nombre de résultats sont proposés, c’est pourquoi j’ai volontairement tronqué la sortie. On observe que le premier résultat indique une image d’Ubuntu officielle, ce qui nous assure qu’elle a été validée par l’équipe de Docker. Les images gérées par l’équipe de Docker se remarquent par le format de leur nom, qui est de la forme nom_image alors que les non-officielles sont plutôt de la forme utilisateur/nom_image.

Je décide de récupérer l’image Ubuntu officielle :

$ docker pull ubuntu:14.04

Vous noterez l’ajout du :14.04 au nom de l’image. Ceci afin d’éviter de récupérer toutes les version d’Ubuntu présentes dans le dépôt Ubuntu. Après quelques secondes de téléchargement (l’image fait 200Mo), la nouvelle image est prête !

$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
ubuntu              14.04               b39b81afc8ca        13 hours ago        188.3 MB

Heu on pourrait avoir des infos sur ce qu’est un Tag ou un ID ?

La sortie de la commande précédente indique que nous avons récupéré l’image portant l’ID b39b81afc8ca dans le dépôt intitulé « ubuntu ». Un ID permet d’identifier de manière unique la version d’une image. Et oui, un dépôt contient plusieurs versions d’une image, sous forme de couches de systèmes de fichiers. Je vous en avais parlé précédemment.

Ensuite, comme un ID n’est pas forcément explicite pour désigner une version, on lui affecte un ou plusieurs tags, ici « 14.04 ». Plusieurs tags peuvent effectivement désigner la même image, mais chaque version d’image ne possède qu’un ID.

Cet empilement de version et de systèmes de fichier peut être affiché avec l’option --all

$ docker images --all
REPOSITORY          TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
ubuntu              14.04               b39b81afc8ca        13 hours ago        188.3 MB
<none>              <none>              615c102e2290        13 hours ago        188.3 MB
<none>              <none>              837339b91538        13 hours ago        188.3 MB
<none>              <none>              53f858aaaf03        13 hours ago        188.1 MB
<none>              <none>              511136ea3c5a        19 months ago       0 B

Ok, mais à quoi correspondent toutes ces images intermédiaire ?

Je ne voudrais pas vous embrouiller tout de suite… Elles correspondent en fait aux différentes commandes qui ont été utilisées pour générer l’image finale. Ces commandes peuvent être affichées avec la commande history.

$ docker history ubuntu:14.04
IMAGE               CREATED             CREATED BY                                      SIZE
b39b81afc8ca        13 hours ago        /bin/sh -c #(nop) CMD [/bin/bash]               0 B
615c102e2290        13 hours ago        /bin/sh -c sed -i 's/^#\s*\(deb.*universe\)$/   1.895 kB
837339b91538        13 hours ago        /bin/sh -c echo '#!/bin/sh' > /usr/sbin/polic   194.5 kB
53f858aaaf03        13 hours ago        /bin/sh -c #(nop) ADD file:ca5b63647c6b7a419e   188.1 MB
511136ea3c5a        19 months ago                                                       0 B

Nous y reviendrons quand nous créerons nos propres images, pas de panique 🙂

Et tout ce bazar, il est stocké où ?

$ docker info
docker info
Containers: 0
Images: 5
Storage Driver: aufs
 Root Dir: /var/lib/docker/aufs
 Dirs: 5
Execution Driver: native-0.2
Kernel Version: 3.16.0-0.bpo.4-amd64
Operating System: Debian GNU/Linux 7 (wheezy)
CPUs: 2
Total Memory: 3.86 GiB
Name: dev
ID: E7WT:PPQD:Z6IE:ZGW2:42EC:T5IE:KXLQ:CIRB:WI54:KJG7:KUYM:SYFB
WARNING: No memory limit support
WARNING: No swap limit support

Voilà toutes les informations relatives notre installation de Docker. Pour répondre à la question, la ligne qui nous intéresse est donc le Root Dir, soit /var/lib/docker/aufs :

$ sudo ls -l /var/lib/docker/aufs/*
/var/lib/docker/aufs/diff:
total 20
drwxr-xr-x  2 root root 4096 Jan 17 11:42 511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158
drwxr-xr-x 21 root root 4096 Jan 17 11:44 53f858aaaf03033e088d357df23e311d71aa93ac578ef39aa8230580ea95076f
drwxr-xr-x  3 root root 4096 Jan 17 11:44 615c102e2290b70d38d89c03a1ad263da8bd8b05fb7fc8479174e5fd2215520e
drwxr-xr-x  6 root root 4096 Jan 17 11:44 837339b915388417a842c87a681a5448df2509068c8d3efd1638f1fad2eacea2
drwxr-xr-x  2 root root 4096 Jan 17 11:44 b39b81afc8cae27d6fc7ea89584bad5e0ba792127597d02425eaee9f3aaaa462
 
/var/lib/docker/aufs/layers:
total 16
-rw-r--r-- 1 root root   0 Jan 17 11:42 511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158
-rw-r--r-- 1 root root  65 Jan 17 11:44 53f858aaaf03033e088d357df23e311d71aa93ac578ef39aa8230580ea95076f
-rw-r--r-- 1 root root 195 Jan 17 11:44 615c102e2290b70d38d89c03a1ad263da8bd8b05fb7fc8479174e5fd2215520e
-rw-r--r-- 1 root root 130 Jan 17 11:44 837339b915388417a842c87a681a5448df2509068c8d3efd1638f1fad2eacea2
-rw-r--r-- 1 root root 260 Jan 17 11:44 b39b81afc8cae27d6fc7ea89584bad5e0ba792127597d02425eaee9f3aaaa462
 
/var/lib/docker/aufs/mnt:
total 20
drwxr-xr-x 2 root root 4096 Jan 17 11:42 511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158
drwxr-xr-x 2 root root 4096 Jan 17 11:44 53f858aaaf03033e088d357df23e311d71aa93ac578ef39aa8230580ea95076f
drwxr-xr-x 2 root root 4096 Jan 17 11:44 615c102e2290b70d38d89c03a1ad263da8bd8b05fb7fc8479174e5fd2215520e
drwxr-xr-x 2 root root 4096 Jan 17 11:44 837339b915388417a842c87a681a5448df2509068c8d3efd1638f1fad2eacea2
drwxr-xr-x 2 root root 4096 Jan 17 11:44 b39b81afc8cae27d6fc7ea89584bad5e0ba792127597d02425eaee9f3aaaa462

Ce retour n’est pas sans rappeler le contenu de .git/objects pour les utilisateurs de Git, nous verrons plus tard que ce n’est absolument pas un hasard si nous retrouvons une syntaxe et une philosophie de versionning très proches de Git.

Enfin, pour connaître tous les détails d’une image :

docker inspect ubuntu:14.04
[{
    "Architecture": "amd64",
    "Author": "",
    "Checksum": "tarsum.dev+sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
    "Comment": "",
    "Config": {
        "AttachStderr": false,
        "AttachStdin": false,
        "AttachStdout": false,
        "Cmd": [
            "/bin/bash"
        ],
        "CpuShares": 0,
        "Cpuset": "",
        "Domainname": "",
        "Entrypoint": null,
        "Env": [
            "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
        ],
        "ExposedPorts": null,
        "Hostname": "2338b11efbe1",
        "Image": "615c102e2290b70d38d89c03a1ad263da8bd8b05fb7fc8479174e5fd2215520e",
        "MacAddress": "",
        "Memory": 0,
        "MemorySwap": 0,
        "NetworkDisabled": false,
        "OnBuild": [],
        "OpenStdin": false,
        "PortSpecs": null,
        "StdinOnce": false,
        "Tty": false,
        "User": "",
        "Volumes": null,
        "WorkingDir": ""
    },
    "Container": "485a6058231fa39ea20d4c7a526ff5916aee18284fd1fa7d78e1e5bea7ff662b",
    "ContainerConfig": {
        "AttachStderr": false,
        "AttachStdin": false,
        "AttachStdout": false,
        "Cmd": [
            "/bin/sh",
            "-c",
            "#(nop) CMD [/bin/bash]"
        ],
        "CpuShares": 0,
        "Cpuset": "",
        "Domainname": "",
        "Entrypoint": null,
        "Env": [
            "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
        ],
        "ExposedPorts": null,
        "Hostname": "2338b11efbe1",
        "Image": "615c102e2290b70d38d89c03a1ad263da8bd8b05fb7fc8479174e5fd2215520e",
        "MacAddress": "",
        "Memory": 0,
        "MemorySwap": 0,
        "NetworkDisabled": false,
        "OnBuild": [],
        "OpenStdin": false,
        "PortSpecs": null,
        "StdinOnce": false,
        "Tty": false,
        "User": "",
        "Volumes": null,
        "WorkingDir": ""
    },
    "Created": "2015-01-16T21:25:30.510167396Z",
    "DockerVersion": "1.4.1",
    "Id": "b39b81afc8cae27d6fc7ea89584bad5e0ba792127597d02425eaee9f3aaaa462",
    "Os": "linux",
    "Parent": "615c102e2290b70d38d89c03a1ad263da8bd8b05fb7fc8479174e5fd2215520e",
    "Size": 0,
    "VirtualSize": 188305056
}
]

Le mot de la fin

Voilà j’espère que cette première approche vous a plu. Notre environnement de travail est maintenant prêt et nous allons pouvoir commencer à rentrer dans le vif du sujet, à savoir manipuler les conteneurs.

Lancer des conteneurs Docker

Préparation

Voilà donc où nous en étions :

$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
ubuntu              14.04               b39b81afc8ca        36 hours ago        188.3 MB

Sauf que finalement nous n’allons pas travailler sur une image d’Ubuntu, mais plutôt de Debian.

Plutôt que de travailler avec une image d’Ubuntu, je vais lui préférer une image de Debian, deux fois moins lourde.

# Je récupère la dernière image de Debian
$ docker pull debian:latest

Manipulation de nos premiers conteneurs

Mais au fait, c’est quoi déjà un conteneur ?

Pas de panique c’est très simple : un conteneur est une instance active d’une image. On lance un conteneur afin d’y exécuter un ou plusieurs processus, et ce avec la commande run.

$ docker run debian:latest /bin/echo 'Hello World!'
Hello World!

Félicitation, vous avez lancé votre premier conteneur !

Mais que s’est-il passé ?

Nous avons demandé à Docker de lancer un conteneur à partir de l’image debian:latest grâce à la commande run. Docker a donc cherché l’image debian:latest sur l’hôte local, ou l’aurait automatiquement téléchargée si elle n’avait pas été présente. Une fois le conteneur instancié, Docker a exécuté la commande /bin/echo 'Hello World!' à l’intérieur de celui-ci et nous a retourné le résultat : Hello World!

Qu’est-il arrivé à notre conteneur ensuite ?

Et bien Docker l’a tout simplement terminé. Retenez bien cela : la durée de vie d’un conteneur dépend directement du temps de vie des processus qui y sont exécutés ! Ici en l’occurrence, notre conteneur a « vécu » le temps que la commande /bin/echo 'Hello World!' s’exécute.

Essayons autre chose :

$ docker run -t -i debian:latest /bin/bash
root@82a7a75b7d25:/#

Nous avons passé deux options à la commande run : -t assigne un terminal à l’intérieur du conteneur, et -i nous autorise à nous y connecter en récupérant directement l’entrée standard (STDIN). Notre conteneur est donc lancé, et nous nous retrouvons avec un shell à l’intérieur. Essayez diverses commandes, vous verrez que c’est comme si vous étiez connectés à une Debian classique. Une fois vos expériences terminées, utilisez exit pour fermer le shell. La commande /bin/bash est terminée, et donc notre conteneur avec.

Des conteneurs qui exécutent des commandes et s’arrêtent une fois la commande terminée ? On m’explique l’intérêt ?

Ok, l’intérêt est limité pour le moment. C’était histoire de lancer un premier conteneur… Essayons de « daemoniser » notre Hello World! :

$  docker run -d debian:latest /bin/sh -c "while true; do echo hello world; sleep 1; done"
a0018e7b5dd3c93b056c4086ca811233881570058a2dd8011360a25e7e84f0b7

Le retour ressemble à tout sauf à « Hello World! ». C’est normal, remarquez que nous avons ajouté l’option -d, qui indique à Docker d’instancier un conteneur à partir de l’image debian:latest, et de le placer en arrière-plan, de le « daemoniser ». Le code retourné est juste l’identifiant unique de notre conteneur.

Tu veux dire que là j’ai un conteneur de démarré qui fait un /bin/echo 'Hello World!' toutes les secondes ?!

Exactement ! D’ailleurs, pour le vérifier, nous pouvons utiliser la commande docker ps, qui demande à Docker des informations à propos de tous les conteneurs en cours d’exécution.

$ docker ps
CONTAINER ID        IMAGE               COMMAND                CREATED             STATUS              PORTS               NAMES
a0018e7b5dd3        debian:latest       "/bin/sh -c 'while t   3 minutes ago       Up 3 minutes

Quelques remarques à propos du résultat :

  1. On retrouve bien l’ID de notre conteneur (a0018e7b5dd3)
  2. Ce conteneur a hérité d’un nom (determined_bell), automatiquement attribué par Docker, ceci afin de pouvoir manipuler notre conteneur plus facilement qu’avec son ID. Il est tout a fait possible de choisir un nom avec l’option --name
  3. Où sont passés les conteneurs précédents ? Ceux qui s’étaient arrêtés ?

L’option -a de la commande ps nous permet d’afficher tous les conteneurs, y compris ceux arrêtés :

$ docker ps -a
CONTAINER ID        IMAGE               COMMAND                CREATED             STATUS                      PORTS               NAMES
a0018e7b5dd3        debian:latest       "/bin/sh -c 'while "   9 minutes ago       Up 9 minutes                                    determined_bell    
02a9b1d3cc9a        debian:latest       "/bin/bash"            19 minutes ago      Exited (0) 19 minutes ago                       thirsty_elion        
c8367cbba7ff        debian:latest       "/bin/echo 'Hello W"   32 minutes ago      Exited (0) 32 minutes ago

Mais revenons à notre Hello World daemonisé. Le conteneur est bien lancé, mais fait-il ce que nous lui avons demandé ? Pour le vérifier, nous allons utiliser la commande logs :

$ docker logs determined_bell
hello world
hello world
hello world
hello world
hello world
hello world
hello world
hello world
hello world
hello world
hello world
hello world
hello world
hello world
hello world
[...]

Cette commande retourne la sortie standard (STDOUT) d’un conteneur donné.

Quelques commandes pour manipuler nos conteneurs

Je profite d’avoir un conteneur lancé pour vous parler de quelques commandes utiles. Par exemple, il est tout à fait possible de mettre un conteneur en pause !

$ docker pause determined_bell
determined_bell
$ docker unpause determined_bell
determined_bell

Celui-ci se figera tant qu’il n’est pas sorti de pause, via la commande docker unpause.

Pour arrêter un conteneur, nous avons la commande docker stop (ou kill si vous souhaitez passer un code particulier) :

$ docker stop determined_bell
determined_bell
$ docker -l
CONTAINER ID        IMAGE               COMMAND                CREATED             STATUS                       PORTS               NAMES
a0018e7b5dd3        debian:latest       "/bin/sh -c 'while t   26 minutes ago      Exited (-1) 47 seconds ago

Le conteneur est bien arrêté.

Il est aussi possible de redémarrer un conteneur arrêté. Prenons par exemple le conteneur où nous avions exécuté un shell interactif (il s’intitulait insane_kowalevski chez moi). Pour le démarrer, il y a la commande start

$ docker start insane_kowalevski
insane_kowalevski

La commande attach nous permet de nous reconnecter à ce conteneur :

$ docker attach insane_kowalevski
root@82a7a75b7d25:/#

Les commits sous Docker

Je vous propose de voir comment est-ce que nous pourrions « dockeriser » une application PHP classique.

Reprenons où nous en étions restés :

docker ps --all
CONTAINER ID        IMAGE               COMMAND                CREATED             STATUS                    PORTS               NAMES
a0018e7b5dd3        debian:latest       "/bin/sh -c 'while t"  3 weeks ago         Exited (-1) 3 weeks ago                       determined_bell  
82a7a75b7d25        debian:latest       "/bin/bash"            3 weeks ago         Exited (0) 3 weeks ago                        insane_kowalevski 
c8367cbba7ff        debian:latest       "/bin/echo 'Hello Wo"  3 weeks ago         Exited (0) 3 weeks ago

Nous nous étions amusés à lancer quelques conteneurs pour voir comment les manipuler. Comme base pour notre application web, qui se compose d’un serveur web, de PHP et d’une base de données, je vous propose de réutiliser le conteneur avec le shell. Et pour plus de commodité, je vais renommer ce conteneur en « sonerezh ».

docker rename insane_kowalevski sonerezh
docker start sonerezh
docker attach sonerezh
root@82a7a75b7d25:/#

Je travaille maintenant dans mon conteneur, les commandes passées à l’hôte seront précédées de admin@docker, et celles passées au conteneur par root@646c9d0d39a0:/#.

Mettons à jour notre image de Debian.

root@646c9d0d39a0:/# apt-get update && apt-get upgrade
[...]

Vous vous souvenez quand je vous avais dis que lorsque vous instanciiez un conteneur, Docker ajoutais au dessus de celui-ci une couche accessible en lecture et écriture ? Il est possible de voir les différences qui ont été apportées par rapport à votre image d’origine avec la commande :

admin@docker:~$ docker diff sonerezh
C /root
A /root/.bash_history
C /var
C /var/lib
C /var/lib/apt
[...]

Les lignes précédées d’un A représentent les ajouts, d’un C les modification et D les suppressions.

Je vous propose maintenant d’installer Nginx, avec un simple :

root@646c9d0d39a0:/# apt-get install nginx
root@646c9d0d39a0:/# service nginx start

Et c’est tout ! Nginx est démarré sur notre conteneur ! Vous ne me croyez pas ? Plusieurs moyens de le vérifier :

Depuis le conteneur :

root@646c9d0d39a0:/# ls -ld /proc/[0-9]* | grep www-data
dr-xr-xr-x 9 www-data www-data 0 Feb 10 20:51 /proc/598
dr-xr-xr-x 9 www-data www-data 0 Feb 10 20:51 /proc/599
dr-xr-xr-x 9 www-data www-data 0 Feb 10 20:51 /proc/600
dr-xr-xr-x 9 www-data www-data 0 Feb 10 20:51 /proc/601

Depuis l’hôte :

admin@docker:~$ docker top sonerezh
UID                 PID                 PPID                C                   STIME               TTY                 TIME                CMD
root                6294                6084                0                   21:38               pts/1               00:00:00            /bin/bash
root                6933                6294                0                   21:50               ?                   00:00:00            nginx: master process /usr/sbin/nginx
www-data            6934                6933                0                   21:50               ?                   00:00:00            nginx: worker process
www-data            6935                6933                0                   21:50               ?                   00:00:00            nginx: worker process
www-data            6936                6933                0                   21:50               ?                   00:00:00            nginx: worker process
www-data            6937                6933                0                   21:50               ?                   00:00:00            nginx: worker process

Et enfin, vous pouvez aussi le vérifier en accédant directement à l’adresse du conteneur :

admin@docker:~$ wget -q -O - http://172.17.0.4 
<html>
<head>
<title>Welcome to nginx!</title>
</head>
<body bgcolor="white" text="black">
<center><h1>Welcome to nginx!</h1></center>
</body>
</html>

Pour connaître l’adresse IP d’un conteneur :

root@646c9d0d39a0:/# ip -4 -o a show eth0
14: eth0    inet 172.17.0.4/16 scope global eth0\       valid_lft forever preferred_lft forever

Et côté conteneur, ou :

guillaume@docker:~$ docker inspect --format "{{.NetworkSettings.IPAddress}}" sonerezh
172.17.0.4

depuis l’hôte.

Je vous passe l’installation de PHP et compagnie puisqu’à partir de là tout est comme si vous étiez sur une Debian classique… puisque vous êtes sur une image de Debian !

À ce stade, nous avons donc un conteneur avec Nginx qui fonctionne. Sauf qu’il n’est pas très élégant de proposer un service de cette manière, directement avec l’adresse IP du conteneur, qui a fortiori, est difficilement accessible depuis une machine extérieure. L’idéal serait de pouvoir faire de la redirection de port. Que l’hôte Docker qui reçoit une requête la transmette directement au conteneur adéquat.

Que nous dit la documentation à ce propos ? Argh il aurait fallu utiliser l’option -p, ou --publish avec la commande docker run ! D’ailleurs si vous vous souvenez bien, il y a une colonne intitulée « PORTS » en sortie de la commande docker ps.

Han ça veut dire que je dois relancer un conteneur et me retaper toute la configuration de PHP ?! 0o

Oui… et non ! On peut très bien recommencer, mais on peut aussi transformer note conteneur en image, afin de pouvoir la réutiliser avec la commande run. Cette action s’appelle un commit, et n’est pas sans rappeler le commit de Git. Et les similitudes ne s’arrêtent pas là, regardez plutôt comment on fait pour créer un commit d’un conteneur :

admin@docker:~$ docker commit sonerezh sonerezh-nginx
ecb48406a489ba67885f9ec3fdd98b606da701791fa2db06646a84b766bda204

Une nouvelle image a été créé à partir des modifications que nous avons apporté à l’image debian:latest.

admin@docker:~$ docker images 
REPOSITORY          TAG                 IMAGE ID            CREATED              VIRTUAL SIZE
sonerezh-nginx      latest              ecb48406a489        About a minute ago   155.4 MB
debian              latest              4d6ce913b130        3 weeks ago          84.98 MB
ubuntu              14.04               b39b81afc8ca        3 weeks ago          188.3 MB

Cette image a été affectée par défaut du tag « latest », que vous pouvez modifier avec la commande tag.

admin@docker:~$ docker tag sonerezh-nginx:latest sonerezh-nginx:0.9.0-beta
guillaume@docker:~$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
sonerezh-nginx      latest              ecb48406a489        5 minutes ago       155.4 MB
sonerezh-nginx      0.9.0-beta          ecb48406a489        5 minutes ago       155.4 MB
debian              latest              4d6ce913b130        3 weeks ago         84.98 MB
ubuntu              14.04               b39b81afc8ca        3 weeks ago         188.3 MB

Oulala ! J’ai deux images Sonerezh de 155.4 MB du coup ! Mon stockage va diminuer très vite à ce rythme là.

Mais non pas de panique. Rappelez-vous ce que je vous avais dit lors de nos premiers pas avec les images : les tags sont seulement un moyen efficace et facile d’identifier une image, or si vous regardez l’ID de l’image sonerezh-nginx, c’est le même sur les deux lignes. On parle donc bien de la même image, qui ne prend qu’une fois 155.4MB.

D’ailleurs, il faut soustraire à ces 155.4 MB les 84.98 MB de l’image debian sur laquelle elle se base. Et oui ! Elles sont cela en commun !

admin@docker:~$ docker history sonerezh-nginx 
IMAGE               CREATED             CREATED BY                                      SIZE
ecb48406a489        9 minutes ago       /bin/bash                                       70.47 MB
4d6ce913b130        3 weeks ago         /bin/sh -c #(nop) CMD [/bin/bash]               0 B
d0a18d3b84de        3 weeks ago         /bin/sh -c #(nop) ADD file:4a58c36173b61e8a7b   84.98 MB
511136ea3c5a        20 months ago

Vous comprenez mieux pourquoi Docker est vraiment séduisant sur bien des aspects ? 🙂

Revenons maintenant à la redirection de port avant de refermer ce chapitre. Je vous le disais, nous allons utiliser l’option -p :

admin@docker:~$ docker run -d -p 80 sonerezh-nginx /usr/sbin/nginx -g "daemon off;"
2fb71867a9eabf18731352b20deda2da517c1e283ca7c904088d2ba02f51658e
admin@docker:~$ docker ps -l
CONTAINER ID        IMAGE                       COMMAND                CREATED             STATUS              PORTS                   NAMES
2fb71867a9ea        sonerezh-nginx:0.9.0-beta   "/usr/sbin/nginx -g    59 seconds ago      Up 58 seconds       0.0.0.0:49155->80/tcp   cranky_morse

On voit que le port 49155 de l’hôte Docker (ma machine) a été redirigé vers le port 80 du conteneur cranky_morse, qui est une instance de l’image sonerezh-nginx. Vous pouvez essayer d’accéder à votre machine depuis une autre, avec son IP, sur le port adéquat.

Par exemple mon hôte Docker ayant l’IP 192.168.100.51, je tape depuis une autre machine de mon réseau local :

wget -q -O - http://192.168.100.51:49155
<html>
<head>
<title>Welcome to nginx!</title>
</head>
<body bgcolor="white" text="black">
<center><h1>Welcome to nginx!</h1></center>
</body>
</html>

Pas mal non ? 😀

Le Docker Hub

À l’instar d’un GitHub où vous allez pouvoir envoyer et partager vos codes sources, le Docker Hub est un site en ligne qui va vous permettre d’envoyer et partager vos images. Il est au cœur de la communauté Docker puisque c’est ici que vous allez retrouver tous les dépôts et images officiels. Docker interroge d’ailleurs ce site lorsque nous utilisons les commandes docker pull ou docker search.

En plus d’être joli, le site propose tout un tas d’informations très précises sur les différentes versions d’images qui pourraient vous intéresser, par exemple pour Debian :

Dépôt officiel de Debian, sur le Docker Hub

N’hésitez pas à l’explorer 😉

Comme mes images ne sont pas tout à fait prêtes je ne vais pas les envoyer maintenant sur le Docker Hub, nous aurons tous le temps de voir cela une fois Sonerezh proprement dockerisée !

Conclusion pour aujourd’hui

Nous progressons tranquillement dans la mise en place de Sonerezh sous Docker. Nous avons une image, qui embarque Nginx, PHP et Sonerezh. Que nous manque-t-il ? Une base de données, qui fera l’objet d’un autre conteneur. Ça sera l’occasion de voir comment nous pouvons lier plusieurs conteneurs entre eux.

Ensuite Sonerezh a besoin d’accéder à des fichiers audios. Il faudrait que les conteneurs Sonerezh puissent accéder aux fichiers audios présents sur mon hôte. Ça sera là aussi l’occasion d’un prochain article où nous verrons comment gérer les volumes avec Docker.

Les liaisons sous Docker

Nous avions vu comment instancier une image Debian pour y ajouter notre application web (Nginx, PHP et le code source de l’application) pour enfin comprendre comment sauvegarder les changements apportés à l’image d’origine grâce aux commits.

Pour des raisons de simplicité je m’étais arrêté à la simple installation de Nginx pour vous montrer aussi comment faire dialoguer nos conteneurs avec le reste du monde grâce à la gestion de leurs ports d’écoute, et leur redirection entre conteneurs et l’hôte Docker.

Aujourd’hui nous allons terminer notre image Docker contenant Sonerezh et voir comment la lier à un conteneur qui offrira la base de données MySQL dont Sonerezh a besoin. Mais d’abord, revoyons comment j’ai créé l’image de Sonerezh.

Création de l’image Sonerezh

Cette section permet juste de faire une révision par rapport aux précédents articles, c’est pourquoi je ne commenterai pas ce que je fais. C’est en faisant qu’on apprend, et en rédigeant cet article je me suis rendu compte que le conteneur Nginx / PHP n’était pas le plus simple à implémenter pour faire une démonstration de ce genre. J’ai en effet raisonné au départ de manière à croire qu’un conteneur = une machine virtuelle. Or Docker n’est pas de la virtualisation dans le sens où j’y ai été habitué jusqu’à maintenant. Docker permet simplement de cloisonner des services. Un conteneur = un service.

Partant de ce principe, il m’aurait fallu un conteneur pour Nginx, un pour PHP et un pour MySQL. Ça commence à être un peu tordu pour débuter… On va donc rester sur le bon vieux Apache (qui embarque PHP par défaut) !

$ docker run -t -i debian:jessie /bin/bash
Unable to find image 'debian:jessie' locally
jessie: Pulling from debian
 
3cb35ae859e7: Downloading [===========================================>       ] 44.56 MB/51.36 MB
41b730702607: Download complete

root@1426b082ab71:/ apt update
root@1426b082ab71:/ apt upgrade
root@1426b082ab71:/ apt install -y php5-gd php5-mysql php5 apache2 libav-tools git vim
root@1426b082ab71:/ vim /etc/php5/fpm/conf.d/00-local.ini

[date]
date.timezone = Europe/Paris

root@1426b082ab71:/ mkdir /www && cd /www
root@1426b082ab71:/ git clone --branch master https://github.com/Sonerezh/sonerezh.git
root@1426b082ab71:/ chown -R www-data: sonerezh
root@1426b082ab71:/ chmod -R 775 sonerezh
root@1426b082ab71:/ vim /etc/apache2/sites-available/000-default.conf

<VirtualHost *:80>
    ServerName      localhost
    DocumentRoot    /www/sonerezh
 
    <Directory /www/sonerezh>
        Options -Indexes
        AllowOverride All
        Require all granted
    </Directory>
 
    CustomLog   /var/log/apache2/sonerezh-access.log "Combined"
    ErrorLog    /var/log/apache2/sonerezh-error.log
</VirtualHost>

root@1426b082ab71:/ a2enmod php5
root@1426b082ab71:/ a2enmod rewrite
root@1426b082ab71:/ service apache2 restart
root@1426b082ab71:/ exit

J’ai tout simplement déployé Sonerezh dans mon conteneur à base de Debian ! Premièrement : installation et configuration d’Apache, PHP et Git (notez que le Virtual Host Apache par défaut a été effacé). Ensuite je clone le dépôt GitHub de Sonerezh dans /www/sonerezh.

Maintenant que mon conteneur est prêt, mais à l’arrêt puisque je l’ai quitté, profitons-en pour en faire une image, grâce à un commit :

$ docker commit --author="Admin <admin@serveur-linux>" thirsty_mccarthy sonerezh/sonerezh:v0.9.0-beta
b3282df5ff60517432e44f56ede09f77cdda53d514b5aa0a91398fe0b4b0504d
$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED              VIRTUAL SIZE
sonerezh/sonerezh   v0.9.0-beta         8d7a0b0c64fb        About a minute ago   398.7 MB
debian              jessie              41b730702607        8 days ago           125.1 MB

Et voilà le travail ! Je peux maintenant transporter et déployer Sonerezh très facilement avec un simple docker run !

Non, il manque la base de données !

Exact, et c’est là que l’article prend tout son intérêt. Les applications que nous plaçons dans des conteneurs fonctionnent rarement seules, surtout en matière de web où il est très souvent nécessaire d’avoir une base de données à portée de main. Nous l’avons vu avec Docker il est très facile d’instancier des conteneurs et on se retrouver vite avec un bordel sans nom où il devient compliquer de faire communiquer tout ce petit monde.

Heureusement, Docker propose des mécanismes pour lier plusieurs conteneurs entre eux lorsqu’on les exécute. Comme cela se passe-t-il ? Tout bêtement avec les fichiers /etc/hosts des conteneurs, que Docker va aller modifier en fonction des arguments que nous passerons lors du lancement d’un conteneur. Exemple.

Lier plusieurs conteneurs entre eux

Récupérer une image MariaDB

Pour le coup je vais faire plus simple que précédemment en récupérant directement l’image de MariaDB sur le dépôt officiel.

$ docker run --name sonerezh-mariadb -e MYSQL_ROOT_PASSWORD=sonerezh -d mariadb:latest

Rappelez-vous l’option -d a placé le conteneur en arrière plan mais il fonctionne bien :

$ docker ps
CONTAINER ID        IMAGE               COMMAND                CREATED             STATUS              PORTS               NAMES
ae15f2411af2        mariadb:latest      "/docker-entrypoint.   52 seconds ago      Up 50 seconds       3306/tcp            sonerezh-mariadb

On remarque d’ailleurs qu’il écoute sur le port standard de MySQL (3306), on peut essayer de s’y connecter depuis l’hôte :

$ docker inspect --format "{{.NetworkSettings.IPAddress}}" sonerezh-mariadb
172.17.0.2
$ mysql -h172.17.0.2 -uroot -psonerezh
 
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MariaDB connection id is 4
Server version: 10.0.17-MariaDB-1~wheezy-log mariadb.org binary distribution
 
Copyright (c) 2000, 2015, Oracle, MariaDB Corporation Ab and others.
 
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
 
MariaDB [(none)]>

On pourrait même en profiter pour créer l’utilisateur sonerezh !

CREATE DATABASE sonerezh;
GRANT ALL PRIVILEGES ON sonerezh.* TO 'sonerezh'@'localhost' IDENTIFIED BY 'password';
FLUSH PRIVILEGES;
exit;

Lier les conteneurs sonerezh-app et sonerezh-mariadb

La liaison se fait avec l’option --link (ou -l) et prend en argument nom_du_conteneur:alias où là encore l’alias reste à votre appréciation. Nous allons relancer l’image préalablement créée en lui demandant d’y exécuter Apache (et donc Sonerezh), sans oublier la liaison avec sonerezh-mariadb. Oh et n’oublions pas de demander au conteneur Apache d’écouter le port 80 !

$ docker run --link sonerezh-mariadb:mariadb -d -p 80 --name sonerezh-app sonerezh/sonerezh:v0.9.0-beta /usr/sbin/apache2ctl -D FOREGROUND
163ceac2c1573590f1ad5bfabf85d81b92ceb666378c366addb7143ed17fee8a

Je vous laisse relire cette commande deux ou trois fois pour bien comprendre ce qu’il s’y passe.

$ docker ps
CONTAINER ID        IMAGE                           COMMAND                CREATED             STATUS              PORTS                   NAMES
1eaafc9d9f66        sonerezh/sonerezh:v0.9.0-beta   "/usr/sbin/apache2ct   3 seconds ago       Up 2 seconds        0.0.0.0:32771->80/tcp   sonerezh-app        
ae15f2411af2        mariadb:latest                  "/docker-entrypoint.   35 minutes ago      Up 35 minutes       3306/tcp                sonerezh-mariadb

Vérification

Quoi de mieux pour vérifier que de tenter d’installer Sonerezh ? D’après le ps ci-dessus, mon hôte Docker redirige son port 32771 vers le port 80 de mon conteneur sonerezh-app, celui-là même lié au conteneur sonerezh-mariadb.

J’accède donc, via mon navigateur, à mon hôte Docker (192.168.100.240:32771)

Comment mon conteneur sonerezh-app peut-il contacter le conteneur sonerezh-mariadb ? Grâce à l’alias que j’ai utilisé avec l’option --link. Pour l’installation j’ai donc comme configuration :

Ce que permet aussi --link

En fait, pour que ces alias fonctionnent, Docker est allé modifier le fichier /etc/hosts de mes conteneurs, voyez plutôt, ici sur sonerezh-app :

root@1eaafc9d9f66:/# cat /etc/hosts
172.17.0.3	1eaafc9d9f66
127.0.0.1	localhost
::1	localhost ip6-localhost ip6-loopback
fe00::0	ip6-localnet
ff00::0	ip6-mcastprefix
ff02::1	ip6-allnodes
ff02::2	ip6-allrouters
172.17.0.2	mariadb ae15f2411af2 sonerezh-mariadb

De plus, on trouve tout un tas de nouvelles variables d’environnements qui pourraient être très pratiques dans des scripts par exemple :

root@1eaafc9d9f66:/# env
MARIADB_ENV_MARIADB_VERSION=10.0.17+maria-1~wheezy
HOSTNAME=1eaafc9d9f66
MARIADB_PORT=tcp://172.17.0.2:3306
TERM=xterm
MARIADB_PORT_3306_TCP=tcp://172.17.0.2:3306
MARIADB_PORT_3306_TCP_PORT=3306
MARIADB_ENV_MYSQL_ROOT_PASSWORD=sonerezh
MARIADB_PORT_3306_TCP_PROTO=tcp
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
PWD=/
SHLVL=1
HOME=/root
MARIADB_NAME=/sonerezh-app/mariadb
MARIADB_ENV_MARIADB_MAJOR=10.0
MARIADB_PORT_3306_TCP_ADDR=172.17.0.2
_=/usr/bin/env

Pas mal non ? 🙂

Nous avons encore bien avancé dans la découverte de Docker et nous pouvons maintenant commencer à créer de belles petites architectures ! Maintenant que Sonerezh est prête sous forme d’image Docker, je vais pouvoir l’exploiter et la distribuer facilement via le Hub Docker ! Nous verrons tout cela un peu plus tard.

Avant cela, il va falloir que Sonerezh accède à ma musique. Or je ne vais pas copier ma musique dans mon conteneur cela n’aurait aucun sens. L’idéal serait que mon conteneur sonerezh-app puisse accéder à un dossier de mon hôte qui contiendrait ma musique.

La gestion des volumes sous Docker

la philosophie de Docker nous dit qu’un conteneur égal une application ou un service. Hors de question alors d’embarquer MariaDB, Apache et Sonerezh dans le même conteneur.

Ces containers sont volatiles et doivent en tout cas être conçus pour l’être. Comment dès lors avoir de la persistance de données ? Comment être sûr que si mon container MariaDB qui contient la base de données de mon application ne va pas faire disparaître les données qu’il contient si par mégarde je l’arrête, ou si j’ai besoin de le supprimer ?

Tout cela, Docker le gère à travers ce qui s’appelle les volumes de données.

Qu’est-ce que les volumes de données Docker ?

C’est tout simplement un dossier un peu spécial qui outrepasse la couche UnionFS de nos containers et permet le partage de données entre un ou plusieurs containers, et la persistance de celles-ci. On trouve plusieurs autres fonctionnalités intéressantes :

  • Les volumes sont initialisés à la création du container. Si l’image de base contient des données au point de montage spécifié, celles-ci sont copiés dans le volume lors de son initialisation
  • Les volumes de données peuvent être partagés entre plusieurs containers (très pratique pour créer des clusters de container par exemple)
  • Les changements apportés aux données du volume sont pris en compte immédiatement
  • Les données restent sur votre hôte même si le container vient à disparaître

Ça peut sembler un peu flou au premier abord mais vous allez voir qu’avec un exemple ça va aller beaucoup mieux.

Ajouter un volume de données

La gestion des volumes se fait avec l’option -v (pour --volume) des commandes docker run ou docker create. Évidemment, plusieurs -v peuvent être placés les uns après les autres en fonction du nombre de volumes souhaités. Ajoutons un volume à l’image de MariaDB par exemple :

$ docker run --name mariadb-test --volume /var/lib/mysql -d mariadb

Ici le volume /var/lib/mysql a été créé dans mon container. Il est par défaut accessible en lecture / écriture mais il est possible de restreindre ces accès en utilisant /var/lib/mysql:ro pour read-only.

Où trouver ce volume sur mon hôte ?

Toujours grâce à la commande inspect :

$ docker inspect mariadb-test
[...]
"Mounts": [
        {
            "Name": "0afd0...68d73",
            "Source": "/var/lib/docker/volumes/0afd0...68d73/_data",
            "Destination": "/var/lib/mysql",
            "Driver": "local",
            "Mode": "",
            "RW": true
        }
    ],
[...]

Où « Source » indique le dossier sur l’hôte et « Destination » le point de montage dans le container. « RW » indiquant si le volume est accessible en lecture / écriture.

Mais il est vrai qu’il n’est pas évident de travailler avec des volumes à si bas niveau. Qui voudrait s’embêter à fouiller dans /var/lib/docker pour trouver le volume qu’il cherche. On peut faire plus simple : créer un autre conteneur qui va monter ce volume automatiquement. Ce deuxième conteneur pourrait être un service de sauvegarde de MySQL par exemple, qui monterait le volume, réaliserait sa sauvegarde et se terminerait.

$ docker run --volumes-from mariadb-test debian:latest monscriptdesauvegarde.sh

Notez que les volumes sont toujours conservés, que les containers soient arrêtés ou non. En revanche le volume (et les données) est supprimé une fois que le dernier container y faisant référence l’est aussi.

Pas mal non ? Mouais pas mal. Mais dans le cas de Sonerezh, ma musique est dans /home/guillaume/Musique. Si je démarre un container Sonerezh avec un volume, il faudra que je copie toute ma musique dedans ?!

Non, c’est là que ça devient intéressant. Plutôt que de laisser Docker choisir l’emplacement du volume sur l’hôte, on peut spécifier un dossier en particulier. Dans le cas de Sonerezh, on va être capable d’indiquer à Docker de monter le dossier /home/guillaume/Musique de notre hôte sur le point de montage /music de notre container. L’application Sonerezh accédera donc à la musique via /music.

Et d’ailleurs ça se passe un peu comme les liaisons du chapitre précédent :

$ mkdir /home/guillaume/mariadb-volume
$ docker run --name mariadb-test --volume /home/guillaume/mariadb-volume:/var/lib/mysql -d -e MYSQL_ROOT_PASSWORD=pass mariadb
$ docker inspect mariadb-test
[...]
"Mounts": [
        {
            "Source": "/home/guillaume/mariadb-volume",
            "Destination": "/var/lib/mysql",
            "Mode": "",
            "RW": true
        }
    ],
[...]

Ici le dossier de ma /home est monté dans le container à /var/lib/mysql. D’ailleurs comme c’est ici que MySQL stocke toute ses données par défaut je devrais y trouver du contenu.

$ ls -l /home/guillaume/mariadb-volume
-rw-rw---- 1 999 docker  16K oct.  26 22:39 aria_log.00000001
-rw-rw---- 1 999 docker   52 oct.  26 22:39 aria_log_control
-rw-rw---- 1 999 docker  12M oct.  26 22:39 ibdata1
-rw-rw---- 1 999 docker  48M oct.  26 22:39 ib_logfile0
-rw-rw---- 1 999 docker  48M oct.  26 22:39 ib_logfile1
-rw-rw---- 1 999 docker    0 oct.  26 22:39 multi-master.info
drwx------ 2 999 root   4,0K oct.  26 22:39 mysql
drwx------ 2 999 docker 4,0K oct.  26 22:39 performance_schema

Tadaa !!!

C’est plus clair maintenant ? Je peux faire ce que je veux avec mes containers mariadb. Les arrêter, les supprimer, les mettre à jour… Tant que je continuerai de mapper /home/guillaume/mariadb-volume à /var/lib/mysql, mes données persisteront dans mes containers.

D’ailleurs à ce propos, si vous regardez la documentation de l’image Docker officielle de Sonerezh, vous voyez qu’il est conseillé de monter le volume /music, et de le faire correspondre à l’endroit où votre musique est stockée !

$ docker run --name sonerezh-app --link sonerezh-mariadb:mariadb \
                                 --volume /path/to/music:/music \
                                 --detach --publish 8080:80 \
                                 sonerezh/sonerezh:latest

Les containers de volumes de données

Il peut aussi y avoir d’autres cas où nous avons besoin de partager un volume entre un ou plusieurs containers. Il est conseillé dans ce cas d’utiliser un container de volume de données (« data volume container« ).

Ce container de volume de données se créé avec la commande docker create. Restons avec MariaDB et créons un nouveau container de type volume de données.

$ docker create --volume /var/lib/mysql --name mariadb-data mariadb /bin/true

Bien que ce container n’exécute aucune application (le docker ps ne renvoie rien), on réutilise quand même l’image de mariadb car n’oubliez pas que les couches de filesystem sont partagées entre tous les containers utilisant la même image pour gagner de la place.

On peut maintenant réutiliser l’option --volumes-from pour monter /var/lib/mysql dans un autre container :

$ docker run --name mariadb-test --volumes-from mariadb-data  -d -e MYSQL_ROOT_PASSWORD=pass mariadb 

Et un deuxième :

$ docker run --name mariadb-test2 --volumes-from mariadb-data  -d -e MYSQL_ROOT_PASSWORD=pass mariadb

Les containers mariadb-test et mariadb-test2 accèdent tous les deux aux mêmes données puisqu’ils montent le même volume créé dans mariadb-data.

Sauvegarde, restauration et migration de volumes

L’avantage d’utiliser les containers de volume, c’est que nous pouvons facilement sauvegarde, exporter et restaurer nos données !

$ docker run --volumes-from mariadb-data -v $(pwd):/backup ubuntu tar cvf /backup/backup.tar /var/lib/mysql

Hein ?! Il se passe quoi là ?!

  • On crée un container qui va monter le volume présenté par mariadb-data que nous avions créé un peu plus tôt
  • L’option -v nous permet de lier le dossier /backup du container au dossier courant de notre hôte ($(pwd))
  • Ce container utilise l’image ubuntu
  • Et y exécute la commande tar cf /backup/backup.tar /var/lib/mysql

Comme /backup = $(pwd), l’archive se retrouve directement sur notre hôte !

$ ls -l *.tar
-rw-r--r-- 1 root root 119644160 nov.   1 12:45 backup.tar

Pour restaurer sur un autre hôte par exemple, il faut faire la procédure inverse :

# 1. On recrée le container de volume
$ docker create --volume /var/lib/mysql --name mariadb-data2 mariadb /bin/true
# 2. On y décompresse l'archive
$ docker run --volumes-from mariadb-data2 -v $(pwd):/backup ubuntu tar xvf /backup/backup.tar

Et le tour est joué !

Où héberger ses applications sous Docker ?

Depuis la progression rapide de Docker, de multiples hébergeurs proposent de stocker vos conteneurs, en voici une liste :

Conclusion

Maintenant que vous savez tout des images, des containers, des liens et des volumes, vous pouvez commencer sérieusement à vous amuser sur Docker.

Merci à Guillaume pour la rédaction !

Pour marque-pages : Permaliens.

Laisser un commentaire