Docker pour les nu... pour les débutants

Suite aux deux excellents articles de Michael Pagès sur Docker (Docker présentation – Part1, et Docker  – Tutoriel : Isolation d’application par Projet – Part 2), j’ai eu envie d’essayer d’utiliser cet outil. Cependant, j’ai rencontré quelques difficultés lors de la mise en oeuvre, avec des résultats pas toujours voulus, ou bien de longues minutes à essayer de comprendre comment faire telle ou telle chose. Cet article n’est donc pas destiné à vous expliquer comment fonctionne docker, mais plutôt : “Comment l’utiliser”.

Principes de base

Pourquoi utiliser Docker ?

Docker est un outil permettant de construire et de distribuer rapidement un ensemble d’applications (par exemple des serveurs pré-configurés dans le cadre d’un projet). Il permet ensuite de partager ce système entre différentes personnes, afin que tout le monde puisse travailler sur les mêmes bases (éviter de prévoir une demi-journée pour installer l’environnement, demander à tout le monde s’il a un dump à jour de la base, etc.). Ce partage se fait via la distribution d’un fichier de configuration (qui contient toutes les informations pour reconstruire l’environnement cible) ou directement d’une image (comme une VM, mais en plus léger). Concrètement, Docker est bien pour :

  • Partager un environnement de travail entre plusieurs personnes
  • Tester des choses que l’on n’oserait pas faire sur son propre système
  • Garder son système hôte propre, en installant tout sur Docker
  • Avoir des versions spécifiques d’une librairie, d’un serveur, d’une base de données, etc…, par projet

Images, Dockerfiles, et conteneurs

Votre système d’exploitation est majoritairement composé de 2 choses : un système de fichiers, et des processus. Une image Docker représente le système de fichiers, sans les processus. Elle contient tout ce que vous avez décidé d’y installer (Java, une base de donnée, un script que vous allez lancer, etc…), mais est dans un état inerte. Les images sont créées à partir de fichiers de configuration, nommés “Dockerfile”, qui décrivent exactement ce qui doit être installé sur le système. Un conteneur est l’exécution d’une image : il possède la copie du système de fichiers de l’image, ainsi que la capacité de lancer des processus. En gros, c’est un OS, avec lequel vous pouvez interagir. Dans ce conteneur, vous allez donc pouvoir interagir avec les applications installées dans l’image, exécuter des scripts, faire tourner un serveur, etc. Pour faire l’analogie avec le monde Java (ou le monde des langages objets en général) :

  • le “Dockerfile” est votre fichier source (vous décrivez ce que vous voulez)
  • l’image est le fichier compilé (gardé au chaud en attendant d’être utilisé)
  • le container est une instance de votre classe (vous pouvez changer ses propriétés, et appeler des méthodes)

Un “Dockerfile” peut être inclus dans d’autres “Dockerfile”, et être à la base de plusieurs images différentes. Par exemple, si tous vos projets utilisent MySQL comme SGDB, vous pouvez créer un “Dockerfile” qui installe MySQL, et ensuite créer un autre “Dockerfile” pour chacun de vos projets. Si vous mettez à jour votre “Dockerfile” MySQL, vos images projets pourront êtres mises à jour en même temps. Vous pouvez aussi utiliser une même image pour créer plusieurs conteneurs différents, mais avec les mêmes propriétés (pensez aux instances de classes).

Boot2Docker

Docker existe actuellement uniquement sur Linux. Pour les utilisateurs de Windows et Mac OS, il faut passer par Boot2docker, qui est en fait une VM linux permettant de faire tourner Docker. Cela n’est pas vraiment un problème (très faible consommation mémoire), mais il faut garder en tête que certaines choses décrites dans ce post seront un peu différentes dans ce cas (mais je vais les indiquer). Cela concerne par exemple :

  • le partage de volumes
  • l’interaction avec votre conteneur

En pratique (commandes de base)

Récupérer une image pour lancer un conteneur avec pseudo-terminal

Si vous avez bien tout suivi jusque là, il nous faut une image pour pouvoir créer un conteneur (et donc pouvoir vraiment utiliser Docker). L’installation de Docker est décrite ici, et ne fait pas partie du périmètre de cet article. Vous pouvez créer une image vous-même (mais nous verrons cela plus tard), ou bien en utiliser une existante, en la recherchant sur le site https://registry.hub.docker.com. Pour le moment, nous allons simplement utiliser une image “ubuntu” (il s’agit d’une version très light d’Ubuntu). Pour la récupérer, vous pouvez lancer la commande suivante :

$> docker pull ubuntu:trusty

