Conteneurs en pratique#
Jusqu’à présent nous avons exploré les fondements théoriques de la conteneurisation. Il est temps de mettre les mains dans le moteur. Ce chapitre couvre le cycle de vie complet d’un conteneur, les commandes du quotidien et les mécanismes de persistance des données.
Le cycle de vie d’un conteneur#
Un conteneur n’est pas simplement « arrêté » ou « en marche ». Il traverse une suite d”états bien définis, chacun correspondant à une étape précise de son existence.
Pensez à un conteneur comme à un programme de concert. Il y a une phase de préparation (la salle est montée), une phase de fonctionnement (le concert), une pause éventuelle (entracte), un arrêt (fin du concert), et enfin le démontage de la salle. Chaque étape est distincte et réversible (sauf la dernière).
Les états en détail#
IMAGE → CREATED (docker create). Docker alloue un espace de stockage pour la couche lecture-écriture du conteneur, configure les namespaces et les cgroups, mais ne démarre pas encore le processus. La commande docker create est utile quand on veut préparer un conteneur à l’avance sans l’exécuter immédiatement.
CREATED → RUNNING (docker start). Docker lance le PID 1 du conteneur — le processus défini par CMD ou ENTRYPOINT dans l’image. À cet instant, les namespaces sont activés, l’interface réseau est configurée, et le processus démarre.
RUNNING → PAUSED (docker pause). Docker envoie un signal SIGSTOP à tous les processus du conteneur via les cgroups. Les processus sont gelés en l’état — ils ne consomment plus de CPU mais restent en mémoire. Utile pour prendre un snapshot cohérent.
RUNNING → STOPPED (docker stop ou docker kill). docker stop envoie d’abord SIGTERM (arrêt gracieux), attend 10 secondes, puis envoie SIGKILL si nécessaire. docker kill envoie directement SIGKILL. La couche R/W est conservée — vous pouvez docker start à nouveau et retrouver vos fichiers.
STOPPED → REMOVED (docker rm). La couche R/W est supprimée. Irréversible. Avec docker run --rm, cette suppression se fait automatiquement dès que le conteneur s’arrête.
Les commandes essentielles#
Lancer un conteneur#
# Forme minimale : lance nginx en avant-plan (foreground)
docker run nginx
# Détaché (-d) avec un nom (--name) et mapping de port (-p)
docker run -d --name mon-nginx -p 8080:80 nginx
# Interactif avec un pseudo-terminal (-it) : lance bash dans ubuntu
docker run -it ubuntu bash
# Éphémère : supprimé automatiquement après arrêt (--rm)
docker run --rm -it python:3.12-slim python3
# Avec des variables d'environnement (-e)
docker run -d \
-e POSTGRES_PASSWORD=secret \
-e POSTGRES_DB=myapp \
--name postgres \
postgres:16
# Avec un fichier de variables d'environnement
docker run -d --env-file .env --name mon-app mon-image:latest
Inspecter et surveiller#
# Lister les conteneurs en cours d'exécution
docker ps
# Tous les conteneurs (y compris arrêtés)
docker ps -a
# Logs d'un conteneur (avec suivi en temps réel)
docker logs -f mon-nginx
# Dernières 100 lignes avec timestamps
docker logs --tail 100 --timestamps mon-nginx
# Statistiques en temps réel (CPU, RAM, réseau, I/O disque)
docker stats
# Inspection complète (JSON détaillé)
docker inspect mon-nginx
# Exécuter une commande dans un conteneur en cours
docker exec mon-nginx nginx -t # test de config nginx
docker exec -it mon-nginx bash # shell interactif
Gérer le cycle de vie#
# Arrêter proprement (SIGTERM puis SIGKILL après délai)
docker stop mon-nginx
# Arrêter immédiatement (SIGKILL)
docker kill mon-nginx
# Redémarrer
docker restart mon-nginx
# Supprimer un conteneur arrêté
docker rm mon-nginx
# Supprimer un conteneur en cours (forcer)
docker rm -f mon-nginx
# Nettoyage : supprimer tous les conteneurs arrêtés
docker container prune
Options de docker run : la référence#
Volumes : la persistance des données#
Un conteneur est, par défaut, éphémère : quand vous le supprimez avec docker rm, la couche lecture-écriture disparaît avec lui. Tout fichier créé dans le conteneur est perdu. Pour les bases de données, les fichiers uploadés par des utilisateurs, les logs persistants, il faut un mécanisme de persistance.
Docker offre trois mécanismes distincts, chacun adapté à des cas d’usage différents.
Bind mounts#
Un bind mount monte directement un répertoire ou un fichier de la machine hôte dans le conteneur. Les deux voient le même contenu en temps réel.
# Monter le répertoire courant dans /app (développement)
docker run -v $(pwd):/app -w /app python:3.12-slim python3 main.py
# Monter un fichier de configuration spécifique (read-only)
docker run -v /etc/nginx/nginx.conf:/etc/nginx/nginx.conf:ro nginx
# Monter un répertoire de données
docker run -d \
-v /data/postgres:/var/lib/postgresql/data \
--name postgres \
postgres:16
Le bind mount est idéal en développement : vos modifications de code sont immédiatement visibles dans le conteneur sans avoir à rebuilder l’image. En production, c’est risqué car il crée une dépendance à la structure de fichiers de l’hôte spécifique.
Volumes nommés#
Un volume nommé est géré entièrement par Docker. Il est stocké dans /var/lib/docker/volumes/<nom>/ sur l’hôte Linux, mais vous n’interagissez pas directement avec ce chemin.
# Créer un volume explicitement
docker volume create mes-donnees
# Utiliser un volume dans un conteneur
docker run -d \
-v mes-donnees:/var/lib/postgresql/data \
--name postgres \
postgres:16
# Inspecter un volume
docker volume inspect mes-donnees
# Lister les volumes
docker volume ls
# Nettoyer les volumes non utilisés
docker volume prune
L’avantage des volumes nommés est leur portabilité : ils peuvent être sauvegardés, migrés, et partagés entre conteneurs facilement. Un volume peut être utilisé par plusieurs conteneurs simultanément (lecture seule recommandée pour la cohérence).
tmpfs#
Un tmpfs est un système de fichiers stocké en mémoire vive. Les données disparaissent à l’arrêt du conteneur. C’est parfait pour les fichiers temporaires qui ne doivent jamais toucher le disque (cache, fichiers de session, tokens secrets temporaires).
# tmpfs de 100 MB dans /tmp
docker run --tmpfs /tmp:size=100m,mode=1777 mon-image
# Plusieurs tmpfs
docker run \
--tmpfs /tmp \
--tmpfs /run:size=50m \
mon-image
Variables d’environnement et configuration#
Les variables d’environnement sont le mécanisme principal pour configurer un conteneur sans modifier son image. C’est l’un des principes des applications twelve-factor : la configuration doit venir de l’environnement, pas du code.
# Variable simple
docker run -e DATABASE_URL=postgresql://localhost/myapp mon-image
# Plusieurs variables
docker run \
-e DB_HOST=postgres \
-e DB_PORT=5432 \
-e DB_NAME=myapp \
-e REDIS_URL=redis://redis:6379 \
mon-image
# Fichier .env (une variable par ligne, sans valeurs entre guillemets)
docker run --env-file .env mon-image
Sécurité : ne jamais mettre de secrets dans les variables d’environnement de l’image
Les variables d’environnement définies dans le Dockerfile avec ENV sont visibles dans docker inspect, dans les logs, et dans les outils de monitoring. Ne jamais mettre de mots de passe, clés API ou tokens dans des instructions ENV du Dockerfile. Utilisez les Docker Secrets (en production avec Swarm/Kubernetes) ou des volumes montant des fichiers de secrets depuis un gestionnaire de secrets (Vault, AWS Secrets Manager…).
Mapping de ports : comprendre l’exposition#
Un conteneur vit dans son propre namespace réseau — il a sa propre adresse IP privée (par exemple 172.17.0.2). Pour qu’une application extérieure (votre navigateur, un autre service) puisse atteindre un service dans le conteneur, Docker doit créer une règle de translation d’adresse (NAT) via iptables.
# Mapper le port 80 du conteneur sur le port 8080 de l'hôte
docker run -p 8080:80 nginx
# Uniquement sur localhost (sécurité : pas exposé sur le réseau)
docker run -p 127.0.0.1:8080:80 nginx
# Publier sur tous les ports EXPOSE (port aléatoire côté hôte)
docker run -P nginx
# Voir les mappings de ports d'un conteneur
docker port mon-nginx
Il est important de distinguer EXPOSE et la publication de ports :
EXPOSE 80dans le Dockerfile est de la documentation — il indique qu’un service écoute sur ce port, mais ne l’ouvre pas.-p 8080:80dansdocker runpublie réellement le port en créant la règle NAT.
Un conteneur = un processus (PID 1)#
L’analogie Unix fondamentale : un conteneur bien conçu ne fait tourner qu’un seul processus principal (PID 1). C’est le processus défini par CMD ou ENTRYPOINT dans l’image.
Pourquoi est-ce important ? Parce que le cycle de vie du conteneur est lié à celui de son PID 1 :
Si le PID 1 se termine avec le code 0 → conteneur stopped (succès)
Si le PID 1 se termine avec un code non-nul → conteneur stopped (erreur)
Si le PID 1 crashe → le conteneur redémarre (si
--restartest configuré)
# Simulation : inspection du résultat de docker inspect (JSON)
# Ce JSON est simulé pour illustrer ce que retourne la vraie commande
docker_inspect_simule = {
"Id": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2",
"Name": "/mon-nginx",
"State": {
"Status": "running",
"Running": True,
"Paused": False,
"Restarting": False,
"OOMKilled": False,
"Pid": 4213,
"ExitCode": 0,
"StartedAt": "2026-03-21T10:00:00.000000000Z",
"FinishedAt": "0001-01-01T00:00:00Z"
},
"HostConfig": {
"PortBindings": {
"80/tcp": [{"HostIp": "0.0.0.0", "HostPort": "8080"}]
},
"Memory": 536870912, # 512 MB en octets
"NanoCpus": 500000000, # 0.5 CPU
"RestartPolicy": {"Name": "unless-stopped", "MaximumRetryCount": 0},
"Binds": ["/data/nginx:/usr/share/nginx/html:ro"],
},
"NetworkSettings": {
"IPAddress": "172.17.0.2",
"Ports": {
"80/tcp": [{"HostIp": "0.0.0.0", "HostPort": "8080"}]
}
},
"Mounts": [
{
"Type": "bind",
"Source": "/data/nginx",
"Destination": "/usr/share/nginx/html",
"Mode": "ro",
"RW": False
}
],
"Config": {
"Image": "nginx:1.25",
"Env": ["NGINX_VERSION=1.25.3", "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],
"Cmd": ["nginx", "-g", "daemon off;"],
}
}
def analyser_inspect(data: dict) -> None:
"""Extrait les informations clés d'un docker inspect."""
state = data.get("State", {})
host = data.get("HostConfig", {})
net = data.get("NetworkSettings", {})
cfg = data.get("Config", {})
print("=" * 55)
print(f"Conteneur : {data.get('Name', '?')}")
print(f"Image : {cfg.get('Image', '?')}")
print("=" * 55)
# État
statut = "🟢 En cours" if state.get("Running") else "🔴 Arrêté"
print(f"\n État : {statut} (PID {state.get('Pid', 0)})")
print(f" OOM Kill : {state.get('OOMKilled', False)}")
print(f" Démarré : {state.get('StartedAt', 'N/A')[:19]}")
# Ressources
mem_mb = host.get("Memory", 0) / 1024 / 1024
cpu = host.get("NanoCpus", 0) / 1e9
print(f"\n Mémoire limite : {mem_mb:.0f} MB")
print(f" CPU limite : {cpu:.1f}")
print(f" Restart policy : {host.get('RestartPolicy', {}).get('Name', 'no')}")
# Réseau
print(f"\n IP interne : {net.get('IPAddress', 'N/A')}")
for port_ct, bindings in net.get("Ports", {}).items():
if bindings:
for b in bindings:
print(f" Port : {b['HostIp']}:{b['HostPort']} → {port_ct}")
# Volumes
print(f"\n Montages :")
for mount in data.get("Mounts", []):
rw = "R/W" if mount.get("RW") else "lecture seule"
print(f" {mount['Type']} {mount['Source']} → {mount['Destination']} ({rw})")
# Commande
print(f"\n Commande : {' '.join(cfg.get('Cmd', []))}")
analyser_inspect(docker_inspect_simule)
=======================================================
Conteneur : /mon-nginx
Image : nginx:1.25
=======================================================
État : 🟢 En cours (PID 4213)
OOM Kill : False
Démarré : 2026-03-21T10:00:00
Mémoire limite : 512 MB
CPU limite : 0.5
Restart policy : unless-stopped
IP interne : 172.17.0.2
Port : 0.0.0.0:8080 → 80/tcp
Montages :
bind /data/nginx → /usr/share/nginx/html (lecture seule)
Commande : nginx -g daemon off;
Commandes de débogage et d’inspection#
Le débogage d’un conteneur défaillant est une compétence essentielle. Voici les outils du quotidien.
# Voir les statistiques en temps réel (CPU, RAM, réseau, I/O)
docker stats mon-conteneur
# Voir les processus dans un conteneur
docker top mon-conteneur
# Copier un fichier entre l'hôte et le conteneur
docker cp mon-conteneur:/app/logs/app.log ./logs_local/
docker cp ./config.yml mon-conteneur:/app/config.yml
# Voir les changements de fichiers depuis la création du conteneur
docker diff mon-conteneur
# A = Added, C = Changed, D = Deleted
# Exécuter un shell de débogage même si l'image n'en a pas
# (avec nsenter sur Linux — sans Docker)
docker exec -it mon-conteneur sh # sh si bash absent (Alpine)
# Inspecter les events Docker en temps réel
docker events
# Simulation de docker stats : analyse de l'utilisation des ressources
import json
from datetime import datetime, timezone
# Données simulées de docker stats (format JSON renvoyé par l'API Docker)
stats_simules = [
{
"name": "web-api",
"cpu_percent": 12.4,
"mem_usage_mb": 128.5,
"mem_limit_mb": 512.0,
"net_rx_mb": 45.2,
"net_tx_mb": 23.1,
"block_read_mb": 12.0,
"block_write_mb": 8.5,
"pids": 8
},
{
"name": "database",
"cpu_percent": 28.7,
"mem_usage_mb": 387.2,
"mem_limit_mb": 1024.0,
"net_rx_mb": 12.0,
"net_tx_mb": 45.8,
"block_read_mb": 120.5,
"block_write_mb": 95.2,
"pids": 12
},
{
"name": "cache-redis",
"cpu_percent": 2.1,
"mem_usage_mb": 64.3,
"mem_limit_mb": 256.0,
"net_rx_mb": 102.4,
"net_tx_mb": 98.7,
"block_read_mb": 0.5,
"block_write_mb": 2.1,
"pids": 5
},
{
"name": "worker",
"cpu_percent": 87.3,
"mem_usage_mb": 310.0,
"mem_limit_mb": 512.0,
"net_rx_mb": 5.2,
"net_tx_mb": 3.1,
"block_read_mb": 45.0,
"block_write_mb": 22.0,
"pids": 4
},
]
def format_stats(conteneurs: list[dict]) -> None:
"""Affiche un tableau formaté de statistiques de conteneurs."""
print(f"{'NOM':<14} {'CPU %':>7} {'MEM USAGE/LIMIT':>22} {'MEM %':>7} "
f"{'NET I/O':>15} {'BLOCK I/O':>15} {'PIDs':>5}")
print("-" * 92)
for c in conteneurs:
mem_pct = c["mem_usage_mb"] / c["mem_limit_mb"] * 100
net_io = f"{c['net_rx_mb']:.1f}/{c['net_tx_mb']:.1f} MB"
block_io = f"{c['block_read_mb']:.1f}/{c['block_write_mb']:.1f} MB"
# Alerte si CPU ou mémoire élevés
cpu_flag = " ⚠" if c["cpu_percent"] > 80 else " "
mem_flag = " ⚠" if mem_pct > 80 else " "
mem_str = f"{c['mem_usage_mb']:.0f} MB / {c['mem_limit_mb']:.0f} MB"
print(f"{c['name']:<14} {c['cpu_percent']:>6.1f}%{cpu_flag} "
f"{mem_str:>22} {mem_pct:>6.1f}%{mem_flag} "
f"{net_io:>15} {block_io:>15} {c['pids']:>5}")
print("Sortie simulée de : docker stats --no-stream")
print()
format_stats(stats_simules)
print()
# Identifier les conteneurs en surcharge
surcharge = [c for c in stats_simules
if c["cpu_percent"] > 80 or
c["mem_usage_mb"] / c["mem_limit_mb"] > 0.8]
if surcharge:
print("Alertes détectées :")
for c in surcharge:
mem_pct = c["mem_usage_mb"] / c["mem_limit_mb"] * 100
if c["cpu_percent"] > 80:
print(f" ⚠ {c['name']} : CPU à {c['cpu_percent']:.1f}% (> 80%)")
if mem_pct > 80:
print(f" ⚠ {c['name']} : Mémoire à {mem_pct:.1f}% "
f"({c['mem_usage_mb']:.0f}/{c['mem_limit_mb']:.0f} MB)")
Sortie simulée de : docker stats --no-stream
NOM CPU % MEM USAGE/LIMIT MEM % NET I/O BLOCK I/O PIDs
--------------------------------------------------------------------------------------------
web-api 12.4% 128 MB / 512 MB 25.1% 45.2/23.1 MB 12.0/8.5 MB 8
database 28.7% 387 MB / 1024 MB 37.8% 12.0/45.8 MB 120.5/95.2 MB 12
cache-redis 2.1% 64 MB / 256 MB 25.1% 102.4/98.7 MB 0.5/2.1 MB 5
worker 87.3% ⚠ 310 MB / 512 MB 60.5% 5.2/3.1 MB 45.0/22.0 MB 4
Alertes détectées :
⚠ worker : CPU à 87.3% (> 80%)
Résumé#
Un conteneur traverse des états bien définis : created → running → paused → stopped → removed. La couche R/W est préservée jusqu’au
docker rm.docker run -d -p -v -e --name --restartsont les options du quotidien à maîtriser.Il existe trois types de stockage : bind mounts (répertoire hôte), volumes nommés (gérés par Docker), tmpfs (mémoire volatile). Choisissez selon la durabilité requise et le contexte.
Les variables d’environnement configurent l’application ; ne jamais mettre de secrets dans le Dockerfile ou en clair dans des scripts.
La publication de ports crée des règles iptables NAT sur l’hôte.
EXPOSEest de la documentation,-pest la vraie publication.Un conteneur bien conçu tourne un seul processus (PID 1) dont la terminaison déclenche l’arrêt du conteneur.
docker inspect,docker stats,docker logsetdocker execsont vos outils de débogage principaux.
Le prochain chapitre explore en profondeur le réseau Docker : comment les conteneurs communiquent entre eux, avec l’extérieur, et comment orchestrer le réseau de plusieurs services.