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 -q)
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
ee3a09d2248a: Pull complete
5b5fa0b64d74: Pull complete
7382b41547b8: Pull complete
1733a4cd5954: Pull complete
5b219a92f92a: Pull complete
9ee60c6c0558: Pull complete
114e699da838: Pull complete
11488ed04caf: Download complete
adeb5aba46ee: Download complete
Digest: sha256:fb01117203ff38c2f9af91db1a7409459182a37c87cced5cb442d1d8fcc66d19
Status: Downloaded newer image for nginx:latest
6f67ce7ed3484eb4ecf30c37880360556132c718ef7748be9eac53748c9c3410
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
8586aa918ff4 nginx "/docker-entrypoint.…" About a minute ago Up About a minute 0.0.0.0:8090->80/tcp, [::]:8090->80/tcp srvweb
On peut également scruter les différentes composantes et paramètres du conteneur, avec :
docker inspect srvweb
On y trouvera beaucoup d'informations, comme par exemple :
[
{
"Id": "8586aa918ff41f51a60669aa772867aa7a60e516eb2bd5ca55afac8be1dee460",
"Created": "2025-12-11T08:29:02.113312719Z",
"Path": "/docker-entrypoint.sh",
"Args": [
"nginx",
"-g",
"daemon off;"
],
"State": {
"Status": "running",
...
"PortBindings": {
"80/tcp": [
{
"HostIp": "",
"HostPort": "8090"
}
]
},
...
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"NGINX_VERSION=1.29.4",
"NJS_VERSION=0.9.4",
"NJS_RELEASE=1~trixie",
"PKG_RELEASE=1~trixie",
"DYNPKG_RELEASE=1~trixie"
],
"Cmd": [
"nginx",
"-g",
"daemon off;"
],
"Image": "nginx",
"Volumes": null,
"WorkingDir": "",
"Entrypoint": [
"/docker-entrypoint.sh"
],
"Labels": {
"maintainer": "NGINX Docker Maintainers <docker-maint@nginx.com>"
},
"StopSignal": "SIGQUIT"
},
...
]
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@8586aa918ff4:/#
On installe l'éditeur nano afin de modifier le fichier html :
apt update && apt upgrade sh
apt install nano
nano /usr/share/nginx/html/index.html
exit
Je vous propose cet exemple :
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>La Gloire des Rillettes du Mans</title>
<link href="https://fonts.googleapis.com/css2?family=Lobster&family=Montserrat:wght@400;900&display=swap" rel="stylesheet">
<style>
/* --- STYLE & COULEURS --- */
body {
margin: 0;
padding: 0;
background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab);
background-size: 400% 400%;
animation: gradientBG 15s ease infinite;
font-family: 'Montserrat', sans-serif;
color: white;
overflow-x: hidden;
text-align: center;
}
@keyframes gradientBG {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
h1 {
font-family: 'Lobster', cursive;
font-size: 5rem;
text-shadow: 4px 4px 0px #ff0055, 8px 8px 0px #000000;
margin-top: 50px;
animation: bounce 2s infinite;
pointer-events: none; /* Laisse passer les clics */
}
h2 {
font-size: 2rem;
color: #ffd700;
text-transform: uppercase;
letter-spacing: 5px;
margin-bottom: 50px;
}
.container {
position: relative;
z-index: 10;
padding: 20px;
}
.card {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border: 2px solid rgba(255, 255, 255, 0.2);
border-radius: 20px;
padding: 40px;
max-width: 600px;
margin: 0 auto 50px auto;
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
transition: transform 0.3s, box-shadow 0.3s;
cursor: pointer;
}
.card:hover {
transform: scale(1.05) rotate(1deg);
box-shadow: 0 0 50px #ffcc00;
background: rgba(255, 255, 255, 0.2);
}
p {
font-size: 1.2rem;
line-height: 1.6;
}
/* Le Bouton Magique */
#mega-btn {
background: #ffcc00;
color: #d32f2f;
font-family: 'Lobster', cursive;
font-size: 2.5rem;
padding: 20px 50px;
border: none;
border-radius: 50px;
cursor: pointer;
box-shadow: 0 10px 0 #b38f00, 0 20px 20px rgba(0,0,0,0.4);
transition: all 0.1s;
margin-bottom: 100px;
position: relative;
overflow: hidden;
}
#mega-btn:active {
transform: translateY(10px);
box-shadow: 0 0 0 #b38f00, 0 0 0 rgba(0,0,0,0);
}
#mega-btn:hover {
background: #ffe066;
}
/* Animations Emojis Flottants */
.emoji-rain {
position: fixed;
top: -50px;
font-size: 30px;
z-index: 1;
user-select: none;
pointer-events: none;
}
/* Animations Clés */
@keyframes bounce {
0%, 20%, 50%, 80%, 100% {transform: translateY(0);}
40% {transform: translateY(-30px);}
60% {transform: translateY(-15px);}
}
@keyframes spin {
100% { transform:rotate(360deg); }
}
.spinner {
font-size: 80px;
display: inline-block;
animation: spin 4s linear infinite;
}
/* Curseur personnalisé */
.cursor-trail {
position: absolute;
width: 10px;
height: 10px;
background: #ffd700;
border-radius: 50%;
pointer-events: none;
z-index: 9999;
}
</style>
</head>
<body>
<div class="container">
<h1>Rillettes du Mans</h1>
<h2>Le Goût de l'Absolu</h2>
<div class="card" id="card1">
<span class="spinner">🐷</span>
<h3>Une Tradition Mythique</h3>
<p>Nées dans la Sarthe, élevées au rang d'art. Une cuisson lente, une texture filandreuse, un goût qui explose en bouche !</p>
</div>
<div class="card" id="card2">
<span class="spinner">🍞</span>
<h3>Le Duo Parfait</h3>
<p>Une tranche de pain de campagne grillée, une couche généreuse de rillettes, et c'est le paradis assuré. Pas besoin de cornichons, la pureté suffit !</p>
</div>
<button id="mega-btn">JE VEUX DU GRAS !</button>
</div>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.5.1/dist/confetti.browser.min.js"></script>
<script>
$(document).ready(function() {
// 1. Animation d'apparition des cartes au chargement
$(".card").hide().fadeIn(2000);
$("#card1").css("left", "-100px").animate({left: "0px"}, 1000);
$("#card2").css("left", "100px").animate({left: "0px"}, 1000);
// 2. Pluie d'Emojis (Cochon, Viande, Pain)
function createRain() {
const emojis = ['🐷', '🍖', '🥖', '🐖', '🧂'];
const $el = $('<div class="emoji-rain"></div>');
const char = emojis[Math.floor(Math.random() * emojis.length)];
$el.text(char);
$el.css({
left: Math.random() * 100 + 'vw',
animationDuration: (Math.random() * 2 + 3) + 's', // Vitesse aléatoire
opacity: Math.random()
});
$('body').append($el);
$el.animate({
top: "110vh"
}, Math.random() * 3000 + 3000, "linear", function() {
$(this).remove();
});
}
// Lancer la pluie en continu
setInterval(createRain, 300);
// 3. Effet de traînée de souris (Mouse Trail)
$(document).on('mousemove', function(e) {
const $trail = $('<div class="cursor-trail"></div>');
$trail.css({
left: e.pageX,
top: e.pageY
});
$('body').append($trail);
$trail.fadeOut(500, function() {
$(this).remove();
});
});
// 4. Interaction Hover sur les cartes (jQuery + CSS)
$(".card").hover(function() {
$(this).find("h3").css("color", "#ffcc00");
$(this).find(".spinner").css("animation-duration", "0.5s");
}, function() {
$(this).find("h3").css("color", "white");
$(this).find(".spinner").css("animation-duration", "4s");
});
// 5. LE GROS BOUTON (Explosion de confettis)
$("#mega-btn").click(function() {
// Changer le texte
$(this).text("MIAM MIAM MIAM !");
// Secouer le bouton
$(this).animate({marginLeft: "-10px"}, 50)
.animate({marginLeft: "10px"}, 50)
.animate({marginLeft: "-10px"}, 50)
.animate({marginLeft: "0px"}, 50);
// Lancer les confettis (Bibliothèque Canvas Confetti)
var duration = 3000;
var animationEnd = Date.now() + duration;
var defaults = { startVelocity: 30, spread: 360, ticks: 60, zIndex: 0 };
var interval = setInterval(function() {
var timeLeft = animationEnd - Date.now();
if (timeLeft <= 0) {
return clearInterval(interval);
}
var particleCount = 50 * (timeLeft / duration);
// random fall
confetti(Object.assign({}, defaults, { particleCount, origin: { x: Math.random(), y: Math.random() - 0.2 } }));
}, 250);
// Ajouter des effets sonores visuels (texte qui pop)
let phrases = ["C'est bon !", "Le gras c'est la vie !", "Sarthe Power !"];
let phrase = phrases[Math.floor(Math.random() * phrases.length)];
let $pop = $("<h2 style='position:fixed; top:50%; left:50%; transform:translate(-50%, -50%); font-size:6rem; color:yellow; text-shadow: 0 0 20px red; z-index:9999; display:none'>" + phrase + "</h2>");
$('body').append($pop);
$pop.fadeIn(100).delay(800).fadeOut(300, function(){ $(this).remove(); });
});
});
</script>
</body>
</html>
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 rendu sur un navigateur est assez sympa, n'est ce pas ?
Le problème est que si le conteneur est relancé depuis son image, les modifications apportées au fichier index.html seront perdues :
docker stop srvweb
docker rm srvweb
docker run -d --name srvweb -p 8090:80 nginx
curl 127.0.0.1:8090
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 le contenu HTML de votre choix (ou celui proposé juste avant).
On vérifie que le serveur web du conteneur utilise bien désormais ce fichier :
curl 127.0.0.1:8090
Essayez aussi sur un navigateur web http://172.16.90.73:8090 ; l'ip est à adapter évidemment selon votre situation.

