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
sleep 1 # ça se lance vite, mais quand même...
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 (et créer au besoin) 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. C'est le choix standard et privilégié.Voici un petit schéma récapitulatif de ces trois types :

Il existe aussi des drivers qui permettent de connecter des volumes à des ressources non locales (NFS, SMB/CIFS, CEPH, cloud AWS Elastic Block Store, Azure, ..)
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 | grep access_log
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
IMAGE ID DISK USAGE CONTENT SIZE EXTRA
kl/nginx:latest d064495e2b06 239MB 63.3MB
nginx:latest 7272239bd214 240MB 65.7MB U
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 mieux tout cela, nous allons 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 toutes les indications d’utilisation de l'image mariadb 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
-equi 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
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. Pour cela, nous allons créer un réseau Docker et y connecter à la fois le serveur (servmdb) et un nouveau conteneur client basé sur la même image MariaDB.
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 créés sans spécification réseau particulière sont placés sur le réseau bridge de Docker (celui que nous avons vu précédemment avec l'interface docker0), et peuvent communiquer entre eux via leurs adresses IP (réseau 172.17.0.0/16).
Cependant, cette approche présente des inconvénients :
Docker propose une solution bien plus élégante : les réseaux définis par l'utilisateur (user-defined networks). Ces réseaux permettent :
Voici quelques commandes élémentaires pour la gestion des réseaux.
docker network ls
NETWORK ID NAME DRIVER SCOPE
xxxxxxxxxxxx bridge bridge local
xxxxxxxxxxxx host host local
xxxxxxxxxxxx none null local
docker network create mon-reseau
docker network inspect mon-reseau
docker network connect mon-reseau nom-conteneur
docker network disconnect mon-reseau nom-conteneur
docker network rm mon-reseau
Ceci est impossible s'il existe des conteneurs connectés au réseau.

Les conteneurs sur un même réseau user-defined se découvrent automatiquement par leur nom, grâce au serveur DNS interne de Docker.
Revenons à notre objectif.
Commençons par créer un réseau dédié pour notre application de base de données :
docker network create app-db
Vérifions que le réseau a bien été créé :
docker network ls
Vous devriez voir votre nouveau réseau dans la liste :
NETWORK ID NAME DRIVER SCOPE
xxxxxxxxxxxx app-db bridge local
xxxxxxxxxxxx bridge bridge local
xxxxxxxxxxxx host host local
xxxxxxxxxxxx none null local
Notre conteneur servmdb existant est actuellement sur le réseau bridge par défaut. Nous allons le supprimer et le recréer sur notre nouveau réseau.
Supprimons d'abord le conteneur existant :
docker stop servmdb
docker rm servmdb
Puis recréons-le en le connectant à notre réseau app-db :
docker run -d --name servmdb --network app-db -v /var/lib/mysql_docker:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=mysqlAdmin mariadb
L'option
--network app-dbconnecte directement le conteneur au réseau app-db au moment de sa création.
Vérifions que le conteneur est bien actif :
docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
25d3c10b651c mariadb "docker-entrypoint.s…" 12 seconds ago Up 12 seconds 3306/tcp servmdb
Lançons maintenant un conteneur client temporaire sur le même réseau pour tester la communication :
docker run -it --rm --network app-db mariadb bash
On obtient un prompt bash sur ce conteneur :
root@4104993d7539:/#
Regardons le fichier /etc/hosts :
cat /etc/hosts
La sortie ressemblera à ceci :
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.18.0.3 4104993d7539
Contrairement à l'ancienne méthode
--link(dépréciée), le fichier/etc/hostsne contient plus d'entrée pour servmdb. Cependant, le serveur DNS interne de Docker permet quand même la résolution du nom.
Testons la résolution DNS avec la commande ping (il faudra d'abord l'installer) :
apt update && apt install -y iputils-ping
ping -c 3 servmdb
PING servmdb (172.18.0.2) 56(84) bytes of data.
64 bytes from servmdb.app-db (172.18.0.2): icmp_seq=1 ttl=64 time=0.049 ms
64 bytes from servmdb.app-db (172.18.0.2): icmp_seq=2 ttl=64 time=0.097 ms
64 bytes from servmdb.app-db (172.18.0.2): icmp_seq=3 ttl=64 time=0.120 ms
--- servmdb ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1998ms
rtt min/avg/max/mdev = 0.049/0.088/0.120/0.029 ms
Le nom servmdb est bien résolu par le DNS interne de Docker.
Regardons maintenant les variables d'environnement :
env
HOSTNAME=4104993d7539
PWD=/
HOME=/root
LANG=C.UTF-8
LS_COLORS=rs=0[...]ucf-old=00;90:
MARIADB_VERSION=1:12.1.2+maria~ubu2404
GOSU_VERSION=1.19
TERM=xterm
SHLVL=1
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
_=/usr/bin/env
Avec les réseaux user-defined, il n'y a plus de variables d'environnement automatiquement propagées depuis le conteneur servmdb (comme c'était le cas avec
--link). C'est en fait une bonne pratique de sécurité : les secrets ne sont plus exposés automatiquement.
Revenons au shell du Docker Engine :
exit
Pour nous connecter au serveur MariaDB, nous devons maintenant passer le mot de passe explicitement. Lançons un conteneur client qui ouvre directement le shell MySQL :
docker run -it --rm --network app-db mariadb mariadb -h servmdb -u root -pmysqlAdmin
Note : l'option
-pmysqlAdminpasse le mot de passe (sans espace après -p). En production, il vaudrait mieux utiliser un fichier de configuration ou des secrets Docker.
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)]>
Créons une base de données testdocker :
CREATE DATABASE testdocker;
EXIT;
On revient au shell du Docker Engine. Regardons le contenu du volume local /var/lib/mysql_docker :
ls -lh /var/lib/mysql_docker/
Cette nouvelle base est bien visible :
total 155M
-rw-rw---- 1 999 systemd-journal 4,7M 10 janv. 17:06 aria_log.00000001
-rw-rw---- 1 999 systemd-journal 52 10 janv. 17:06 aria_log_control
-rw-rw---- 1 999 systemd-journal 9 10 janv. 17:07 ddl_recovery.log
-rw-rw---- 1 999 systemd-journal 706 10 janv. 17:06 ib_buffer_pool
-rw-rw---- 1 999 systemd-journal 12M 10 janv. 16:57 ibdata1
-rw-rw---- 1 999 systemd-journal 96M 10 janv. 16:57 ib_logfile0
-rw-rw---- 1 999 systemd-journal 12M 10 janv. 17:07 ibtmp1
-rw-r--r-- 1 999 systemd-journal 14 10 janv. 16:57 mariadb_upgrade_info
-rw-rw---- 1 999 systemd-journal 0 10 janv. 16:57 multi-master.info
drwx------ 2 999 systemd-journal 4,0K 10 janv. 16:57 mysql
drwx------ 2 999 systemd-journal 4,0K 10 janv. 16:57 performance_schema
drwx------ 2 999 systemd-journal 12K 10 janv. 16:57 sys
-rw-rw---- 1 999 systemd-journal 24K 10 janv. 17:07 tc.log
drwx------ 2 999 systemd-journal 4,0K 10 janv. 17:12 testdocker
-rw-rw---- 1 999 systemd-journal 10M 10 janv. 16:57 undo001
-rw-rw---- 1 999 systemd-journal 10M 10 janv. 16:57 undo002
-rw-rw---- 1 999 systemd-journal 10M 10 janv. 16:57 undo003
Comme nous avons mis en place la persistance des données via le volume, si l'on supprime le conteneur serveur servmdb et qu'on lance de nouvelles instances, 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 sur le même réseau :
docker run -d --name servmdb --network app-db -v /var/lib/mysql_docker:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=mysqlAdmin mariadb
On se connecte au shell MySQL, toujours via un conteneur client éphèmère :
docker run -it --rm --network app-db 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;
EXIT;
Notre base testdocker est bien présente :
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
| sys |
| testdocker |
+--------------------+
5 rows in set (0.001 sec)
MariaDB [(none)]> exit;
Bye
Pour illustrer encore plus l'aspect réseau, 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… 1134 [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 26
vulhub/phpmyadmin 1
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… 11
rkcreation/phpmyadmin Pre-configured phpMyAdmin (new theme, settin… 0
computersciencehouse/phpmyadmin phpMyAdmin 0
drud/phpmyadmin PHPMyadmin container for ddev 1
arm32v7/phpmyadmin phpMyAdmin - A web interface for MySQL and M… 2
amd64/phpmyadmin phpMyAdmin - A web interface for MySQL and M… 0
venatrix/phpmyadmin phpMyAdmin 0
s390x/phpmyadmin phpMyAdmin - A web interface for MySQL and M… 0
ppc64le/phpmyadmin phpMyAdmin - A web interface for MySQL and M… 0
cmptstks/phpmyadmin phpMyAdmin container 0
i386/phpmyadmin phpMyAdmin - A web interface for MySQL and M… 0
morozovgroup/phpmyadmin Openshift Compatible phpmyadmin 0
siwa/phpmyadmin PHPMyAdmin with support of docker secrets 0
marvambass/phpmyadmin phpMyAdmin - (marvambass/phpmyadmin) (+ opti… 0
mips64le/phpmyadmin phpMyAdmin - A web interface for MySQL and M… 1
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 de communiquer avec un serveur MySQL/MariaDB.
Nous allons instancier un conteneur, en exposant le service web sur le port 8080 de notre LAN et en le connectant au réseau app-db :
docker run -d --name pma --network app-db -p 8080:80 -e PMA_HOST=servmdb phpmyadmin
Docker charge 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 MariaDB est accessible par son nom servmdb grâce à la résolution DNS du réseau app-db. Nous passons ce nom au paramètre PMA_HOST utilisé par l'application phpMyAdmin.
Nos conteneurs actifs :
docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
125c4fd33ed1 phpmyadmin "/docker-entrypoint.…" 12 seconds ago Up 11 seconds 0.0.0.0:8080->80/tcp, [::]:8080->80/tcp pma
f4378e66d69b mariadb "docker-entrypoint.s…" 9 minutes ago Up 9 minutes 3306/tcp servmdb
L'application phpMyAdmin est immédiatement opérationnelle. Ouvrez un navigateur web et accédez à l'interface (adaptez l'adresse IP selon votre configuration) :

Identifiants de connexion :
rootmysqlAdmin (défini lors de la création du conteneur servmdb via le paramètre MYSQL_ROOT_PASSWORD)Vous pouvez désormais administrer votre serveur MariaDB via cette interface web conviviale, et retrouver les bases de données que nous avons créées précédemment (testdocker et france).
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 du dockerfile), 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 du dockerfile)
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
Créons aussi un réseau dédié pour notre application web :
docker network create app-web
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 conteneur au réseau app-web et le volume local html 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 --network app-web -v html:/script php:8.4.15-fpm-trixie
Puis on lance notre conteneur nginx sur le même réseau :
docker run -d -p 8080:80 --network app-web -v html:/var/www/html --name sweb pm/nginx
Faisons le point sur les conteneurs actifs :
docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
03e6f5b5c73e pm/nginx "nginx -g 'daemon of…" 3 seconds ago Up 2 seconds 0.0.0.0:8080->80/tcp, [::]:8080->80/tcp sweb
2fadec1c9bd8 php:8.4.15-fpm-trixie "docker-php-entrypoi…" 6 seconds ago Up 5 seconds 9000/tcp phpfpm
Et voilà !

L'image nginx que nous venons de créer fonctionne parfaitement, mais elle présente deux petits défauts pour un environnement de production professionnel :
Pour remédier à cela, Docker propose une fonctionnalité clé : le Multi-stage Build (construction en plusieurs étapes).
Le principe est de diviser le Dockerfile en deux parties :
Voici à quoi ressemble notre Dockerfile optimisé pour nginx :
# --- Stage 1 : Préparation des sources ---
FROM debian:trixie AS builder
# On prépare nos fichiers dans un dossier temporaire
WORKDIR /app
COPY index.php .
COPY default .
# --- Stage 2 : Image finale optimisée ---
# On utilise une image officielle basée sur Alpine (très légère, ~40Mo)
FROM nginx:alpine
# On copie la config depuis notre dossier projet local ou depuis le builder
COPY --from=builder /app/default /etc/nginx/conf.d/default.conf
COPY --from=builder /app/index.php /var/www/html/
# Cette image lance nginx automatiquement, pas besoin d'ENTRYPOINT manuel
EXPOSE 80
Dans notre exemple, le stage 1 est très "pauvre". Dans la réalité, lors de cette étape auraient lieu des opérations complexes sur le fichiers (compilations, builds, tests, ...)
On supprime le conteneur nginx précédent :
docker rm -f sweb
Nous sommes toujours dans le répertoire du projet. Nous allons modifier le dockerfile pour créer cette nouvelle image nginx.
nano dockerfile
Remplacez le contenu par le nouveau dockerfile optimisé (ci-dessus).
Il nous faut aussi modifier le fichier de configuration de nginx (il fait référence à un dossier snippets qui n'existe pas dans cette image minimale ; le reste est inchangé) :
nano default
remplacer par :
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$ {
# cette section remplace le snippets/fastcgi-php.conf
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_pass phpfpm:9000;
fastcgi_param SCRIPT_FILENAME /script$fastcgi_script_name;
}
}
On reconstruit la nouvelle image (avec un nom différent) :
docker build -t pm/nginx2 .
Regardons les images :
docker images
On peut voir la différence de taille des images nginx:
IMAGE ID DISK USAGE CONTENT SIZE EXTRA
php:8.4.15-fpm-trixie 38842e604cf7 709MB 182MB U
pm/nginx2:latest f1aee1782fc5 92.3MB 25.9MB
pm/nginx:latest e0ad535b07ca 202MB 54.1MB
On lance le conteneur à partir de cette nouvelle image :
docker run -d -p 8080:80 --network app-web -v html:/var/www/html --name sweb pm/nginx2
On vérifie les conteneurs actifs :
docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
abcd9125f035 pm/nginx2 "/docker-entrypoint.…" 3 seconds ago Up 3 seconds 80/tcp, 0.0.0.0:8080->8080/tcp, [::]:8080->8080/tcp sweb
46fcdf4985a7 php:8.4.15-fpm-trixie "docker-php-entrypoi…" 10 minutes ago Up 10 minutes 9000/tcp phpfpm
Allons voir du côté des images durcies fournies gratuitement par Docker (voir https://hub.docker.com/hardened-images/catalog ; il faut créer un compte Docker pour accéder à ces images).
Pour utiliser une image Docker Hardened (DHI), il y a deux contraintes techniques majeures à gérer dans la configuration :
Il faut d'abord se connecter à Docker :
docker login dhi.io
Disposer d'un compte donne accès à nombre limité mais suffisant d'images durcies. Une contrainte également est qu'on doit préciser le tag lors du pull (on ne peut pas utiliser latest par exemple).
Adaptons notre projet dockerfile, tout d'abord en modifiant le fichier de configuration de nginx :
nano default
Remplacer par ce contenu, affiné pour ce mode "durci" ; la section location est inchangée:
## Section Globale
# On déplace le PID dans une zone inscriptible (/tmp)
pid /tmp/nginx.pid;
events {
worker_connections 1024;
}
http {
# On inclut les types MIME (nécessaire pour le CSS/JS)
include /etc/nginx/mime.types;
# On déplace les fichiers temporaires dans /tmp car le reste est en lecture seule
client_body_temp_path /tmp/client_body;
proxy_temp_path /tmp/proxy;
fastcgi_temp_path /tmp/fastcgi;
uwsgi_temp_path /tmp/uwsgi;
scgi_temp_path /tmp/scgi;
## Section Server (habituelle)
server {
listen 8080; # Port > 1024 obligatoire (non-root)
server_name _;
# Le dossier web standard est désormais ici :
root /usr/share/nginx/html;
index index.php index.html;
location / {
try_files $uri $uri/ =404;
}
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_pass phpfpm:9000;
fastcgi_param SCRIPT_FILENAME /script$fastcgi_script_name;
}
}
}
Modification du dockerfile :
nano dockerfile
# --- STAGE 1 : PRÉPARATION ---
FROM alpine:latest AS builder
WORKDIR /app
# On rassemble les fichiers locaux
COPY default .
COPY index.php .
# --- STAGE 2 : IMAGE DURCIE (DHI) ---
# Utilisation de l'image DHI officielle
FROM dhi.io/nginx:1-debian13-dev
# LABEL pour la traçabilité
LABEL org.opencontainers.image.title="Mon Nginx Durci"
# 1. Copie de la configuration Nginx
# Sur les images durcies, la conf principale est souvent nginx.conf
COPY --from=builder /app/default /etc/nginx/nginx.conf
# 2. Copie du site web
COPY --from=builder /app/index.php /usr/share/nginx/html/index.php
# 3. Documentation du port (Port 8080 obligatoire en non-root)
EXPOSE 8080
# L'utilisateur par défaut est déjà "nonroot" ou "nginx"
# Le CMD est déjà configuré dans l'image de base
Une subtilité supplémentaire de l'écosystème DHI/Hardened :
dhi.io/nginx:1-debian13) : Pas de shell (impossible de "rentrer" dans le conteneur), ultra-sécurisée, pour la Production.dhi.io/nginx:1-debian13-dev) : Contient un shell (bash/sh) et des outils de base, utilisée uniquement pour le Développement ou le débogage. C'est celle que nous avons choisie ici.On supprime le conteneur précédent :
docker rm -f sweb
On reconstruit la nouvelle image durcie :
docker build -t pm/nginx-dhi .
Regardons une nouvelle fois les images :
docker images
et comparer leur taille respectives :
IMAGE ID DISK USAGE CONTENT SIZE EXTRA
php:8.4.15-fpm-trixie 38842e604cf7 709MB 182MB U
pm/nginx-dhi:latest da7a72465d4e 114MB 26.2MB
pm/nginx2:latest 2278927897dc 92.3MB 25.9MB
pm/nginx:latest 125baf7b47a0 202MB 54.1MB
On lance le conteneur sweb depuis cette nouvelle image:
docker run -d -p 8080:8080 --network app-web -v html:/usr/share/nginx/html --name sweb pm/nginx-dhi
Notez bien :
8080 vers le 8080/usr/share/nginx/html)On vérifie les conteneurs actifs :
docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
42f09a821cf1 pm/nginx "/docker-entrypoint.…" 5 minutes ago Up 5 minutes 0.0.0.0:8080->80/tcp, [::]:8080->80/tcp sweb
4d86420624a4 php:8.4.15-fpm-trixie "docker-php-entrypoi…" 5 minutes ago Up 5 minutes 9000/tcp phpfpm
Enfin, on peut créer une dernière image dhi stricte (sans dev), en modifiant le dockerfile (ligne 11) :
docker build -t pm/nginx-dhix .
Regardons une dernière fois les images :
docker images
et vérifier que cette ultime image dhix a un poids optimisé :
IMAGE ID DISK USAGE CONTENT SIZE EXTRA
php:8.4.15-fpm-trixie 38842e604cf7 709MB 182MB U
pm/nginx-dhi:latest da7a72465d4e 114MB 26.2MB
pm/nginx-dhix:latest a84b9241f110 53.4MB 11.8MB
pm/nginx2:latest 2278927897dc 92.3MB 25.9MB
pm/nginx:latest 125baf7b47a0 202MB 54.1MB
On peut aussi vérifier qu'elle est fonctionnelle :
docker rm -f sweb
docker run -d -p 8080:8080 --network app-web -v html:/usr/share/nginx/html --name sweb pm/nginx-dhix