Cela va télécharger l’image en locale, et vous pourrez ensuite lancer un conteneur. D’ailleurs, si vous lancez un conteneur sans avoir l’image sur votre machine, elle sera téléchargée automatiquement à partir du hub Docker (donc la commande ci-dessus n’est utile que pour préparer votre système, si par exemple vous comptez utiliser le conteneur sans internet par la suite). Pour lancer votre conteneur avec un terminal, vous pouvez exécuter la commande suivante :

$> docker run -ti –rm ubuntu:trusty

Le paramètre ‘-t’ permet d’avoir un pseudo-terminal (pour exécuter des commandes dans le conteneur une fois lancé). Le paramètre ‘-i’ permet d’activer le mode interactif, qui redirige tous les messages sur les sorties standards (permet de voir les logs). Le paramètre ‘–rm’ permet de supprimer automatiquement le conteneur à la fin de l’exécution.

Tip : Vous avez sûrement remarqué que le nom de l’image est postfixé de ‘:trusty’. Cela indique la version que l’on veut de l’image. Il est recommandé de toujours l’indiquer afin d’être sûr d’obtenir le même résultat dans le futur. Ici, ‘Trusty’ est la dernière version d’Ubuntu à la date de ce post.
Une fois la commande précédente lancée, vous pourrez donc interagir avec votre système comme bon vous semble. Par exemple, installer MySQL avec ‘apt-get’.
Attention : A cause du paramètre ‘–rm’ de la commande précédente, les commandes que vous effectuez ne sont pas sauvegardées une fois que vous sortez du conteneur, car ce dernier sera effacé. Nous discuterons de cette option plus loin dans le post.

Lancer une image en arrière-plan (deamon)

Vous pouvez aussi choisir de lancer une image en arrière plan (c.a.d. sans terminal), comme par exemple un serveur sur lequel vous voulez vous connecter. Dans ce cas, il faut utiliser le paramètre ‘-d’ au lieu de ‘-ti’. De plus, le paramètre ‘–rm’ n’est pas supporté par ce mode.

$> docker run -d ubuntu:trusty <a_script_that_starts_the_server>

En dernier paramètre de la commande, vous pouvez passer une commande à exécuter (par exemple, démarrer le serveur). Votre conteneur tournera en fond, jusqu’à ce qu’il ait fini sa tâche, ou que vous l’arrêtiez vous-même.

Voir les conteneurs existants

Il peut être intéressant de voir les conteneurs existants (ceux que vous avez créé), ainsi que leur état (démarré/arrêté). Pour cela vous pouvez lancer la commande suivante :

$> docker ps

Cette commande affiche tous les conteneurs en train de s’exécuter. Pour voir tous les conteneurs créés, rajoutez l’option ‘-a’ :

$> docker ps -a

L’affichage sera de la sorte :

CONTAINER ID  IMAGE                     COMMAND                CREATED      STATUS                      PORTS                    NAMES 61f894ce37c9  ubuntu:14.04              "/bin/echo Test"       2 days ago   Exited (0) 10 minutes ago                            elegant_yonath 4ce8a706b140  busybox:latest            "true"                 2 days ago   Exited (0) 22 hours ago                              d_sg 5f8e966087f8  afillatre/mysql:latest    "/bin/bash /opt/star   3 days ago   Up 33 hours                 0.0.0.0:3306->3306/tcp   mysql f2a1b71c6ded  svendowideit/samba:latest "/setup.sh --start d   3 days ago   Up 35 hours                 0.0.0.0:137->137/tcp     samba-server a0e84b157ce7  busybox:latest            "true"                 3 days ago   Exited (0) 35 hours ago                              d_mysql 268dcfc741d4  busybox:latest            "true"                 4 days ago   Exited (0) 44 hours ago                              d_ccip

Voilà le détail des colonnes :

  • CONTAINER ID : Identifiant technique du conteneur (mais on utilisera plus souvent le nom)
  • IMAGE : Identifiant technique de l’image utilisée pour créer le conteneur. La version de cette image est aussi indiquée
  • COMMAND : Commande passée en paramètre lors de la création du conteneur
  • CREATED : Date de création du conteneur
  • STATUS : Etat du conteneur (En cours d’exécution, ou arrêté)
  • PORTS : Les ports NATés (redirections de ports entre le conteneur et le système hôte (ou boot2docker le cas échéant)). Nous en parlerons plus tard dans ce post
  • NAMES : Noms aléatoires données aux conteneurs. Il est possible de préciser le nom vous-même en ajoutant le paramètre ‘–name ’ à la commande ‘docker run …’