Et pourquoi pas un jeu ? retro-snake
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)
On aura ici uniquement l'info sur le volume monvolume :
[
{
"CreatedAt": "2025-12-11T10:23:21+01:00",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/monvolume/_data",
"Name": "monvolume",
"Options": null,
"Scope": "local"
}
]
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 standard 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@8b6e9067a167:/#
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 donc ê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
172.17.0.1 - - [11/Dec/2025:11:26:55 +0100] "GET / HTTP/1.1" 200 9987 "-" "curl/8.14.1" "-"
172.17.0.1 - - [11/Dec/2025:11:27:12 +0100] "GET / HTTP/1.1" 200 9987 "-" "curl/8.14.1" "-"
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 -q)
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
runté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).Pour plus de clarté, on va insérer des retours à la ligne (avec \) dans notre longue commande :
docker run -d \
-v /var/lib/mysql_docker:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=mysqlAdmin \
--name servmdb \
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.
Inspectons le conteneur :
docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' servmdb
ou encore de cette façon (en exécutant la commande env sur le conteneur lui-même) :
docker exec servmdb env
docker inspect --format='{{json .Mounts}}' servmdb
Sur l’hôte (Docker Engine), on constate que les fichiers de la base de données sont bien présents dans /var/lib/mysql_docker (qui a été créé automatiquement par docker) :
ls -laht /var/lib/mysql_docker/
retournera quelque chose comme cela :
total 155M
drwxr-xr-x 5 999 systemd-journal 4,0K 11 déc. 11:44 .
-rw-rw---- 1 999 systemd-journal 24K 11 déc. 11:44 tc.log
-rw-rw---- 1 999 systemd-journal 12M 11 déc. 11:44 ibtmp1
-rw-rw---- 1 999 systemd-journal 9 11 déc. 11:44 ddl_recovery.log
-rw-rw---- 1 999 systemd-journal 4,7M 11 déc. 11:44 aria_log.00000001
-rw-rw---- 1 999 systemd-journal 52 11 déc. 11:44 aria_log_control
-rw-rw---- 1 999 systemd-journal 706 11 déc. 11:44 ib_buffer_pool
-rw------- 1 999 systemd-journal 118 11 déc. 11:44 .my-healthcheck.cnf
drwx------ 2 999 systemd-journal 4,0K 11 déc. 11:44 mysql
-rw-rw---- 1 999 systemd-journal 0 11 déc. 11:44 multi-master.info
-rw-r--r-- 1 999 systemd-journal 14 11 déc. 11:44 mariadb_upgrade_info
-rw-rw---- 1 999 systemd-journal 96M 11 déc. 11:44 ib_logfile0
-rw-rw---- 1 999 systemd-journal 12M 11 déc. 11:44 ibdata1
-rw-rw---- 1 999 systemd-journal 10M 11 déc. 11:44 undo001
-rw-rw---- 1 999 systemd-journal 10M 11 déc. 11:44 undo002
-rw-rw---- 1 999 systemd-journal 10M 11 déc. 11:44 undo003
drwx------ 2 999 systemd-journal 12K 11 déc. 11:44 sys
drwx------ 2 999 systemd-journal 4,0K 11 déc. 11:44 performance_schema
drwxr-xr-x 26 root root 4,0K 11 déc. 11:44 ..
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 (en cours de dépréciation) permet à un conteneur d’avoir accès plus "élaboré" sur autre conteneur, au travers 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:: ip6-localnet
ff00:: ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
172.17.0.2 xy 2bb84ccc22ec servmdb
172.17.0.3 f184575535ce
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 mariadb -h servmdb -u root -pmysqlAdmin
NB : les liaisons permettaient de récupérer les variables d'environnement du conteneur (notamment le mot de passe admin sql avec
XY_MYSQL_ROOT_PASSWORD), mais ceci a été supprimé dans les dernières versions de Docker. Il faut donc connaître le mot de passe admin.
Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MariaDB connection id is 6
Server version: 12.1.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)]>
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 11 déc. 11:44 aria_log.00000001
-rw-rw---- 1 999 systemd-journal 52 11 déc. 11:44 aria_log_control
-rw-rw---- 1 999 systemd-journal 9 11 déc. 11:44 ddl_recovery.log
-rw-rw---- 1 999 systemd-journal 706 11 déc. 11:44 ib_buffer_pool
-rw-rw---- 1 999 systemd-journal 12M 11 déc. 11:44 ibdata1
-rw-rw---- 1 999 systemd-journal 96M 11 déc. 11:44 ib_logfile0
-rw-rw---- 1 999 systemd-journal 12M 11 déc. 11:44 ibtmp1
-rw-r--r-- 1 999 systemd-journal 14 11 déc. 11:44 mariadb_upgrade_info
-rw-rw---- 1 999 systemd-journal 0 11 déc. 11:44 multi-master.info
drwx------ 2 999 systemd-journal 4,0K 11 déc. 11:44 mysql
drwx------ 2 999 systemd-journal 4,0K 11 déc. 11:44 performance_schema
drwx------ 2 999 systemd-journal 12K 11 déc. 11:44 sys
-rw-rw---- 1 999 systemd-journal 24K 11 déc. 11:44 tc.log
drwx------ 2 999 systemd-journal 4,0K 11 déc. 12:12 testdocker
-rw-rw---- 1 999 systemd-journal 10M 11 déc. 11:44 undo001
-rw-rw---- 1 999 systemd-journal 10M 11 déc. 11:44 undo002
-rw-rw---- 1 999 systemd-journal 10M 11 déc. 11:44 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 mariadb -h servmdb -u root -pmysqlAdmin
Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MariaDB connection id is 3
Server version: 12.1.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;
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)
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… 1132 [OK]
phpmyadmin/phpmyadmin A web interface for MySQL and MariaDB. 1202
bitnami/phpmyadmin Bitnami Secure Image for phpmyadmin 49
bitnamicharts/phpmyadmin Bitnam Helm chart for phpMyAdmin 0
elestio/phpmyadmin Phpmyadmin, verified and packaged by Elestio 0
shinsenter/phpmyadmin 🔋 (PHP / phpMyAdmin) Production-ready Docke… 3
linuxserver/phpmyadmin 22
vulhub/phpmyadmin 1
nazarpc/phpmyadmin phpMyAdmin as Docker container, based on off… 61
...
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
5e4542c43815: Pull complete
34d0d313218a: Pull complete
b33e61c85737: Pull complete
1733a4cd5954: Pull complete
543828ab9fd4: Pull complete
9ab8f1d104c5: Pull complete
d5ce62661451: Pull complete
60df91c45d0a: Pull complete
e16d68226cdd: Pull complete
b1c5c71ad843: Pull complete
da081f25a1b3: Pull complete
b1f8710b0f16: Pull complete
4f4fb700ef54: Pull complete
a72b699530ff: Pull complete
c5e0ba6f6712: Pull complete
a4482e26b5c3: Pull complete
745a60025a92: Pull complete
0c3a8464b740: Pull complete
a923251b43fa: Pull complete
99cb708df8f0: Pull complete
3ce7470ee145: Pull complete
57a726ef1745: Download complete
Digest: sha256:86701cba0ac0397e5d0f39f398192117cbfa8ba11f2c66b9b8556ee04776b6d3
Status: Downloaded newer image for phpmyadmin:latest
55f4f83a0f027dfb15171c68995ae5b02272ddbbc3be95defd6e6de0b3b26a06
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
55f4f83a0f02 phpmyadmin "/docker-entrypoint.…" 25 seconds ago Up 24 seconds 0.0.0.0:8080->80/tcp, [::]:8080->80/tcp pma
8ecf9089f664 mariadb "docker-entrypoint.s…" About an hour ago Up About an hour 3306/tcp servmdb
L’application PhpMyAdmin est immédiatement opérationnelle :

