Sélectionner une page

Introduction

Docker est un gestionnaire de conteneurs très utilisé de nos jours. Il n’est pas le seul qui existe, mais c’est certainement le plus célèbre, et est adopté par la majorité des entreprises et projets open-source.

Ce tutoriel vous permettra d’acquérir les connaissances de base pour pouvoir utiliser Docker en tant que développeur, mais également à comprendre l’intérêt d’un tel système et ses usages.

De la théorie…

Docker en bref

Docker repose sur le principe de virtualisation, mais de manière plus souple, et plus optimisée.

La virtualisation permet de démarrer plusieurs systèmes d’exploitation sur un même serveur physique. Cela présente l’avantage de pouvoir isoler différentes applications qui pourraient entrer en conflit. Par exemple, si l’on ne dispose que d’un serveur physique, et de deux applications développées en PHP (l’une compatible PHP 5.6, et l’autre PHP 7.3 uniquement), la cohabitation devient compliquée.

Les conflits peuvent être nombreux lorsqu’on souhaite faire cohabiter plusieurs applications, et la virtualisation permet de les isoler simplement. On parle alors de VM (« Virtual Machine » en anglais, soit « Machine Virtuelle« ).

L’idée de base de Docker est très similaire, et ce même si d’un point de vue technique les deux sont très différents.

En effet, Docker permet de créer ce qu’on appelle des “Containers”. Un container est l’équivalent d’une VM. Il est basé sur un OS (Système d’Exploitation précis), et est isolé. Mais en terme d’automatisation, on va pouvoir aller beaucoup plus loin avec Docker.

Il n’y a pas d’intérêt à installer un système d’exploitation s’il n’y a rien à installer dessus. C’est pourquoi, lorsqu’on déploie un serveur Web par exemple, on poursuit en installant divers paquets (ex: Apache, MySQL, PHP, …). S’en suit également de la configuration (définition des droits, virtual hosts, …).

Avec Docker, on va pouvoir écrire toutes ces opérations dans un fichier nommé “Dockerfile”. Il s’agit en quelque sorte d’une documentation expliquant comment installer l’application.

Si on souhaitait rédiger une documentation pour installer une application Symfony sur un serveur en prod, à la main, cela pourrait ressembler à quelques chose comme :

  1. Installer un serveur Debian 10
  2. Installer apache et php (apt-get update && apt-get install apache2 php php-common …)
  3. Créer un fichier “myapp.conf” dans le dossier “/etc/apache2/sites-available/”

Et si au lieu de dire au technicien quoi faire pour installer l’application, on codait un script qui s’occuperait de faire toutes ces manipulations ? C’est exactement l’idée derrière le Dockerfile.

Exemple :

FROM debian:buster
RUN apt-get -y update \
  && apt-get install apache2 php php-common

COPY php.ini /usr/local/etc/php/php.ini

# ...

Ce fichier permet de dire à Docker ce qu’il devra faire pour déployer le container (l’environnement). Il suffit d’un simple fichier “Dockerfile” pour pouvoir démarrer un container, depuis n’importe où (sur un serveur, un poste utilisateur tournant sur linux, ou même Windows ou Mac).

Bien sûr, il est possible de joindre des fichiers/dossiers comme dans l’exemple plus haut (pour le fichier php.ini par exemple), afin que ceux-ci soient utilisés lors du déploiement du container. De cette manière, on peut joindre des fichiers de configurations par exemple.

Au final, pour pouvoir utiliser cette application, il suffit de :

  • Disposer d’un hôte (un serveur, ou un ordinateur tournant sur Linux/Windows/MacOS)
  • Installer Docker
  • Créer un fichier “Dockerfile” (et les éventuels fichiers/dossiers complémentaires)

Quel genre d’application ?

N’importe quel type d’application. Il peut s’agir d’une application effectuant un “Hello World” en bash, ou bien d’une application Web à part entière. Certaines applications vont donner lieux à des containers “éphémères”. C’est-à-dire que le container n’existe que durant l’exécution de l’application, laquelle se termine après avoir affiché “Hello World” par exemple. D’autres applications vont tourner “indéfiniment”, comme une appli web par exemple.

Et les conflits dans tout ça ?

Pour éviter les conflits entre deux appli PHP fonctionnant avec des versions différentes, il me suffit de créer deux container distincts ? Donc deux “Dockerfile” (l’un installant PHP 5.6 et l’autre PHP 7.3) ?

Exactement ! La logique est en réalité la même qu’avec de la virtualisation où il suffirait de créer deux VMs distinctes.

