M2L - Déploiement de conteneurs avec Docker

Écrit par Alexandre Dos Reis
Créé le 2021-3-23, modifié le 2021-6-8.
Docker version 20.10.6, build 370c289 - Documentation - Docker Hub

I. Présentation

a. Introduction

Docker est un logiciel libre permettant de lancer des applications dans des conteneurs logiciels.

Selon la firme de recherche sur l'industrie 451 Research, « Docker est un outil qui peut empaqueter une application et ses dépendances dans un conteneur isolé, qui pourra être exécuté sur n'importe quel serveur ». Il ne s'agit pas de virtualisation, mais de conteneurisation, une forme plus légère qui s'appuie sur certaines parties de la machine hôte pour son fonctionnement. Cette approche permet d'accroître la flexibilité et la portabilité d’exécution d'une application, laquelle va pouvoir tourner de façon fiable et prévisible sur une grande variété de machines hôtes, que ce soit sur la machine locale, un cloud privé ou public, une machine nue, etc.

Techniquement, Docker étend le format de conteneur Linux standard, LXC, avec une API de haut niveau fournissant une solution pratique de virtualisation qui exécute les processus de façon isolée. Pour arriver à ses fins, Docker utilise entre autres LXC, cgroups et le noyau Linux lui-même. Contrairement aux machines virtuelles traditionnelles, un conteneur Docker n'inclut pas de système d'exploitation, mais s'appuie au contraire sur les fonctionnalités du système d’exploitation fournies par la machine hôte.

La technologie de conteneur de Docker peut être utilisée pour étendre des systèmes distribués de façon qu'ils s'exécutent de manière autonome depuis une seule machine physique ou une seule instance par nœud. Cela permet aux nœuds d'être déployés au fur et à mesure que les ressources sont disponibles, offrant un déploiement transparent et similaire aux PaaS pour des systèmes comme Apache Cassandra, Riak ou d'autres systèmes distribués.

b. Mise en situation

MachineOSDistributionVersionRôleNom d'hôteIP & port
Machine PVE ProxmoxGNU / LinuxDebian10.8Grappe Proxmoxns3121671192.168.1.254
VM DockerGNU / LinuxDebian10.8Serveur Dockerdocker192.168.1.2
Conteneur DockerGNU / LinuxDebian10.8App MRBS1web1192.168.1.2:10010
Conteneur DockerGNU / LinuxDebian10.8App MRBS2web2192.168.1.2:10011
Conteneur DockerGNU / LinuxDebian10.8App MRBS3web3192.168.1.2:10012
Conteneur Dockerimage MariaDB/10.8Bdd MRBS3db/

Voici la description de l'infrastructure, un serveur Proxmox est accessible depuis une adresse IP publique, 3 ports ont été ouverts sur ce serveur : 10010, 10011, 10012. Grâce à iptables, il y a une redirection des paquets joignants ces ports vers les mêmes ports sur l'IP privée 192.168.1.2, machine où sera installée Docker.

Les commandes pour ouvrir ces ports sur iptables sont :

$
iptables -t nat -A PREROUTING -j DNAT -i vmbr0 -p tcp --dport 10010--to-destination 192.168.1.2:10010
$
iptables -t nat -A PREROUTING -j DNAT -i vmbr0 -p tcp --dport 10011--to-destination 192.168.1.2:10011
$
iptables -t nat -A PREROUTING -j DNAT -i vmbr0 -p tcp --dport 10012--to-destination 192.168.1.2:10012

Nous aurons donc 3 applications dans 4 conteneurs :

  • Un conteneur avec Apache2, PHP 7.3 et mariaDB.
  • Un conteneur avec Apache2, PHP 8.0 et mariaDB.
  • Un conteneur avec Apache2 et PHP 7.3 qui communiquera vers un 4ème conteneur avec MariaDB. Ce dernier conteneur ne devra pas être joignable depuis l'extérieur.

L'application choisie est MRBS, une application de réservation de salle. Voici le contenu du dossier /home/alex/docker sur le serveur Docker :