C’est grâce à cette commande que nous pourrons :

  • démarrer ou arrêter des conteneurs
  • supprimer des conteneurs
  • afficher les logs d’un conteneur
  • etc.

Arrêter / re-démarrer un conteneur

Une fois le conteneur créé (et s’il a été créé sans l’option ‘–rm’) vous pouvez interagir avec lui. Par exemple, pour couper votre serveur, il faut lister tous les processus en cours (‘docker ps’), puis exécuter la commande suivante :

$> docker stop <container_id_or_name>

Pour redémarrer ce conteneur, rien de plus simple :

$> docker start <container_id_or_name>

Attention : Lorsque vous re-démarrez un conteneur, il est re-lancé avec les mêmes options que lors de sa création (mode, ports, volumes, etc.). Vous ne pouvez plus les changer. C’est pour cela qu’il est parfois préférable de créer des conteneurs temporaires (avec le paramètre ‘–rm’) lorsque cela est possible.

Rediriger des ports

Par défaut, vous ne pouvez pas accéder aux applications que vous lancez dans le conteneur car elles sont cloisonnées : les ports ne sont accessibles que localement. Pour pouvoir interagir avec vos applications, il faut faire des re-directions de ports entre le conteneur et la machine hôte. Par exemple, dire que pour un conteneur A qui expose une application sur le port 8080, on veut pouvoir y accéder sur le port 8090.

Pour cela, il faut ajouter le paramètre ‘-p’ au lancement de votre conteneur. Si l’on reprend l’exemple suivant :

$> docker run -d -p 8080:8090 ubuntu:trusty <a_script_that_starts_the_server_on_port_8080>  