Et si on factorisait ?

Je me retrouve donc avec deux “Dockerfile”, mais ceux-ci sont similaires en tout point à l’exception de la version de PHP. On ne pourrait pas isoler Apache également ?

Oui mais… le fait de créer plusieurs containers permettrait d’installer Apache une seule fois dans un container, et les deux versions de PHP dans des containers différents. Sauf que le déploiement n’est pas très pratique… Heureusement, Docker-Compose est là !

A quoi ça sert concrètement ?

Le but principal de Docker est de permettre le déploiement d’une application en une seule commande, sans autres prérequis que Docker. Par exemple, si vous avez créé un site internet qui tourne sous PHP, et qui nécessite l’installation d’un serveur HTTP comme Apache, sans Docker vous devriez installer PHP, les différentes extensions nécessaires de PHP, Apache, et modifier les fichiers de configuration (php.ini, le virtual host, etc…). Avec Docker, vous pouvez permettre le déploiement d’un container, déjà configuré avec tous ces aspects là.

Docker-Compose

Ce petit outil permet simplement de créer des applications “multi-containers”.

Prenons un exemple simple : une application Web basée sur Apache, PHP 7.3, et MySQL.
Grâce à Docker-Compose, on va pouvoir créer 3 containers différents, un pour chaque application.

Pour chaque application (Apache, PHP 7.3, MySQL), on créera un Dockerfile. Le Dockerfile du container “Apache” permettra d’installer Apache, et potentiellement de configurer des virtual hosts. Le Dockerfile du container “PHP 7.3” permettra d’installer PHP 7.3, et les extensions nécessaires. Etc…

On peut également choisir d’installer Apache et PHP sur le même container. Tout dépend de l’architecture que vous avez choisie pour votre application.

Des images, pas des containers !

Depuis un moment, je parle de containers, mais le but du “Dockerfile”, c’est de créer une image. Depuis une image, nous pouvons en réalité créer autant de containers qu’on le souhaite. Si on veut, on peut créer deux containers, tous deux basés sur l’image “Apache”.

Pour créer une image, on part d’un Dockerfile. Avec la commande “build”, Docker génère une image, qui pourra être utilisée pour déployer l’application autant de fois qu’on le souhaite. Un container est donc une “instance” d’une image.

Ces images sont intéressantes, car elles peuvent être exportées/importée facilement, et donc partagées. De plus, elles peuvent être suffisamment génériques pour répondre à de nombreux problèmes. On peut par exemple créer une image “PHP”, laquelle pourra être utilisée pour démarrer des containers PHP sous n’importe quelle version. Lors du déploiement du container, il suffirait de passer la version de PHP désirée en paramètre.

Dernier bonus : les images peuvent être héritées ! Oui, comme les classes en programmation. La commande “FROM” du Dockerfile sert à cela. Ainsi, on peut se baser sur une image existante, avec certains packages déjà installés, afin d’en ajouter d’autres.

Docker Hub

Docker Hub est tout simplement un dépôt pour les images Docker. Si vous créez une image “PHP”, une autre “Apache”, etc… Vous pouvez les stocker sur Docker Hub afin qu’elles soient accessibles de manière publique, ou privée.

Lorsqu’on souhaite créer un container, on peut bien sûr utiliser une image créée/récupérée manuellement. Mais ces images sont généralement elles-mêmes basées sur d’autres images (lesquelles peuvent également être basées sur d’autres images). Toutes ces images sont donc référencées sur Docker Hub, soit l’annuaire public de référence pour les images Docker.

A la pratique

Le Dockerfile

Rappelez-vous, il s’agit de la “notice d’installation”. On va donc écrire dans ce fichier tout ce qui permet de mettre en place l’environnement désiré.

Exemple pour un serveur Web sous CentOS 7 :

# Notre image se base sur CentOS 7 (une autre image hébergée sur Docker Hub)
FROM centos:7

# On définit la séquence de commande à exécuter
RUN yum -y update \
    && yum install httpd
# Deuxième séquence de commande à exécuter
RUN systemctl start httpd \
    && systemctl enable httpd \
    && firewall-cmd --zone=public --permanent --add-service=http \
    && firewall-cmd --reload

Créer une image

Maintenant que nous avons créé un Dockerfile, il faut générer l’image correspondante. Il suffit de se rendre dans le dossier contenant le Dockerfile, est d’exécuter la commande suivante :