-rw-r--r-- 1 alex alex  800 avril 28 11:12 1-mrbs-lamp-php7.3.Dockerfile
-rw-r--r-- 1 alex alex 1109 avril 27 10:11 2-mrbs-lamp-php8.0.Dockerfile
-rw-r--r-- 1 alex alex  333 avril 28 11:11 3-mariaDB.Dockerfile
-rw-r--r-- 1 alex alex  227 avril 28 14:16 3-php7.3.Dockerfile
-rw-r--r-- 1 alex alex  594 avril 28 14:16 docker-compose.yml
drwxr-xr-x 3 alex alex 4096 avril 27 01:42 mrbs-1+2
drwxr-xr-x 3 alex alex 4096 avril 28 11:40 mrbs-3

Des tests seront effectuées avec la commande docker, puis on effectuera les déploiements des 2 premiers serveurs avec Dockerfile, puis le 3ème déploiement s'effectuera avec Docker-Compose.

II. Installation de Docker

a. Plusieurs méthodes d'installation

L'installation de docker peut se faire de plusieurs manières depuis le site officiel disponible ici.

Selon le site officiel, la méthode recommandée est effectuée en ajoutant le dépôt distant docker et en téléchargeant l'application depuis ce dépôt. Pour un environnement de développement ou de test, on choisi la méthode par script.

Donc pour une installation rapide, on peut faire :

$
curl -fsSL https://get.docker.com -o get-docker.sh
$
sudo sh get-docker.sh

b. Test de fonctionnement

Pour tester que Docker tourne bien, on peut faire :

$
sudo docker -v
Docker version 20.10.6, build 370c289

Une fois ceci fait, on peut fait tourner un container de test :

$
sudo docker run hello-world

On devrait obtenir ce message :

Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
b8dfde127a29: Pull complete
Digest: sha256:f2266cbfc127c960fd30e76b7c792dc23b588c0db76233517e1891a4e357d519
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
(amd64)
 3. The Docker daemon created a new container from that image which runs the
executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/

III. Les commandes de bases

a. 1er démarrage de conteneur

Dans l'environnement Docker, les conteneurs sont lancés à partir d'images. Ces dernières sont disponible sur le docker hub en téléchargement libre.

Pour lancer un conteneur, il suffit d'utiliser la commande run suivi du nom de l'image. Si l'image est inexistante sur la machine, Docker utilisera implicitement la commande docker pull <Nom de l'image> pour télécharger l'image depuis le dépôt officiel.

Essayons de lancer un conteneur Debian avec la commande suivante :

$
sudo docker run debian

On obtient :

Unable to find image 'debian:latest' locally
latest: Pulling from library/debian
bd8f6a7501cc: Pull complete
Digest: sha256:ba4a437377a0c450ac9bb634c3754a17b1f814ce6fa3157c0dc9eef431b29d1f
Status: Downloaded newer image for debian:latest

On remarque que l'image n'a pas été trouvée par Docker qui l'a donc téléchargée depuis le dépôt officiel. On voit aussi que chaque image possède un tag comme ceci image:tag

Par exemple, l'image php peut avoir plusieurs tags comme :

php:7.3-fpm-alpine3.13

php:7.4.16-apache

php:8.0

Pour voir les images téléchargés sur le système, on peut utiliser la commande :

$
sudo docker image ls
REPOSITORY   TAG          IMAGE ID       CREATED       SIZE
debian       latest       0d587dfbc4f4   2 weeks ago   114MB

Docker affiche ici les images stockées en local sur notre machine.

Mais revenons au conteneur que nous avons lancé un peu plus haut, pour afficher les conteneurs démarrés, on fait :

$
sudo docker container ls
CONTAINER ID   IMAGE COMMAND CREATED STATUS PORTS NAMES

Bizarre, la liste est vide. Que s'est-il passé ? Docker a démarré un conteneur avec l'image debian:latest mais comme aucun processus n'a été démarré le conteneur s'est terminé brutalement.

Voyons avec la commande suivante l'affichage de tous les conteneurs démarrés comme arrêtés :

$
sudo docker container ls -a
CONTAINER ID   IMAGE     COMMAND     CREATED       STATUS                   PORTS     NAMES
9b84776b5ebc   debian    "bash"      2 hours ago   Exited (0) 2 hours ago             angry_shaw

Notre conteneur apparait donc bien, il a quitté. Son status est Exited

b. Mode détaché