Tip : Vous pouvez utiliser plusieurs fois le paramètre ‘-p’ si vous avez plusieurs ports à rediriger
Vous pouvez alors accéder à l’application à l’adresse suivante : [http://localhost:8090](http://localhost:8090)
Attention : Pour les utilisateurs de boot2docker, la redirection de port se fait en fait du conteneur à la VM boot2docker. Vous devez donc utiliser son ip (indiquée au démarrage de boot2docker, ou en exécutant la commande suivante ‘$> boot2docker ip’ sur votre système hôte) : http://:8090
Gardez cependant bien en tête que vous pouvez tout à fait lancer plusieurs fois le même serveur sur le même port, dans des conteneurs différents (par exemple, plusieurs projet avec un apache Tomcat écoutant sur le port 8080). Dans ce cas, c’est au niveau de votre système hôte qu’il faudra faire les bonnes re-directions. Par exemple, ‘-p 8080:8090’ pour le conteneur 1, ‘-p 8080:8091’ pour le conteneur 2, etc.

Partager ses données avec le système hôte

Vous l’avez sûrement remarqué, à chaque fois que vous créez un conteneur, vous perdez toutes les modifications préalablement apportées, car le conteneur est re-créé à partir de l’image Docker. Vous pourriez simplement utiliser toujours le même conteneur (commande ‘start/stop’ plutôt que ‘run’), mais dans ce cas, vous ne pourriez plus changer sa configuration (le lancer en mode terminal/deamon, ajouter/supprimer des re-directions de ports, etc.).

Un meilleur moyen de procéder est de créer un conteneur dédié pour recevoir les données. Ce conteneur dédié ne fera rien, à part contenir un dossier où vous pourrez écrire et lire. Du coup, jamais besoin de changer sa configuration, donc de perdre ses données. Nous allons donc créer ce conteneur avec un volume, c’est à dire un dossier partagé. C’est ce dossier qui sera utilisé par notre conteneur principal pour stocker ses données :

$> docker run -v /myData:<path_on_host_machine> –name d_my-data busybox true

Le paramètre ‘-v /myData:<path_on_host_machine>’ crée un dossier ‘myData’ à la racine du conteneur. Toutes les données écrites dans ce dossier le seront aussi dans le dossier de la machine hôte, défini par le chemin ‘<path_on_host_machine>’

Le paramètre ‘–name d_my-data’ donne le nom ‘d_my-data’ à notre conteneur. C’est purement pratique, pour pouvoir accéder à ce conteneur plus facilement via un nom défini par nous-même.

Une fois ce réceptacle créé, nous allons créer notre conteneur principal en lui disant d’utiliser le volume du conteneur ‘d_my-data’ :

$>  docker run -d -p 8080:8090 –volumes-from d_my-data ubuntu:trusty <a_script_that_starts_the_server_on_port_8080>

A partir de maintenant, toutes les données créées dans le dossier ‘/myData’ seront sauvées sur votre disque.

Attention : Pour les utilisateurs de boot2docker, le dossier partagé est créé sur la VM boot2docker. Il vous faudra donc un serveur samba pour accéder aux dossiers : https://github.com/boot2docker/boot2docker#folder-sharing. Notez que vous ne pourrez accéder qu’aux volumes d’un seul conteneur à la fois (une nouvelle connexion samba ferme la précédente). Il existe d’autres solutions (comment installer du SSHFS par exemple, mais cela n’est pas spécialement recommandé, et ne sera pas traité dans cet article).

Attention : Le montage d’un volume d’un autre conteneur se fait au “runtime”, c’est à dire lorsque le conteneur est en train de fonctionner. Ainsi vous pouvez déclarer des volumes dans votre fichier “Dockerfile”, mais pas utiliser d’autres conteneurs. Si cela était possible, vos fichiers “Dockerfile” ne seraient plus portables, car ils dépendraient de l’organisation du système hôte. Si vous voulez installer des choses dans un répertoire monté, le mieux est d’utiliser un script, exécuté au lancement du conteneur.

Création de vos propres images

Pour créer vos propres images, vous pouvez utiliser un fichier Dockerfile. Le détail de ce fichier est expliqué ici. Je vous conseille aussi de regarder comment sont faits ces fichiers sur le Regitry de Docker (là où vous pouvez chercher des images).

Exemple d’architecture de projet

Afin de mettre en place tout ce que nous avons vu plus haut, voilà un exemple simple de projet :

  • une base MySQL
  • un serveur Apache Tomcat
  • le code source

La solution indiquée ici n’est qu’un exemple, et il n’est absolument pas nécessaire de faire de la sorte pour utiliser Docker. Chacun pourra organiser ses projets comme bon lui semble, mais l’idée est juste ici de montrer ce que l’on peut faire.

Installation de la base de données

Vous avez surement plusieurs projets qui utilisent MySQL (ou une base de données). Il est possible d’installer MySQL sur chacun de vos conteneurs, ou bien d’avoir un conteneur dédié. De cette manière, tous vos projets stockeront leurs données sur le même conteneur. Cela permet de rassembler un maximum de données, et de faciliter la gestion (un seul et même conteneur à lancer quel que soit le projet, à condition que la version de la BDD voulue soit similaire).

Pour trouver une image MySQL, vous pouvez chercher ici : https://registry.hub.docker.com/search?q=mysql&searchfield=

Une fois votre image choisie, suivez les instructions. Si vous regardez les “Dockerfile” de ces images, vous verrez qu’elles créent toutes des volumes pour sauvegarder les données. Ainsi, vous pouvez créer de nouveaux conteneurs à partir de ces images, et vos données seront toujours conservées.

Installation du serveur Tomcat

C’est notre conteneur principal. Nous allons au préalable créer un autre conteneur avec un volume, dans lequel seront placées les données que nous voulons sauvegarder. Ensuite, nous allons créer notre conteneur Tomcat, télécharger le serveur, et inclure un script de déploiement (pour rappel, nous ne pouvons pas monter un volume externe dans l’image).

En supposant que vous passiez par un Dockerfile, dans le répertoire courant :

$> docker run -v /myProjectData –name d_my-project-data busybox true
$> docker build –rm -t ippon/myproject . # Donne le nom ‘ippon/myproject’ à l’image en cas de construction réussie
$> docker run -ti -p 8080:8090 -p 8443:8493 –rm –name my_project –volumes-from d_my-project-data ippon/myproject

Une fois le conteneur lancé, vous pouvez installer votre server avec un script de déploiement, dans le dossier /myProjectData. Il est aussi possible d’automatiser complètement cela, et d’installer et lancer le serveur au démarrage du conteneur. Dans ce cas, il vous faudra utiliser l’option ‘-d’ à la place des options ‘-ti –rm’ pour que le conteneur tourne en arrière plan, et soit indépendant.

Code source

Le code source de l’application que vous allez déployer sur votre server Tomcat peut rester sur votre système hôte. Ainsi, vous n’aurez pas besoin de lancer un conteneur pour y travailler, ni de monter un partage Samba pour les utilisateurs de Boot2docker.

Conclusion

Cet article ne couvre qu’une petite partie de Docker. Il existe beaucoup plus de commandes et de possibilités pour utiliser cet outil. Cependant, je voulais me concentrer sur l’explication des parties qui me semblent vitales pour l’utiliser. Avec cette base, vous ne devriez pas avoir de problème pour vous reporter à la documentation officielle et compléter la liste des commandes possibles.