docker build -t httpd .
  • Le point “.” correspond au chemin du dossier de référence (puisqu’on est dans ce dossier, “.” se réfère au dossier courant). Cela permet de définir le contexte afin de pouvoir utiliser des instructions telles que “COPY”.
  • L’option “-t” permet de définir le nom de l’image que l’on souhaite créer (au format “namespace/name:tag)

Une fois l’image créée, on peut lister les images connues par Docker avec la commande : 

docker images

Créer un container

Maintenant que nous avons une image à disposition, nous pouvons créer un container basé sur cette image. Voici un exemple permettant de faire tourner Apache en arrière plan, sur le port 80 :

docker run -d -p 80:80 httpd

Vous pouvez maintenant tester que tout fonctionne bien en vous rendant sur http://localhost/

Docker-Compose

Docker-Compose permet de démarrer une application faisant appel à plusieurs containers. Il se base sur un fichier principal, nommé “docker-compose.yaml”. C’est dans ce fichier que l’on va définir tous les containers nécessaires pour notre application, et la configuration de chacune d’entre elles.

Exemple d’un “docker-compose.yml” permettant à une application de fonctionner à la fois sur PHP 7.1 et PHP 5.6 :

version: '3'
services:

    db:
        image: mysql:5
        ports:
          - "3306:3306"
        environment:
            MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
            MYSQL_USER: 'root'
        volumes:
          - ./var/data/mysql:/var/lib/mysql

    phpmyadmin:
        image: phpmyadmin/phpmyadmin
        ports:
            - "8080:80"
        depends_on:
            - db
        environment:
          PMA_HOST: db

    php_56:
        build: images/php_56
        ports:
          - "8056:80"
        volumes:
          - ./config/vhosts:/etc/apache2/sites-enabled
          - ./var/www:/var/www

    php_71:
        build: images/php_71
        ports:
          - "8071:80"
        volumes:
          - ./config/vhosts:/etc/apache2/sites-enabled
          - ./var/www:/var/www

Dans cette exemple, notre application est composée de 4 services : un serveur MySQL, un PHPMyAdmin, un serveur Apache avec PHP 5.6 et un serveur Apache avec PHP 7.1.

Ici, le fichier “docker-compose.yml” seul ne suffit pas, il est nécessaire de joindre un dossier “var/data/mysql” afin de stocker les données du serveur MySQL, un dossier “config/vhosts” qui sera partagé avec Apache pour la définition des virtual hosts, et un dossier “var/www” contenant le code de l’application PHP.

L’avantage principal de cette composition est que l’on peut tester notre application simultanément avec deux versions différentes de PHP, et ce en changeant seulement le port dans l’URL (8056 pour PHP 5.6 et 8071 pour PHP 7.1). On peut exécuter le fichier index.php en tapant par exemple “http://localhost:8071/index.php”, ce qui l’exécuterait avec PHP 7.1. Parfait pour un environnement de développement !

De même, la base de données est stockée dans un container à part entière, et PHPMyAdmin est disponible sans aucune manipulation supplémentaire.

Les deux premières images sont hébergées sur Docker Hub, alors que les deux suivantes (php_56 et php_71) sont définies dans les dossiers “images/php_56” et “images/php_71”, lesquels contiennent un fichier Dockerfile (entre autre).

Pour démarrer cette application, il suffirait de lancer les commandes suivantes :

docker-compose build # Construit les images
docker-compose up -d # Démarre les containers en mode détaché

Pour stopper les containers :

docker-compose down  # Arrête les containers

De cette manière, l’application tournerait en arrière plan. Mais pas de panique, en cas de soucis, il est tout à fait possible de se connecter à un container en mode “SSH-like”. Pour cela, il faut identifier l’id du container, et lancer la commande suivante :

docker exec -ti CONTAINER_ID /bin/bash

Cette commande permet d’exécuter un bash interactif (option “-ti”) sur le container.

Private Registry

Docker Hub est pratique, mais l’idéal serait de pouvoir disposer d’un espace privé permettant de stocker toutes ses images. Heureusement, ce cas est prévu par Docker : 

docker run -d -p 5000:5000 --restart=always --name registry registry:2

Cette commande permet de déployer un registry qui tournera en permanence, sur le port 5000. De cette manière, on pourra push/pull des images directement sur/depuis ce registry en renseignant l’adresse. Exemple avec un registry déployé en local :

docker push localhost:5000/custom-image
docker pull localhost:5000/custom-image

Plus d’informations sur https://docs.docker.com/registry/deploying/

Utilisation de Docker en entreprise

