Dans cette deuxième série de travaux autour de Docker, nous allons aborder :
Prérequis : connaissance de Linux, des réseaux. Il faut également avoir suivi le cours introductif sur les conteneurs et Docker, et réalisé le premier TP Docker.
Nous allons continuer avec la VM Docker mise en place précédemment. S’il faut la recréer, reportez-vous aux instructions du premier TP.
On la gère en ssh depuis la machine hôte :
Pour rappel, nous avons expérimenté le lancement (pull
, run
, exec
) de conteneurs à partir d’images, la modification de conteneurs et la génération d’image (commit
). Nous avons vu également comment un conteneur peut exposer, ou "publier", un port de communication (tcp ou udp) sur le réseau local de l’hôte, au travers du routage NAT/PAT intégré dans le Docker Engine sur le réseau bridge :
Avant de continuer, faisons un peu de ménage dans les conteneurs et les images :
docker stop $(docker ps -aq)
docker rm $(docker ps -aq)
docker rmi $(docker images -q) -f
Puis, par exemple, on peut instancier un serveur nginx et exposer son service depuis le port 80 du conteneur sur le port 8090 du serveur Docker Engine :
docker run -d --name srvweb -p 8090:80 nginx
Docker va charger l'image puis instancier le conteneur :
Unable to find image 'nginx:latest' locally
latest: Pulling from library/nginx
a480a496ba95: Pull complete
f3ace1b8ce45: Pull complete
11d6fdd0e8a7: Pull complete
f1091da6fd5c: Pull complete
40eea07b53d8: Pull complete
6476794e50f4: Pull complete
70850b3ec6b2: Pull complete
Digest: sha256:28402db69fec7c17e179ea87882667f1e054391138f77ffaf0c3eb388efc3ffb
Status: Downloaded newer image for nginx:latest
871fdc31bff5074f70a2c0e9908c25909ee0b7eaaab97eb336060d118dcae94c
On vérifie les conteneurs actifs :
docker ps
Le conteneur srvweb est bien présent, et "up" :
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
fd4eab1c82aa nginx "/docker-entrypoint.…" 8 seconds ago Up 7 seconds 0.0.0.0:8090->80/tcp, [::]:8090->80/tcp srvweb
Une requête curl
sur le Docker Engine lui-même (127.0.0.1) et le port exposé (tcp/8090) :
curl 127.0.0.1:8090
nous donne bien la réponse habituelle par défaut d'un serveur nginx :
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
Selon ce qu'on a vu précédemment, pour modifier le contenu HTML fourni par ce service nginx, il faut exécuter un bash interactif sur le conteneur, et modifier le fichier /usr/share/nginx/index.html
:
docker exec -ti srvweb bash
On se retrouve sur le shell du conteneur :
root@38a5cfb24f10:~#
On installe l'éditeur nano afin de modifier le fichier html :
apt update
apt install nano
nano /usr/share/nginx/html/index.html
exit
On relance la requête curl
:
$ curl 127.0.0.1:8090
La sortie doit apparaître modifée selon ce que vous aurez saisie dans le fichier index.html.
Le problème est que si le conteneur est relancé depuis son image, les modifications apportées au fichier index.html seront perdues. Voyons maintenant comment on peut mettre en place cette persistance des données.
Un volume est un répertoire défini en dehors d’un conteneur mais accessible à celui-ci pour qu’il puisse y stocker des données, qui persisteront même lorsque le conteneur sera supprimé (pour une mise à jour par exemple).
Pour ce faire, nous allons monter une arborescence de fichiers de l’hôte à l’intérieur du container. Il est ainsi possible de publier un site Web dont les fichiers sources sont déposés dans un dossier du serveur hôte à travers un conteneur. Docker utilise pour cela le paramètre -v
.
Supprimons d'abord le conteneur actuel :
docker stop srvweb
docker rm srvweb
Puis recréons-le mais cette fois ajoutons le montage du volume :
docker run -d --name srvweb -p 8090:80 -v /var/www/html:/usr/share/nginx/html nginx
Docker va ainsi monter le répertoire /var/www/html
de la machine hôte (Docker Engine) à l’emplacement /usr/share/nginx/html
dans le conteneur. Si les répertoires n’existent pas, ils seront créés (et le propriétaire sera root).
Il suffit donc maintenant de déposer (ou créer) dans /var/www/html
de l’hôte un fichier index.html personnalisé afin de le distinguer de la page par défaut de nginx et de vérifier que c’est bien cette page qui est publiée :
sudo nano /var/www/html/index.html
Copiez par exemple ce joli contenu html :
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bienvenue !</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f0f0f0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
}
.container {
text-align: center;
}
h1 {
font-size: 2.5rem;
color: #333;
animation: slide 5s infinite alternate;
}
@keyframes slide {
0% { transform: translateX(0); }
100% { transform: translateX(30px); }
}
.button {
margin-top: 20px;
padding: 10px 20px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 1rem;
transition: background-color 0.3s ease;
}
.button:hover {
background-color: #45a049;
}
</style>
</head>
<body>
<div class="container">
<h1>Bienvenue sur ma page !</h1>
<button class="button">Clique-moi !</button>
</div>
</body>
</html>
On vérifie que le serveur web du conteneur utilise bien ce fichier :
curl 127.0.0.1:8090
essayer aussi sur un navigateur web http://192.168.68.102:8090 ; l'ip est à adapter évidemment selon votre situation.
Si on arrête et détruit le conteneur :
docker rm -f srvweb
et qu’on le relance avec le montage de volume identique :
docker run -d --name srvweb -p 8090:80 -v /var/www/html:/usr/share/nginx/html nginx
on constate la persistance du fichier html :
curl 127.0.0.1:8090
Il est possible d’administrer les volumes de façon plus précise avec la commande docker volume
. On peut par exemple créer un volume et lui donner un nom :
docker volume create monvolume
La création de volume depuis docker run
, avec l’option -v
, ne permet pas de nommer le volume, il reçoit un identifiant arbitraire.
En revanche, si la commande docker volume
permet de nommer un volume, son emplacement sera fixe : /var/lib/docker/volumes/monvolume/_data
(emplacement accessible en root uniquement).
Pour lister les volumes :
docker volume ls
Pour inspecter les volumes :
docker volume inspect $(docker volume ls -q)
Pour supprimer les volumes non rattachés ("dangling") :
docker volume prune -a
Il y a deux (et même trois) types de montage de volume avec Docker.
/var/lib/docker/volumes/
sous Linux). Il est possible aussi de créer et gérer ces volumes avec la commande docker volume
.Voici un petit schéma récapitulatif de ces trois types :
On utilise un volume quand c’est plutôt le container qui va générer et traiter les données persistantes : logs, statistiques, databases, ...
Exemple (Docker va créer automatiquement un volume associé sur l'hôte) :
docker run -d -v /usr/share/nginx/html nginx
On peut aussi utiliser l’option plus explicite --mount
plutôt que -v
:
docker run -d --mount type=volume,target=/usr/share/nginx/html nginx
Ou bien, si je veux pouvoir nommer un volume, en le créant d'abord :
docker volume create monvolume
docker run -d -v monvolume:/usr/share/nginx/html nginx
Et là aussi je peux utiliser --mount
plutôt que -v
:
docker run -d --mount type=volume,src=monvolume,dst=/usr/share/nginx/html nginx
On utilise un montage lié (bind) pour des fichiers que l’on veut pouvoir administrer localement, et qui doivent être accédés par le container : site web, vidéo, photo, images, ... On doit ici indiquer le chemin côté hôte (source), et le chemin dans le conteneur (destination). Exemple :
docker run -d -v /var/www/html:/usr/share/nginx/html nginx
Avec --mount
plutôt que -v
:
docker run -d --mount type=bind,src=/var/www/html,dst=/usr/share/nginx/html nginx
On peut avoir plusieurs volumes persistants associés à un conteneur. A titre d'exemple, nous allons ajouter ici un volume pour enregistrer les logs de connexion, en plus du volume pour les fichiers html.
Mais tout d’abord, il s’avère qu’avec l’image nginx, la configuration des logs n’est pas activée par défaut. Nous devons donc modifier le fichier de configuration (en décommentant la ligne adéquate avec la commande historique sed
). Tout d'abord, on se "connecte" à notre conteneur :
docker exec -ti srvweb bash
On arrive sur le terminal du conteneur :
root@6109d5c0702e:/#
On modifie la configuration du serveur nginx :
cd /etc/nginx/conf.d
cp default.conf default.conf.save
sed -i 's/#access_log/access_log/1' default.conf
cat default.conf
La ligne access_log du fichier de configuration (default.conf) doit être décommentée :
...
access_log /var/log/nginx/host.access.log main;
...
Nous souhaitons que cette modification soit persistante, il va falloir générer une nouvelle image Docker à partir de nos modifications. Profitons-en aussi pour régler les paramètres timezone, puis générons une nouvelle image personnalisée :
dpkg-reconfigure tzdata
exit
Puis générons l'image :
docker commit srvweb kl/nginx
Vérifions nos images disponibles :
docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
kl/nginx latest b07b44296150 7 seconds ago 193MB
nginx latest 3b25b682ea82 2 weeks ago 192MB
Notre nouvelle image a très peu surchargé l’image initiale, c’est un point important.
Supprimons le conteneur actuel :
docker rm -f srvweb
On peut maintenant lancer notre conteneur selon cette nouvelle image kl/nginx, et avec ses deux volumes liés :
docker run -d --name srvweb -p 8090:80 -v /var/www/html:/usr/share/nginx/html -v /var/log/srvweb:/var/log/nginx/ kl/nginx
volume côté serveur | volume côté conteneur | usage |
---|---|---|
/var/www/html | /usr/share/nginx/html | fichiers html |
/var/log/srvweb | /var/log/nginx/ | fichiers de log |
On peut vérifier que notre fichier log fonctionne, et qu’il est persistant :
curl 127.0.0.1:8090
docker rm -f srvweb
docker run -d --name srvweb -p 8090:80 -v /var/www/html:/usr/share/nginx/html -v /var/log/srvweb:/var/log/nginx/ kl/nginx
curl 127.0.0.1:8090
/var/log/srvweb
) :cat /var/log/srvweb/host.access.log
Le fichier log a bien conservé son historique, il est persistant.
Pour illuster encore plus tout cela, on va mettre en place un serveur de bases de données au travers d'un conteneur mariadb qui "externalisera" ses bases (via un volume) dans /var/lib/mysql_docker/
; on le liera ensuite à d'autres conteneurs applicatifs destinés à exploiter les données.
Nous utiliserons l’image mariadb officielle. Celle-ci expose par défaut le port standard 3306 et permet l’accès au serveur de bases de données à partir d’un hôte distant. Le mot de passe que l’on veut utiliser pour root peut être passé au conteneur via la variable d’environnement MYSQL_ROOT_PASSWORD
.
Retrouvez ici donne toutes les indications d’utilisation du conteneur sur le Docker Hub.
Commençons par un petit ménage radical des images et conteneurs sur notre serveur :
docker stop $(docker ps -aq)
docker rm $(docker ps -aq)
docker rmi $(docker images -q) -f
Et maintenant, on télécharge la dernière version de l’image Docker mariadb officielle :
docker pull mariadb
Rappel : cette étape est optionnelle puisque l’opération
run
télécharge l’image si celle-ci n’est pas disponible localement.
On lance le conteneur que nous nommons servmdb avec :
mysqlAdmin
/var/lib/mysql_docker
(du côté conteneur, le volume exposé est /var/lib/mysql
).docker run -v /var/lib/mysql_docker:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=mysqlAdmin --name servmdb -d mariadb
C’est l’option -e
qui permet de définir les variables d’environnement nécessaires, comme ici le mot de passe pour root (administrateur du système de gestion des bases de données) qui va permettre par exemple d’administrer le serveur en ligne de commande ou via une application comme phpMyAdmin.
Sur l’hôte (Docker Engine), on constate que les fichiers de la base de données sont bien présentsdans /var/lib/mysql_docker
(qui a été créé automatiquement par docker) :
ls -laht /var/lib/mysql_docker/
total 155M
drwxr-xr-x 5 999 systemd-journal 4,0K 21 oct. 20:37 .
-rw-rw---- 1 999 systemd-journal 24K 21 oct. 20:37 tc.log
-rw-rw---- 1 999 systemd-journal 12M 21 oct. 20:37 ibtmp1
-rw-rw---- 1 999 systemd-journal 9 21 oct. 20:37 ddl_recovery.log
-rw-rw---- 1 999 systemd-journal 4,7M 21 oct. 20:37 aria_log.00000001
-rw-rw---- 1 999 systemd-journal 52 21 oct. 20:37 aria_log_control
-rw-rw---- 1 999 systemd-journal 706 21 oct. 20:37 ib_buffer_pool
-rw------- 1 999 systemd-journal 118 21 oct. 20:37 .my-healthcheck.cnf
drwx------ 2 999 systemd-journal 4,0K 21 oct. 20:37 mysql
-rw-rw---- 1 999 systemd-journal 0 21 oct. 20:37 multi-master.info
-rw-r--r-- 1 999 systemd-journal 14 21 oct. 20:37 mariadb_upgrade_info
-rw-rw---- 1 999 systemd-journal 96M 21 oct. 20:37 ib_logfile0
-rw-rw---- 1 999 systemd-journal 12M 21 oct. 20:37 ibdata1
-rw-rw---- 1 999 systemd-journal 10M 21 oct. 20:37 undo001
-rw-rw---- 1 999 systemd-journal 10M 21 oct. 20:37 undo002
-rw-rw---- 1 999 systemd-journal 10M 21 oct. 20:37 undo003
drwx------ 2 999 systemd-journal 12K 21 oct. 20:37 sys
drwx------ 2 999 systemd-journal 4,0K 21 oct. 20:37 performance_schema
drwxr-xr-x 34 root root 4,0K 21 oct. 20:37 ..
Remarquez le propriétaire inhabituel des fichiers. Lorsqu'un volume est partagé entre l'hôte et le conteneur, Docker conserve les permissions d'origine des fichiers sur l'hôte. Cela peut entraîner des discordances entre le conteneur et l'hôte, notamment si un utilisateur spécifique comme systemd-journal utilise le même UID sur l'hôte.
Nous souhaitons nous connecter à ce serveur (conteneur) MariaDB en ligne de commande, via une nouvelle instance de l’image en liaison avec le conteneur d’origine (servmdb).
Une des bonnes pratiques avec Docker est d’avoir un conteneur par service. Ceux-ci ont donc besoin de communiquer entre eux. Par défaut, les conteneurs présents sur le même serveur (Docker Engine) ont une interface sur le réseau bridge, et peuvent communiquer entre elles via leurs adresses IP (réseau 172.17.0.0/16
).
L’opérateur link
permet à un conteneur d’avoir accès plus "élaboré" sur autre conteneur, au travers d’un certain nombre de variables d’environnement pour faciliter l’interaction, et d’une entrée dans le fichier /etc/hosts
pour accéder à la machine liée par son nom.
Ceci peut être représenté par le schéma suivant :
Essayons la commande suivante :
docker run -it --rm --link servmdb:xy mariadb bash
La commande docker run
instancie un nouveau conteneur mariadb, en lançant un shell bash interactif (-it
) ; le conteneur sera détruit dès la sortie du shell (--rm
). L’option --link servmdb:xy
crée un lien avec le conteneur servmdb, en utilisant (c’est optionnel) un alias xy. On obtient donc un prompt bash sur ce conteneur :
root@674b3a507334:/#
Entrons ces quelques commandes :
cat /etc/hosts
La commande cat /etc/hosts
nous montre l’entrée DNS qui relie les noms servmdb
et xy
(ainsi que le hostname généré par Docker 219cf02da815
) avec l’adresse IP du serveur mariadb (ligne 7) :
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 xy 219cf02da815 servmdb
172.17.0.3 674b3a507334
Une autre commande :
env
La commande env
nous renvoie l’ensemble des variables d’environnement, dont celles qui commencent par XY
qui sont spécifiquement créées par l’option --link
.
Certaines sont utiles, comme par exemple XY_ENV_MYSQL_ROOT_PASSWORD
qui contient le mot de passe de la base données, ce qui évitera de le passer en clair dans des appels d’un client vers le serveur de base de données (ligne 18) :
XY_ENV_GOSU_VERSION=1.17
HOSTNAME=674b3a507334
PWD=/
XY_PORT_3306_TCP_PORT=3306
XY_PORT_3306_TCP_PROTO=tcp
HOME=/root
LANG=C.UTF-8
...
MARIADB_VERSION=1:11.5.2+maria~ubu2404
XY_PORT=tcp://172.17.0.2:3306
GOSU_VERSION=1.17
XY_ENV_MARIADB_VERSION=1:11.5.2+maria~ubu2404
TERM=xterm
XY_NAME=/agitated_keldysh/xy
XY_PORT_3306_TCP_ADDR=172.17.0.2
XY_PORT_3306_TCP=tcp://172.17.0.2:3306
SHLVL=1
XY_ENV_MYSQL_ROOT_PASSWORD=mysqlAdmin
XY_ENV_LANG=C.UTF-8
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
_=/usr/bin/env
Revenons au shell du Docker Engine :
exit
Lançons par exemple un nouveau conteneur provisoire (--rm
), lié (sans alias) au conteneur du serveur, qui va ouvrir le shell mysql connecté au serveur mariadb :
docker run -it --rm --link servmdb mariadb bash -c 'exec mariadb -h servmdb -u root -p"$SERVMDB_ENV_MYSQL_ROOT_PASSWORD"'
Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MariaDB connection id is 3
Server version: 11.5.2-MariaDB-ubu2404 mariadb.org binary distribution
Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
MariaDB [(none)]>
Notez la façon assez particulière de lancer la commande qui appelle le shell mariadb :
bash -c 'exec mariadb ...'
. Cette méthode permet que la commande remplace le shell bash comme processus principal dans le conteneur, ce qui est primordial dans le mode de fonctionnement de Docker (on reviendra sur ce point dans le dockerfile)
Créons une base de données testdocker :
create database testdocker;
exit;
On revient au shell du Docker Engine. Regardons le contenur du volume local /var/lib/mysql_docker
:
ls -lh /var/lib/mysql_docker/
Cette nouvelle base est bien visible (ligne 15) :
total 155M
-rw-rw---- 1 999 systemd-journal 4,7M 21 oct. 20:37 aria_log.00000001
-rw-rw---- 1 999 systemd-journal 52 21 oct. 20:37 aria_log_control
-rw-rw---- 1 999 systemd-journal 9 21 oct. 20:37 ddl_recovery.log
-rw-rw---- 1 999 systemd-journal 706 21 oct. 20:37 ib_buffer_pool
-rw-rw---- 1 999 systemd-journal 12M 21 oct. 20:37 ibdata1
-rw-rw---- 1 999 systemd-journal 96M 21 oct. 20:37 ib_logfile0
-rw-rw---- 1 999 systemd-journal 12M 21 oct. 20:37 ibtmp1
-rw-r--r-- 1 999 systemd-journal 14 21 oct. 20:37 mariadb_upgrade_info
-rw-rw---- 1 999 systemd-journal 0 21 oct. 20:37 multi-master.info
drwx------ 2 999 systemd-journal 4,0K 21 oct. 20:37 mysql
drwx------ 2 999 systemd-journal 4,0K 21 oct. 20:37 performance_schema
drwx------ 2 999 systemd-journal 12K 21 oct. 20:37 sys
-rw-rw---- 1 999 systemd-journal 24K 21 oct. 20:37 tc.log
drwx------ 2 999 systemd-journal 4,0K 21 oct. 21:10 testdocker
-rw-rw---- 1 999 systemd-journal 10M 21 oct. 20:37 undo001
-rw-rw---- 1 999 systemd-journal 10M 21 oct. 20:37 undo002
-rw-rw---- 1 999 systemd-journal 10M 21 oct. 20:37 undo003
Comme nous avons mis en place la persistance des données, si l’on supprime le conteneur serveur servmdb et qu’on lance des nouvelles instances (une pour le serveur et l’autre pour l’accès client), nous pouvons vérifier que la base de données créée précédemment existe toujours.
On supprime le conteneur (et on vérifie) :
docker rm -f servmdb
docker ps -a
Puis on le recrée :
docker run -d -v /var/lib/mysql_docker:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=mysqlAdmin --name servmdb mariadb
On se connecte au shell mysql par un conteneur client :
docker run -it --rm --link servmdb mariadb bash -c 'exec mariadb -h servmdb -u root -p"$SERVMDB_ENV_MYSQL_ROOT_PASSWORD"'
Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MariaDB connection id is 3
Server version: 11.5.2-MariaDB-ubu2404 mariadb.org binary distribution
Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
MariaDB [(none)]>
Et on regarde les bases de données disponibles :
show databases;
exit;
Notre base de testdocker est bien présente (ligne 8) :
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
| sys |
| testdocker |
+--------------------+
5 rows in set (0.001 sec)
MariaDB [(none)]> exit;
Bye
Regardons une dernière fois les éléments qu’apportent l’option --link
, cette fois-ci avec un conteneur linux ultraléger : alpine
--link
, la commande :docker run --rm alpine env
nous retourne :
...
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=42c347525fc5
HOME=/root
et la commande :
docker run --rm alpine cat /etc/hosts
nous retourne :
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 4564e52dd4df
--link
, la commande :docker run --rm --link servmdb alpine env
nous retourne :
...
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=214f3588e6fc
SERVMDB_PORT=tcp://172.17.0.2:3306
SERVMDB_PORT_3306_TCP=tcp://172.17.0.2:3306
SERVMDB_PORT_3306_TCP_ADDR=172.17.0.2
SERVMDB_PORT_3306_TCP_PORT=3306
SERVMDB_PORT_3306_TCP_PROTO=tcp
SERVMDB_NAME=/strange_yonath/servmdb
SERVMDB_ENV_MYSQL_ROOT_PASSWORD=mysqlAdmin
SERVMDB_ENV_GOSU_VERSION=1.17
SERVMDB_ENV_LANG=C.UTF-8
SERVMDB_ENV_MARIADB_VERSION=1:11.5.2+maria~ubu2404
HOME=/root
et la commande :
docker run --rm --link servmdb alpine cat /etc/hosts
nous retourne :
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 servmdb ae4a9dd8dc8d
172.17.0.3 647670f435db
On récupère ainsi dans le conteneur alpine des variables issues du conteneur lié (srvmdb). On se rappellera que les variables d’environnement peuvent être utilisées partout dans le conteneur (notamment dans les scripts).
Pour pouvoir utiliser un script présent sur la machine hôte (pour par exemple initialiser une base de données), il est nécessaire de pouvoir y accéder depuis le conteneur, ce qui est possible, on l'a vu, avec des volumes.
Créez sur le serveur Docker Engine le dossier /mnt/scripts
:
sudo mkdir /mnt/scripts
Importez à cet emplacement un script sql de création et d’alimentation d’une base de données :
cd /mnt/scripts
sudo wget https://cdn.pminfo.fr/div/france.sql
Puis lancez un conteneur client sql pour importer les données, en connectant (-v
) ce répertoire local /mnt/scripts
à un répertoire spécifique dans le conteneur /scripts
:
docker run -it --rm --link servmdb -v /mnt/scripts:/scripts mariadb bash -c 'exec mariadb -h servmdb -u root -p"$SERVMDB_ENV_MYSQL_ROOT_PASSWORD" < /scripts/france.sql'
Lancer un conteneur provisoire pour obtenir un shell mysql sur servmdb :
docker run -it --rm --link servmdb mariadb bash -c 'exec mariadb -h servmdb -u root -p"$SERVMDB_ENV_MYSQL_ROOT_PASSWORD"'
Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MariaDB connection id is 6
Server version: 11.5.2-MariaDB-ubu2404 mariadb.org binary distribution
Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
MariaDB [(none)]>
Et tester le fonctionnement en jouant avec SQL et les données importées :
show databases;
use france;
show tables;
show columns from villes;
select ville_nom from villes where ville_nom like "%uz";
...
exit;
Maintenant que nous savons lier des conteneurs, nous allons connecter un conteneur phpmyadmin à notre conteneur serveur servmdb. Recherchons une image phpmyadmin :
docker search phpmyadmin
NAME DESCRIPTION STARS OFFICIAL
phpmyadmin phpMyAdmin - A web interface for MySQL and M… 1037 [OK]
phpmyadmin/phpmyadmin A web interface for MySQL and MariaDB. 1202
bitnami/phpmyadmin Bitnami container image for phpMyAdmin 48
bitnamicharts/phpmyadmin 0
elestio/phpmyadmin Phpmyadmin, verified and packaged by Elestio 0
linuxserver/phpmyadmin 19
shinsenter/phpmyadmin 🔋 (PHP / phpMyAdmin) Production-ready Docke… 2
vulhub/phpmyadmin 1
jitesoft/phpmyadmin PhpMyAdmin data-only image. 0
nazarpc/phpmyadmin phpMyAdmin as Docker container, based on off… 61
jackgruber/phpmyadmin Raspberry Pi / ARM compatible Docker Image f… 7
arm64v8/phpmyadmin phpMyAdmin - A web interface for MySQL and M… 8
rkcreation/phpmyadmin Pre-configured phpMyAdmin (new theme, settin… 0
drud/phpmyadmin PHPMyadmin container for ddev 1
computersciencehouse/phpmyadmin phpMyAdmin 0
arm32v7/phpmyadmin phpMyAdmin - A web interface for MySQL and M… 2
s390x/phpmyadmin phpMyAdmin - A web interface for MySQL and M… 0
amd64/phpmyadmin phpMyAdmin - A web interface for MySQL and M… 0
ppc64le/phpmyadmin phpMyAdmin - A web interface for MySQL and M… 0
i386/phpmyadmin phpMyAdmin - A web interface for MySQL and M… 0
siwa/phpmyadmin PHPMyAdmin with support of docker secrets 0
morozovgroup/phpmyadmin Openshift Compatible phpmyadmin 0
cmptstks/phpmyadmin phpMyAdmin container 0
mips64le/phpmyadmin phpMyAdmin - A web interface for MySQL and M… 1
arm32v5/phpmyadmin phpMyAdmin - A web interface for MySQL and M… 0
L’image la plus utilisée est simplement phpmyadmin. Pour rappel, PhpMyAdmin est une application WEB qui permet d’administrer un SGBD. Cette image va donc proposer un service sur son port 80 (WEB) et aura besoin d’un lien vers un serveur mySQL.
Nous allons instancier un conteneur pour notre cas, en exposant le service WEB sur le port 8080 de notre LAN :
docker run -d --name pma --link servmdb:bd -p 8080:80 -e PMA_HOST=bd phpmyadmin
Docker charges les layers de l'image puis instancie le conteneur :
Unable to find image 'phpmyadmin:latest' locally
latest: Pulling from library/phpmyadmin
a480a496ba95: Pull complete
95ab1cc5ca33: Pull complete
78ee5e1490ca: Pull complete
e807ae4973d0: Pull complete
8a1846dfbe9a: Pull complete
27f1d0bbde81: Pull complete
8fac5e585cd6: Pull complete
92f601fa0c81: Pull complete
cc366ac8ba10: Pull complete
7694a6cd58cb: Pull complete
068f6a4ec7b4: Pull complete
48201d9d6f62: Pull complete
ec91d8faf678: Pull complete
b2dc28920028: Pull complete
212b78a41125: Pull complete
a08dc175e67a: Pull complete
f3b6f6931565: Pull complete
3f0246b3e956: Pull complete
Digest: sha256:142c7a6ab8d25ea4924194bccbd5b83d2e2060ecd95e7806a88296a996929ed3
Status: Downloaded newer image for phpmyadmin:latest
85e67e833e395f6abbcc47a204d7d38a57b09de01206a3ff35013c29e3a1776a
Dans le conteneur créé, le serveur est désigné par l’alias bd
, et nous passons son nom au paramètre PMA_HOST
utilisé par l’application phpmyadmin. N’est-ce pas simplissime ?
Nos conteneur actifs :
docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
85e67e833e39 phpmyadmin "/docker-entrypoint.…" About a minute ago Up About a minute 0.0.0.0:8080->80/tcp, [::]:8080->80/tcp pma
ae4a9dd8dc8d mariadb "docker-entrypoint.s…" 34 minutes ago Up 34 minutes 3306/tcp servmdb
L’application PhpMyAdmin est immédiatement opérationnelle :
Vous séchez sur l'identifiant et le mot de passe pour administrer phpMyAdmin ?... il s'agit de l'utilisateur root et du mot de passe défini lors de la création du conteneur servmdb, via le paramètre
MYSQL_ROOT_PASSWORD
; je vous laisse chercher...
Docker peut créer des images automatiquement en lisant les instructions d'un Dockerfile. Un Dockerfile est un document texte qui contient toutes les commandes qu'un utilisateur peut appeler sur la ligne de commande pour assembler une image. Avec la commande docker build
, les utilisateurs peuvent créer une version automatisée qui exécute plusieurs instructions de ligne de commande successivement, à partir d’un unique dockerfile.
Pour en savoir plus sur dockerfile : https://docs.docker.com/engine/reference/builder/
Une fois encore, faisons au préalable un peu de ménage dans les conteneurs et les images :
docker stop $(docker ps -aq)
docker rm $(docker ps -aq)
docker rmi $(docker images -q) -f
Nous allons maintenant utiliser dockerfile pour créer notre propre image de serveur nginx. Il faut d’abord créer un dossier de projet, et créer le dockerfile dedans :
cd ~
mkdir projet_nginx
cd projet_nginx
nano dockerfile
Voici le contenu de notre dockerfile :
FROM debian:bookworm
# Installation de nginx :
RUN apt update && apt install -y nginx && apt clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# Copie du fichier de config par défaut :
COPY default /etc/nginx/sites-available/
# Copie d’un fichier index par défaut (ça pourrait être un site complet) :
COPY index.php /var/www/html/
# On expose le port 80
EXPOSE 80
# la commande associée au lancement du conteneur
ENTRYPOINT ["nginx", "-g", "daemon off;"]
Quelques notes :
#
sont, comme souvent, des lignes de commentaires, non traitées par docker.RUN
et donc aussi les layers de l'image finale.
ENTRYPOINT
est la commande qui sera exécutée au lancement du container. Il existe une varianteCMD
, la différence est au niveau de la prise en charge d’une commande ajoutée àdocker run
: elle s’ajoute à la commandeENTRYPOINT
, mais elle remplace celle deCMD
. L’écriture JSON des paramètres ENTRYPOINT ou CMD est importante, elle permet de lancer en modeexec
et nonshell
, et donc de donner le PID 1 au processus (et donc recevoir les signaux Unix, et répondre notamment à undocker stop
).
On crée ensuite localement, dans le dossier du projet, les fichiers destinés à être copiés sur l’image, tels qu’ils apparaissent dans le dockerfile. Tout d’abord, le script index.php
(ligne 10), dans lequel on met simplement la fonction de test de php phpinfo()
:
nano index.php
<?php
$date = date("d-m-Y");
$heure = date("H:i");
print("<center><h1>Bienvenue sur mon site web !<br>");
print("Nous sommes le $date et il est $heure<br>");
print("Voici des infos sur php :<br></h1></center>");
phpinfo();
?>
NB : à la place de ce simple script
phpinfo()
, il se trouve en pratique tout le dossier de l’application que l’on souhaite produire (la partie front-end).
On créé également notre fichier default pour la configuration de nginx (ligne 7)
nano default
La configuration intègre serveur PHP, qui sera un autre conteneur lié, appelé phpfpm en écoute sur le port 9000 (ligne 13):
server {
listen 80 default_server;
root /var/www/html ;
index index.php index.html index.htm ;
server_name _;
# access_log /var/log/nginx/access.log;
# error_log /var/log/nginx/error.log;
location / {
try_files $uri $uri/ =404;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass phpfpm:9000;
fastcgi_param SCRIPT_FILENAME /script$fastcgi_script_name;
}
}
Puis on lance le build, la création de l’image, d'une façon très simple (n'oubliez pas le point final, qui rappelons-le désigne l'emplacement courant) :
docker build -t pm/nginx .
La magie opère ...
[+] Building 13.8s (9/9) FINISHED docker:default
=> [internal] load build definition from dockerfile 0.0s
=> => transferring dockerfile: 511B 0.0s
=> [internal] load metadata for docker.io/library/debian:bookworm 0.9s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> CACHED [1/4] FROM docker.io/library/debian:bookworm@sha256:e11072c1614c08bf88b543fcfe09d75a0426d90896408e926454e88078274fcb 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 58B 0.0s
=> [2/4] RUN apt update && apt install -y nginx && apt clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 12.3s
=> [3/4] COPY default /etc/nginx/sites-available/ 0.1s
=> [4/4] COPY index.php /var/www/html/ 0.1s
=> exporting to image 0.3s
=> => exporting layers 0.2s
=> => writing image sha256:b01230c2db3ceac9f111a142c4c0617e6e3461ab6eeff70c37b7a7fde27d4148 0.0s
=> => naming to docker.io/pm/nginx 0.0s
Utilisons pour l’exemple un volume non lié pour les pages web (et scripts PHP) :
docker volume create html
On lance maintenant un conteneur PHP-FPM, que l’on appelle phpfpm, à partir d’une image à choisir selon la version de PHP que l’on souhaite (voir le docker hub). On attache le volume local au répertoire du conteneur /script
(c’est là où nginx lui dira d’aller chercher les scripts php, conformément à la configuration nginx ci-dessus) :
docker run -d --name phpfpm -v html:/script php:8.3.12-fpm-bookworm
Puis on lance notre conteneur nginx, sans oublier de lier le serveur phpfpm, avec un alias correspondant au nom utilisé dans le fichier de configuration (phpfpm), et d’ajouter le volume html pour la racine du site :
docker run -d -p 8080:80 --link phpfpm -v html:/var/www/html --name sweb pm/nginx
un point sur les conteneurs actifs :
docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
23707fbfc013 pm/nginx "nginx -g 'daemon of…" 5 seconds ago Up 4 seconds 0.0.0.0:8080->80/tcp, [::]:8080->80/tcp sweb
c73ba2c98bcd php:8.3.12-fpm-bookworm "docker-php-entrypoi…" About a minute ago Up About a minute 9000/tcp phpfpm
Et voilà !
Et nous voici rendu à l'étape ultime de ce parcours, où nous allons regarder à l'oeuvre la magie de Docker Compose.
Docker Compose est un outil pour définir et exécuter des applications Docker multi-conteneurs. Avec Compose, on utilise un fichier YAML pour configurer les différents sservices de l’application. Ensuite, avec une seule commande, on crée et démarre tous les services à partir de cette configuration.
L'utilisation de Compose est essentiellement un processus en trois étapes :
docker compose up
et Docker Compose démarre et exécute l'intégralité de votre application.Un fichier YAML (YAML Ain’t a Markup Language) est un format de représentation des données hiérarchique et lisible, basé principalement sur des paires "clé : valeur" et une structure hiérarchique par indentation (comme avec Python). Pour en savoir plus : https://fr.wikipedia.org/wiki/YAML
Un docker-compose.yml ressemble à ceci :
services:
web:
build: .
ports:
- "5000:5000"
volumes:
- .:/code
- logvolume01:/var/log
links:
- redis
redis:
image:redis
volumes:
logvolume01:{}
Il y a une chose importante lorsque vous éditez un fichier YAML : L’indentation doit être faite avec un ou plusieurs espaces, mais jamais avec les tabulations.
Nous allons compléter notre exemple précédent, en utilisant cette fois docker compose. Tout d’abord, supprimons tous les conteneurs, les images et les volumes de notre système :
docker rm -f $(docker ps -aq)
docker rmi $(docker images -q)
docker volume prune
Dans le répertoire du projet, nous ajoutons le fichier docker-compose.yml :
cd ~
cd projet_nginx
nano docker-compose.yml
Modifions le port exposé sur l'hôte (8889) et la version de php-fpm :
services:
web:
build: .
depends_on:
- phpfpm
ports:
- "8889:80"
links:
- "phpfpm"
volumes:
- "html:/var/www/html"
phpfpm:
image: php:8.3.2-fpm-bookworm
volumes:
- "html:/script"
volumes:
html: {}
Il est important de se rendre compte que ce fichier de configuration ne se lit pas ligne par ligne tel un script, mais qu'il décrit un "état final" des services, volumes, ports, liens, ... souhaités.
L'heure est venue de lancer docker compose :
docker compose up -d
Le processus complet de déploiement de l’application se lance :
De nouveau un point sur les conteneurs actifs :
docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
b7de589b0b05 projet_nginx-web "nginx -g 'daemon of…" About a minute ago Up About a minute 0.0.0.0:8889->80/tcp, [::]:8889->80/tcp projet_nginx-web-1
03aec4512d01 php:8.3.2-fpm-bookworm "docker-php-entrypoi…" About a minute ago Up About a minute 9000/tcp projet_nginx-phpfpm-1
Et voilà, un site opérationnel en un clin d'oeil !
Que devient notre exemple vu plus haut avec un serveur de base de données MariaDB et un applicatif phpMyAdmin ? Avec Docker Compose, tout devient concis !
Créons d’abord un répertoire pour le projet :
mkdir ~/pma
cd ~/pma
nano docker-compose.yml
Et on saisit le contenu YAML :
services:
sbd:
image: mariadb:latest
environment:
- MYSQL_ROOT_PASSWORD=mysqlAdmin
volumes:
- "/var/lib/mysql_docker:/var/lib/mysql"
pma:
image: phpmyadmin:latest
links:
- "sbd"
ports:
- "8090:80"
environment:
- PMA_HOST=sbd
Puis on lance docker compose :
docker compose up -d
Est-il possible d'imaginer quelque chose de plus simple ?! Il ne reste plus qu’à se connecter au service en web, sur le port 8090 :
On peut remarquer que les bases de données que nous avons créées auparavant sont toujours présentes ; en effet, en réutilisant le même volume local
/var/lib/mysql_docker
on obtient la persistance des données.
Prenons un dernier exemple, issu du site officiel docker : https://docs.docker.com/compose/wordpress/
Il va nous permettre de déployer un site web wordpress, avec une image spécialement développée pour cela (comme c’est le cas pour beaucoup d’applications connues).
Créons d’abord un répertoire pour le projet :
mkdir ~/wp
cd ~/wp
nano docker-compose.yml
Le contenu du YAML, directement repris du site indiqué ci-dessus ; on change juste le port d'exposition (8001 au lieu de 80) :
services:
db:
image: mariadb:10.6.4-focal
command: '--default-authentication-plugin=mysql_native_password'
volumes:
- db_data:/var/lib/mysql
restart: always
environment:
- MYSQL_ROOT_PASSWORD=somewordpress
- MYSQL_DATABASE=wordpress
- MYSQL_USER=wordpress
- MYSQL_PASSWORD=wordpress
expose:
- 3306
- 33060
wordpress:
image: wordpress:latest
volumes:
- wp_data:/var/www/html
ports:
- 8001:80
restart: always
environment:
- WORDPRESS_DB_HOST=db
- WORDPRESS_DB_USER=wordpress
- WORDPRESS_DB_PASSWORD=wordpress
- WORDPRESS_DB_NAME=wordpress
volumes:
db_data:
wp_data:
Puis on lance docker compose :
docker compose up -d
Il ne reste plus là aussi qu’à se connecter au service web, sur le port 8001 :
L’intérêt majeur des conteneurs est la scalabilité. Avec des VM on peut aussi, mais c’est plus lourd (un OS par VM), mais il est vrai qu'on a plus d’isolation.
Docker standardise le package de l'application. Ainsi l'image docker devient le "livrable", et on assure portabilité et standardisation : on livre plus vite, ce qui facilite l’intégration continue.
Par contre, au niveau sécurité, il demeure le problème du partage du noyau (OS partagé) = plus de failles potentielles et de capillarité.
Pour le développeur :
Nous n’avons pas vu tout ce que Docker Compose offre comme options, notamment celles qui permettent de faire fonctionner plusieurs conteneurs pour un service (réplicas).
Docker Compose permet d’automatiser de déploiement de conteneurs et d'applications. Mais son utilisation a un certain nombre de limites, et pour « orchestrer » les conteneurs, il est très courant d’utiliser plutôt Kubernetes (K8S).
Kubernetes permet notamment de mieux assurer la scalabilité, la disponibilité et le monitoring.
Le conteneur fait tourner l'application ; il doit être immuable, non persistant (on gère la persistance par des volumes)
Enfin voici quelques bonnes pratiques concernant le développement avec Docker & Co :