Nous venons de voir que pour qu'un conteneur ne quitte pas, il faut donc qu'il ait un processus en attente. Plusieurs solutions s'offre à nous mais une des plus simples consiste à exécuter une commande au lancement du conteneur avec tail -f. On fait donc :

$
sudo docker run -d debian tail -f

On ajoute l'option -d pour lancer le conteneur en mode détaché, c'est à dire que le conteneur tournera en tâche de fond, il est impératif d'ajouter l'option détaché avec l'ajout d'une commande sous peine d'être bloqué dans notre terminal. On vérifie que le conteneur tourne :

$
sudo docker container ls

Ou avec l'ancienne commande historique :

$
sudo docker ps
CONTAINER ID  IMAGE   COMMAND    CREATED             STATUS             PORTS    NAMES
0adc35b3af7a  debian  "tail -f"  About a minute ago  Up About a minute           heuristic_shannon

Notre conteneur est donc bien démarré avec la commande tail -f, docker lui a donné un nom ce qui bien pratique pour éviter d'avoir à retenir l'id du conteneur.

A partir de là, on peut :

Stopper $
sudo docker stop nomDuConteneur>
Relancer $
sudo docker restart nomDuConteneur
Voir les logs $
sudo docker logs nomDuConteneur

ou si on ne sait que faire :

$
sudo docker --help

c. Mode interactif

On peut désormais lancer nos conteneurs et les faire tourner en tâche de fond mais pour l'instant on n'a pas vraiment interagit avec. Pour ce faire, on va lancer ces commandes :

$
sudo docker run -it imageDocker bash

Ou si le conteneur est déjà en route :

$
sudo docker exec -it idOuNomDuConteneur bash

L'option i veut dire interactif et t veut dire terminal, donc on souhaite lancer un conteneur en mode interactif en mode terminal mais il faut encore indiquer le processus à lancer donc ici bash. On devrait se retrouver avec ça :

root@0adc35b3af7a:/#

Maintenant que nous sommes dans notre conteneur, on peut faire tout ce que l'on veut sans risque de compromettre l'ordinateur hôte sur lequel est installé Docker, c'est le principe même d'un conteneur. Il y tout de même quelques différences entre une vraie machine Debian et un conteneur Debian, voici quelques unes d'entre elles :

apt install devient apt-get install

systemctl start apache2 devient service apache2 start

d. Les volumes

On voudrait partager un volume entre notre ordinateur hôte et un conteneur , pour ce faire on utilise l'option -v comme ceci :

$
sudo docker run -d --name web -p 8080:80 -v ~/web:/usr/local/apache2/htdocs

Cette commande vient de la documentation Docker concernant apache2, on a spécifié l'option --name pour ajouter un nom à notre conteneur, on a relié le port 8080 de notre machine hôte au port 80 du conteneur Apache2 et on a monté le répertoire ~/web de l'ordinateur hôte vers le dossier d'apache2 pour servir notre application.

e. Les réseaux

On peut créer différents réseaux dans Docker et y placer nos différents conteneurs. On affiche la liste des réseaux avec :

$
sudo docker network ls
NETWORK ID     NAME             DRIVER    SCOPE
39e84070fd93   bridge           bridge    local
a46e544f52dd   host             host      local
7b77007a09ea   none             null      local

bridge désigne le réseau par défaut utilisé par Docker pour faire tourner les conteneurs. host est le réseau physique de notre machine hôte, donc on pourrait monter un conteneur sans avoir à mapper le port avec l'option -p avec ce réseau host.

On crée un réseau avec la commande :

$
sudo docker network create nomDuReseau

On ajoute un conteneur dans le réseau créé avec :

$
sudo docker network connect nomDuReseau nomDuConteneur

Le nom donné au conteneur agit comme un nom "dns" et peut être utilisé afin de contacter le conteneur, par exemple créons 2 conteneurs :

$
docker run -d --name web1 -p 8001:80 php:7.3-apache
$
docker run -d --name web2 -p 8002:80 php:7.3-apache

On crée un réseau dans lequel serons placé nos serveurs :

$
docker network create myNetwork
$
docker network connect myNetwork web1
$
docker network connect myNetwork web2

Puis on utilise web1 pour joindre web2 :

$
docker exec -ti web1 ping web2

IV. dockerfile

a. Introduction