Il existe de nombreuses façons plus ou moins poussées d’utiliser Docker en entreprise. Voici donc un exemple complet, apportant son lot d’avantages.

Dans cet exemple, la solution pourra être déployée sur un seul serveur (une seule VM) grâce à Docker-Compose.

Les services qui composent l’application sont tous “Dockerisés”. Mais nous avons une distinction entre les services développés par l’entreprise, et les services externes (ex: Serveur MySQL, PhpMyAdmin, etc…).

Les services développés par l’entreprise sont versionnés sur GitLab (un projet par service). GitLab CI est configuré afin de tester la validité des commits (tests), et de “build” le service. Le build consiste en un build Docker, basé sur le Dockerfile du service. Ce Dockerfile prévoit un ensemble de paramètres/variables d’environnement permettant de personnaliser l’exécution/déploiement du service suivant le contexte (ex: il faut pouvoir définir l’adresse d’un serveur MySQL, si on est en prod/dev, etc…).

Une fois le build réalisé, nous disposons d’une image prête à être utilisée. Cette image sera automatiquement “push” sur un private registry. Cette action de push automatique sera réalisée par GitLab CI via le fichier de configuration correspondant.

Un autre projet est également créé sur GitLab, portant le nom de l’application complète. Ce projet contient à minima un fichier “docker-compose.yml”. Le contenu de ce fichier correspond à la configuration des différents services nécessaires au fonctionnement de l’application dans sa globalité. Ainsi, nous retrouvons tous les services créés par l’entreprise nécessaires à l’application, mais aussi des services du type “Serveur MySQL”, “MariaDB”, etc…

Chaque mise-à-jour du projet sera initiée par un commit du docker-compose sur GitLab. En effet, le nom des images à déployer est généralement accompagné d’un tag. On fera ainsi évoluer ces tags au fur-et-à-mesure. GitLab CD est configuré afin de planifier automatiquement un déploiement des services sur le serveur de prod.

Durant les phases de développement

La création d’un nouveau service pourra être réalisée de la manière suivante :

  • Le développeur clone le dépôt correspondant à l’application (le docker-compose)
  • Il déploie tous les services avec la commande “docker-compose up”
  • Le développeur crée un nouveau projet, versionné avec Git
  • Il développe le nouveau service en se basant sur les autres services qui tournent “localement”
  • Une fois le développement terminé, il push les modifications sur GitLab
  • Lorsque le service est intégré (tests, build), le développeur (ou un autre) modifie le fichier “docker-compose.yml” afin d’y ajouter le nouveau service (et potentiellement modifier la version des autres services)

Le fichier “docker-compose.yml” est une base, un fichier générique. Le but est de le personnaliser suivant le contexte. Un développeur l’adaptera à sa convenance (déploiement local de certains services uniquement, et utilisation d’autres services directement sur les serveurs de dev par exemple), alors que le déploiement en production (ou tout autre type d’environnement) modifiera ce fichier afin de correspondre au contexte (utilisation des noms de domaines réels au lieu de “localhost” par exemple).

Commandes utiles

Créer une image

docker build -t IMAGE_NAME -f DOCKER_FILE_PATH ./
  • -t : permet de spécifier un nom pour l’image créée
  • -f : chemin du Dockerfile (s’il n’est pas dans le répertoire courant, ou qu’il ne s’appel pas Dockerfile)
  • ./ : contexte du build (permet de copier des fichiers du répertoire courant dans le container, ex: code source de l’application)

Créer un container

docker run --name CONTAINER_NAME -p PORT --env-file ENV_FILE_PATH IMAGE_NAME
  • –name : permet de spécifier un nom pour le container créé
  • -p : permet de lier un port au container (ex: 8080:80 -> permet de lier le port 8080 de la machine “hôte” au port 80 du container)
  • –en-file : permet de spécifier le chemin d’un fichier .env contenant les variables d’environnement à utiliser dans le container

Créer les différentes images via Docker-Compose

docker-compose build

Démarrer/Arrêter un container

docker start CONTAINER_ID # Démarre le container
docker stop CONTAINER_ID  # Arrête le container

CONTAINER_ID se réfère à l’identifiant attribué au container (“docker ps” pour le voir).

Démarrer/Arrêter une application Multi-Container

docker-compose up -d # Démarre l'application en arrière-plan
docker-compose down  # Arrête l'application

Lister les containers

docker ps -a
  • a : permet d’afficher tous les containers, même ceux inactifs

Accéder à l’invite de commande d’un container