Rappel :
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 -q)
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:trixie
# 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 ou une application complète) :
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.
ENTRYPOINTest 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 modeexecet 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 21.6s (9/9) FINISHED docker:default
=> [internal] load build definition from dockerfile 0.0s
=> => transferring dockerfile: 530B 0.0s
=> [internal] load metadata for docker.io/library/debian:trixie 3.1s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [1/4] FROM docker.io/library/debian:trixie@sha256:0d01188e8dd0ac63bf155900fad49279131a876a1ea7fac 10.2s
=> => resolve docker.io/library/debian:trixie@sha256:0d01188e8dd0ac63bf155900fad49279131a876a1ea7fac9 0.0s
=> => sha256:2981f7e8980b9f4b6605026e1c5f99b4971ebba15f626e46904554de09f324f4 49.29MB / 49.29MB 9.1s
=> => extracting sha256:2981f7e8980b9f4b6605026e1c5f99b4971ebba15f626e46904554de09f324f4 1.1s
=> [internal] load build context 0.0s
=> => transferring context: 751B 0.0s
=> [2/4] RUN apt update && apt install -y nginx && apt clean && rm -rf /var/lib/apt/lists/* /tmp/* /v 7.2s
=> [3/4] COPY default /etc/nginx/sites-available/ 0.0s
=> [4/4] COPY index.php /var/www/html/ 0.0s
=> exporting to image 0.8s
=> => exporting layers 0.5s
=> => exporting manifest sha256:fe93e6e1c0cba3c78e2fceb7365b5dcc6636395769c2924f90e667221c5c4e52 0.0s
=> => exporting config sha256:55854db9be44a6cd644ad2ff8ec643a2e29ae84c539570a0e6888abebdb7c1fc 0.0s
=> => exporting attestation manifest sha256:93d004d73b6152170c9c0e73ed23edcce244662846284736c5eb01e03 0.0s
=> => exporting manifest list sha256:af587f6599f8bdfea907e8966f96e6fcfca0084bbc29b56b35056f9ff9190984 0.0s
=> => naming to docker.io/pm/nginx:latest 0.0s
=> => unpacking to docker.io/pm/nginx:latest
Utilisons pour l’exemple un volume html 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.4.15-fpm-trixie
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
0412e4442424 pm/nginx "nginx -g 'daemon of…" 3 minutes ago Up 3 minutes 0.0.0.0:8080->80/tcp, [::]:8080->80/tcp sweb
5d9eb0c5880a php:8.4.15-fpm-trixie "docker-php-entrypoi…" 4 minutes ago Up 4 minutes 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 services 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.yml afin qu'ils puissent être exécutés ensemble dans un environnement isolé.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 fichier docker-compose.yml ressemble à ceci :
services:
web:
build: .
ports:
- "5000:5000"
volumes:
- .:/code # volume "bind mount"
- logvolume01:/var/log # volume nommé
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 -q)
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 comprendre que ce fichier de configuration ne se lit pas ligne par ligne tel un script (comme le dockerfile par exemple), 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
0f0695ff71ca projet_nginx-web "nginx -g 'daemon of…" 6 seconds ago Up 5 seconds 0.0.0.0:8889->80/tcp, [::]:8889->80/tcp projet_nginx-web-1
742a3d60dd78 php:8.3.2-fpm-bookworm "docker-php-entrypoi…" 12 seconds ago Up 5 seconds 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
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_dockeron obtient la persistance des données.
Dans notre exemple précédent, les deux conteneurs ont été lancés en même temps, sans dépendance entre eux.
Il est possible de modifier ce comportement, avec deux approches.
Mais d'abord, supprimons ce qui tourne :
docker compose down
Voici comment on peut demander à compose de lancer le conteneur PhpMyAdmin une fois que le conteneur MariaDB l'est.
services:
sbd:
image: mariadb:latest
environment:
- MYSQL_ROOT_PASSWORD=mysqlAdmin
volumes:
- "/var/lib/mysql_docker:/var/lib/mysql"
pma:
image: phpmyadmin:latest
depends_on:
- sbd
ports:
- "8090:80"
environment:
- PMA_HOST=sbd
Sur les lignes 10 et 11 l'instruction depends_on indique que le conteneur pma doit attendre que sdb soit démarré.
Cette fois on demande à Compose de lancer le conteneur PhpMyAdmin une fois que le service MariaDB est opérationnel (et pas seulement le conteneur démarré). Pour cela, il nous faut une "sonde de santé" :
services:
sbd:
image: mariadb:latest
environment:
- MYSQL_ROOT_PASSWORD=mysqlAdmin
volumes:
- "/var/lib/mysql_docker:/var/lib/mysql"
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 10s # On teste toutes les 10s
timeout: 5s
retries: 5 # 5 échecs acceptés
start_period: 30s # On ignore les échecs pendant les 30 premières secondes.
pma:
image: phpmyadmin:latest
# On change le depends_on simple pour une version conditionnelle
depends_on:
sbd:
condition: service_healthy
ports:
- "8090:80"
environment:
- PMA_HOST=sbd
Maintenant :
Le healthcheck sur sbd : Docker va exécuter le script healthcheck.sh --connect --innodb_initialized (fourni avec le paquet MariaDB) à l'intérieur du conteneur toutes les 10 secondes.
Tant que MariaDB démarre (initiation des fichiers, création des tables système...), la commande échoue. Le statut du conteneur sera health: starting.
Dès que MariaDB répond "mysqld is alive", le statut passe à healthy.
La condition dans pma : condition: service_healthy bloque le démarrage de PhpMyAdmin tant que le sdb n'est pas healthy.
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 inspiré du site indiqué ci-dessus, avec :
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
wordpress:
image: wordpress:latest
volumes:
- wp_data:/var/www/html
ports:
- 8001:80
restart: always
depends_on:
- db
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 :