Après avoir vu les commandes de bases, il devient évident qu'il est fastidieux de déployer un serveur de la sorte, raison pour laquelle on va utiliser Dockerfile. On va décrire la configuration dans un fichier nommé monFichierDeConfig.Dockerfile, puis ce fichier nous servira à construire une image que l'on lancera avec la commande docker build.

Avec quelques commandes de bases, on décrit les étapes à effectuer, voici un exemple d'une structure de base :

FROM <Nom de l image de départ>
RUN <Nom de la commande Bash à lancer>
COPY <Copie d un fichier de la machine hôte vers le conteneur>
ENTRYPOINT <Commande avec laquelle sera lancé le conteneur>

Une fois que le fichier est prêt, on lancer la création de l'image avec cette commande :

$
sudo docker build -t image -f fichier.Dockerfile .

L'option -t permet de spécifier un nom:tag à notre image tandis que l'option -f spécifie le chemin du dockerfile depuis lequel l'image sera créé. A noter qu'il faut ajouter un point à la fin de la commande pour indiquer que le fichier est dans le répertoire courant.

b. 1ère situation

Voici le fichier de configuration pour la 1ère situation : php:7.3-apache

FROM debian

# Dépendances
RUN apt-get update
RUN apt-get install -y wget apache2 php7.3 php7.3-mysql mariadb-server

# Copie des fichiers MRBS pour Apache2
RUN rm /var/www/html/index.html
COPY mrbs-1+2/web /var/www/html

# Création de la BDD MRBS
COPY mrbs-1+2/tables.my.sql /tmp/tables.my.sql

# Copie des datas de la M2L
COPY data/m2l_mrbs_data_v1.9.2.sql /tmp/data.sql

# Commandes sur une seule ligne pour fonctionner car mariaDB doit être lancé
RUN /etc/init.d/mysql start && \
    mysqladmin create mrbs && \
    mysql mrbs < /tmp/tables.my.sql && \
    mysql mrbs < /tmp/data.sql && \
    mysql -e "GRANT ALL PRIVILEGES ON mrbs.* TO 'mrbs'@'%' identified by 'mrbs';"