Si le container tourne en arrière-plan (en mode “détaché” avec l’option “-d”), on peut accéder à une interface de commande via la commande suivante : 

docker exec -it CONTAINER_ID /bin/bash

Cela permet d’exécuter un bash sur le container.
CONTAINER_ID se réfère à l’identifiant attribué au container (“docker ps” pour le voir).

Supprimer les containers inactifs

docker container prune

Supprimer les images non utilisées

docker image prune -a

F.A.Q.

Dans le Dockerfile : une ou plusieurs commandes “RUN” ?

Chaque commande RUN va constituer ce qu’on appelle un “layer” (couche). Le résultat est le même entre un Dockerfile avec toutes les commandes enchaînées dans un seul RUN, ou bien séparées dans plusieurs RUN.

Mais en termes d’optimisation, il y a une grosse différence. Lors du démarrage d’un container, Docker va sauvegarder un “screenshot” du container à la fin de chaque couche. De cette manière, lorsqu’on redémarre un container, Docker va être capable de récupérer l’état du container depuis le cache, et donc ne ré-exécuter que les commandes nécessaires.

Exemple, si je définis deux commandes “RUN” dans mon Dockerfile, et que le deuxième RUN échoue parce qu’une commande n’a pas aboutie : je corrige ensuite le deuxième RUN, et je relance le container. Dans ce cas, Docker ne va pas ré-exécuter le premier RUN, qui n’a pas changé depuis la dernière exécution, mais il va charger l’état du container depuis ce moment là, et exécuter les instructions suivantes.

Comment faire communiquer les containers avec Docker-Compose ?

Si on utilise Docker-Compose, c’est généralement (mais pas obligatoirement) parce qu’on veut utiliser une application “multi-container”. Dans ce cas, les containers ont besoin d’échanger des informations, les uns avec les autres, ou bien avec la machine hôte, voir l’extérieur.

Redirection des ports

Cas d’un serveur Web (ex: Apache)

Il suffit d’effectuer une requête HTTP vers le port 80 pour communiquer avec Apache. Depuis un navigateur, cette requête doit être acheminée jusqu’au container, et jusqu’à son port 80.

Par défaut, les containers “partagent” les ports. Mais grâce au fichier “docker-compose.yaml”, on va pouvoir faire une redirection de port (hôte/container) pour le container “Apache”. Ainsi, on pourra lui dire par exemple que les requêtes effectuées sur le port 80 de l’hôte (ou n’importe quel port disponible), seront redirigées sur le port 80 du container Apache.

Du serveur Web (ex: Apache) à PHP

Si l’utilisateur effectue une requête vers le fichier “index.php”, celui-ci doit d’abord être interprété par PHP avant de renvoyer la réponse. Dans ce cas, on installe PHP-FPM sur le container “PHP”, et on configure Apache de manière à ce qu’il puisse communiquer avec PHP-FPM (via une socket).

De PHP à MySQL

Il suffit de définir une redirection de port pour le container “MySQL”, afin que le port 3306 (par exemple) de l’hôte pointe sur le port 3306 du container. De cette manière, il suffira d’établir la connexion MySQL via PHP sur le port 3306.

Réseaux virtuels

Docker permet de créer des « networks ». Un container peut être enregistré sur plusieurs réseaux docker. L’intérêt et de pouvoir limiter la communication entre différents containers.

Deux containers « foo » et « bar » sur un même réseau « my_network » peuvent communiquer simplement en utilisant le nom « foo » ou « bar » au lieu de l’adresse IP du container. Cela simplifie grandement la configuration, puisque Docker s’occupe de l’attribution des adresses IP, et de router les messages en fonction du nom des containers.

Ainsi, notre application PHP, pour pouvoir se connecter à une base de données MySQL, pourra utiliser le nom « mysql » au lieu de l’adresse IP du serveur, du moment où les deux applications sont rattachées au réseau docker « mysql ». Tous container en dehors de ce réseau ne pourra pas communiquer avec le serveur MySQL.

Pour résumer

Docker : Permet de faire fonctionner une application n’importe où

Image : Équivalent d’une « classe » en développement. Il s’agit de la “définition” d’un container, et contient tous les fichiers nécessaires à l’exécution de l’application.

Container : Équivalent d’un objet/instance en développement. Il s’agit d’un processus basé sur une image, mettant à disposition un environnement adapté pour une application (équivalent d’une VM)

Docker-Compose : Permet de faire des applications multi-containers, ou tout simplement d’enregistrer dans un fichier la configuration d’un container.