Et nous voici rendu à l'étape ultime de ce parcours, où nous allons regarder à l'œuvre 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 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
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.
Note importante : Avec Docker Compose, les services définis dans un même fichier sont automatiquement placés sur un réseau commun et peuvent se découvrir par leur nom de service.
Nous allons reprendre 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 -a
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:8080"
volumes:
- "html:/usr/share/nginx/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, ... 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
9fd391ce1c19 projet_nginx-web "nginx -g 'daemon of…" 5 seconds ago Up 5 seconds 0.0.0.0:8889->8080/tcp, [::]:8889->8080/tcp projet_nginx-web-1
ac1fd01d7964 php:8.3.2-fpm-bookworm "docker-php-entrypoi…" 5 seconds ago Up 5 seconds 9000/tcp projet_nginx-phpfpm-1
Pour visualiser le réseau créé automatiquement par Docker Compose :
docker network ls
Vous verrez un réseau nommé projet_nginx_default (ou similaire selon le nom de votre dossier) :
NETWORK ID NAME DRIVER SCOPE
76be90f9ffb1 app-db bridge local
5482b89008cd app-web bridge local
e25871728ea9 bridge bridge local
55d02798b4fb host host local
79fa552c8885 none null local
3f347c88a35b projet_nginx_default bridge local
Et voilà, un site "opérationnel" en un clin d'œil !

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é (health check) :
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 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 réside dans leur légèreté et leur densité. Contrairement aux machines virtuelles (VM) qui nécessitent un OS complet chacune, les conteneurs partagent le noyau de l'hôte. Cela permet de faire tourner beaucoup plus d'applications sur un même serveur, bien que l'isolation soit logicielle (namespaces) et donc moins stricte qu'une isolation matérielle (VM).
Docker révolutionne le flux de travail en standardisant le "livrable" : c'est l'image Docker. Cela garantit la parité entre les environnements (développement, test, production). On élimine ainsi le célèbre problème du "pourtant, ça marche sur ma machine", ce qui accélère considérablement les pipelines d'intégration continue (CI/CD).
Cependant, cette architecture présente des défis de sécurité : le partage du noyau implique que si ce dernier est compromis, tous les conteneurs le sont. La surface d'attaque est potentiellement plus grande, facilitant les mouvements latéraux en cas d'intrusion si les droits ne sont pas restreints.
Nous avons utilisé Docker Compose pour gérer une application multi-conteneurs sur une seule machine. Cependant, pour la production à grande échelle, ses limites se font sentir (notamment en cas de panne du serveur hôte). Pour assurer la haute disponibilité et la scalabilité horizontale (répartition de charge), on privilégiera un orchestrateur de conteneurs comme Kubernetes (K8s), capable de gérer des clusters de plusieurs serveurs (multi-nœuds).
Le conteneur doit être immuable et éphémère (stateless) : toute persistance de données doit être déléguée à des volumes.
Pour garantir performance et sécurité, il est crucial de :
alpine) pour réduire la surface d'attaque.exec ou JSON) pour gérer correctement les signaux d'arrêt système.root dans le conteneur final.