# On efface les fichiers d'import SQL
RUN rm /tmp/*.sql

# Permet de démarrer mysql et apache2 au lancement du conteneur et de ne pas quitter :
ENTRYPOINT service mysql start && service apache2 start && /usr/bin/tail -f

Création de l'image :

$
sudo docker build -t 1-mrbs-lamp-php7.3 -f 1-mrbs-lamp-php7.3.Dockerfile .

Lancement du conteneur :

$
sudo docker run -d -p 10010:80 --name Web1 1-mrbs-lamp-php7.3 tail -f

c. 2ème situation

Voici le fichier de configuration pour la 2ème situation : php:8.0-apache

FROM debian

# Mise à jour du dépôt
RUN apt-get update

# Ajout d'un dépôt PHP 8
RUN apt-get install -y lsb-release apt-transport-https ca-certificates wget
RUN wget -O /etc/apt/trusted.gpg.d/php.gpg https://packages.sury.org/php/apt.gpg
RUN echo "deb https://packages.sury.org/php/ $(lsb_release -sc) main" |tee /etc/apt/sources.list.d/php.list

# Dépendances
RUN apt-get update
RUN apt-get install -y wget apache2 php8.0 php8.0-mysql mariadb-server

# Copie des fichiers MRBS pour Apache2
RUN rm /var/www/html/index.html
COPY mrbs-1+2/web /var/www/html

# Création de la BDD MRBS
COPY mrbs-1+2/tables.my.sql /tmp/tables.my.sql

# Copie des datas de la M2L
COPY data/m2l_mrbs_data_v1.9.2.sql /tmp/data.sql

# Commande sur une seule ligne pour fonctionner car mariaDB doit être lancé
RUN /etc/init.d/mysql start && \
    mysqladmin create mrbs && \
    mysql mrbs < /tmp/tables.my.sql && \
    mysql mrbs < /tmp/data.sql && \
    mysql -e "GRANT ALL PRIVILEGES ON mrbs.* TO 'mrbs'@'%' identified by 'mrbs';"

# On efface les fichiers d'import SQL
RUN rm /tmp/*.sql

# Permet de démarrer les services et de ne pas quitter !
ENTRYPOINT service mysql start && service apache2 start && /usr/bin/tail -f

Création de l'image :

$
sudo docker build -t 2-mrbs-lamp-php8.0 -f 2-mrbs-lamp-php8.0.Dockerfile .

Lancement du conteneur :

$
sudo docker run -d -p 10011:80 --name Web2 2-mrbs-lamp-php8.0 tail -f

d. 3ème situation

Cette situation nécessite de créer 2 fichiers car on lancera 2 conteneurs basés sur 2 images, une pour PHP et une pour MariaDB.

Voici les fichiers de configuration pour le serveur web PHP :

FROM php:7.3-apache
RUN docker-php-ext-install mysqli pdo pdo_mysql && docker-php-ext-enable mysqli pdo pdo_mysql
RUN mv $PHP_INI_DIR/php.ini-production $PHP_INI_DIR/php.ini
ENTRYPOINT service apache2 start && /usr/bin/tail -f

Dans le fichier de configuration de MRBS de la base de données du dossier web, il faut spécifier le nom d'hôte du serveur MariaDB mais comme nous utilisons Docker, cela revient à indiquer le pseudo nom DNS du serveur.

La configuration pour MariaDB sera effectuée avec Docker-compose au chapitre suivant. Les conteneurs créés à partir de cette 3ème situation seront déployés avec Docker-compose car il faut que les conteneurs puissent communiquer entre eux.

V. docker-compose

a. Introduction

Tout comme dockerfile, docker-compose permet de décrire l'ensemble de notre infrastructure dans un fichier yml. C'est un outil puisant pour déployer en une commande toute une infrastructure. Voici un fichier d'exemple :

# Version du langage docker-compose à utiliser :
version: "3"

services:
    service1:
            image: <image de départ sur laquelle le conteneur se lancera>
            container_name: <Nom donnée au conteneur déployé>

            # Permet de relancer le conteneur automatiquement en cas d'arrêt
            restart: always

            # Permet de spécifier un volume à partager entre l'hôte et le conteneur.
            volumes:
            - dossier/hôte/:dossier/conteneur

            # Permet de mapper un port de l'hôte vers le conteneur.
            port:
            - "3306:3306"

    service2:
            container_name: nomDuService2
            restart: always

            # Permet de démarrer à partir d'un dockerfile au lieu d'une image
            build:
            context: ./
            dockerfile: "3-php7.3.Dockerfile"

            # Permet de démarrer ce conteneur après le service 1
            depends_on:
            - service1

            # Permet de relier les 2 services.
            links:
            - service1

            volumes:
            - ./mrbs-3/web/:/var/www/html/

            ports:
            - "10012:80"

b. Commandes

Pour lancer toute notre infrastructure en mode détaché, on fait:

$
sudo docker-compose up -d

Pour l'arrêter (Arrête et supprime les conteneurs, les images, les réseaux et les volumes créés par la commande up !)

$
sudo docker-compose down

Pour voir les logs de tous les conteneurs :

$
sudo docker-compose logs

c. 3ème situation

Voici le fichier de configuration pour la 3ème situation :

version: '3'

services:

    db:
    image: mariadb
    container_name: db
    restart: always
    volumes:
    - /home/alex/docker/data:/docker-entrypoint-initdb.d
    environment:
            MYSQL_ROOT_PASSWORD: root
            MYSQL_DATABASE: mrbs
            MYSQL_USER: mrbs
            MYSQL_PASSWORD: mrbs
    expose:
    - "3306"

    web:
    container_name: web3
    restart: always
    build:
            context: ./
            dockerfile: "3-php7.3.Dockerfile"
    depends_on:
    - db
    links:
    - db
    volumes:
    - ./mrbs-3/web/:/var/www/html/
    ports:
    - "10012:80"

Tous les fichiers SQL qui se trouvent dans le repertoire /home/alex/docker/data seront chargés dans l'ordre alphabétique.

VI. Conclusion

Docker est très utile pour tester des applications rapidement, effectuer du déploiement rapides et régler les problèmes d'environnement différents entre les machines. Il permet aussi de faire du load-balancing d'application.

VII. Notes :

Voir ici pour un lexique des commandes Docker les plus utilisées