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.

Hide code cell source

import matplotlib
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from matplotlib.patches import FancyBboxPatch, FancyArrowPatch
import numpy as np
import pandas as pd
import seaborn as sns
import json
import os

sns.set_theme(style="whitegrid", palette="muted")
plt.rcParams.update({
    "figure.dpi": 120,
    "font.family": "DejaVu Sans",
    "axes.titlesize": 13,
    "axes.labelsize": 11,
})

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).

Hide code cell source

fig, ax = plt.subplots(figsize=(14, 7))
ax.set_xlim(0, 14)
ax.set_ylim(0, 8)
ax.axis("off")
ax.set_title("Cycle de vie d'un conteneur Docker", fontsize=14, fontweight="bold", pad=12)

etats = [
    (1.5, 5.5, "IMAGE", "#37474f", "white",
     "Modèle immuable\nsur le disque"),
    (4.5, 5.5, "CREATED", "#1565c0", "white",
     "Couche R/W créée\nprocessus non démarré"),
    (7.5, 5.5, "RUNNING", "#2e7d32", "white",
     "PID 1 actif\ntâche en cours"),
    (10.5, 5.5, "PAUSED", "#f57f17", "white",
     "SIGSTOP envoyé\ncgroups figés"),
    (7.5, 2.5, "STOPPED", "#c62828", "white",
     "PID 1 terminé\ncouche R/W préservée"),
    (10.5, 2.5, "DEAD", "#4a148c", "white",
     "Erreur fatale\nimpossible de redémarrer"),
    (4.5, 2.5, "REMOVED", "#546e7a", "white",
     "Couche R/W supprimée\n(irréversible)"),
]

for x, y, label, color, tc, desc in etats:
    box = FancyBboxPatch((x - 1.2, y - 0.7), 2.4, 1.4,
                         boxstyle="round,pad=0.12",
                         facecolor=color, edgecolor="white", linewidth=2, alpha=0.9)
    ax.add_patch(box)
    ax.text(x, y + 0.2, label, ha="center", va="center",
            color=tc, fontsize=10, fontweight="bold")
    ax.text(x, y - 0.35, desc, ha="center", va="center",
            color=tc, fontsize=7.5, linespacing=1.4)

# Transitions
transitions = [
    # (de_xy, vers_xy, label, courbure, couleur)
    ((2.7, 5.5), (3.3, 5.5), "docker create", 0, "#546e7a"),
    ((5.7, 5.5), (6.3, 5.5), "docker start", 0, "#2e7d32"),
    ((8.7, 5.5), (9.3, 5.5), "docker pause", 0, "#f57f17"),
    ((9.3, 5.5), (8.7, 5.5), "docker unpause", 0, "#43a047"),
    ((7.5, 4.8), (7.5, 3.2), "docker stop\n/kill ou exit", 0, "#c62828"),
    ((9.3, 5.5), (8.7, 3.2), "docker kill", 0, "#b71c1c"),
    ((6.3, 2.5), (5.7, 2.5), "docker rm", 0, "#546e7a"),
    ((6.3, 5.5), (6.3, 2.5), "docker run\n(exit immédiat)", 0, "#c62828"),
    ((7.5, 2.5), (5.7, 2.5), "docker rm", 0, "#546e7a"),
    ((7.5, 3.2), (7.5, 4.8), "docker start\n(restart)", 0, "#1565c0"),
]

for (x1, y1), (x2, y2), label, curv, color in transitions:
    ax.annotate("", xy=(x2, y2), xytext=(x1, y1),
                arrowprops=dict(arrowstyle="->", color=color,
                                lw=1.8, connectionstyle=f"arc3,rad={curv}"))
    mx, my = (x1 + x2) / 2, (y1 + y2) / 2
    ax.text(mx, my + 0.18, label, ha="center", va="bottom",
            fontsize=7, color=color, fontweight="bold",
            bbox=dict(boxstyle="round,pad=0.1", facecolor="white", alpha=0.8, edgecolor="none"))

# Note spéciale docker run
ax.text(7.0, 1.2,
        "docker run = docker create + docker start (en une seule commande)",
        ha="center", va="center", fontsize=9,
        bbox=dict(boxstyle="round,pad=0.4", facecolor="#e3f2fd", edgecolor="#1565c0"))

plt.tight_layout()
plt.savefig("_static/03_cycle_vie.png", dpi=130, bbox_inches="tight")
plt.show()
_images/ab7998c6e9d7b75d960e1584d0c3920f129f9ee362e23a1ed671799916b9b931.png

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#

Hide code cell source

fig, ax = plt.subplots(figsize=(14, 9))
ax.set_xlim(0, 14)
ax.set_ylim(0, 10)
ax.axis("off")
ax.set_title("Options docker run — référence visuelle", fontsize=14, fontweight="bold", pad=12)

options = [
    # (flag, valeur exemple, description, categorie, couleur)
    ("-d",         "--detach",              "Détaché : le conteneur tourne en arrière-plan",
     "Mode", "#37474f"),
    ("-it",        "--interactive --tty",   "Interactif avec pseudo-terminal (shell, debug)",
     "Mode", "#37474f"),
    ("--rm",       "",                      "Suppression automatique après arrêt",
     "Mode", "#37474f"),
    ("-p",         "8080:80",               "Mapping port : -p <port_hôte>:<port_conteneur>",
     "Réseau", "#1565c0"),
    ("-p",         "127.0.0.1:8080:80",     "Mapping local uniquement (pas exposé sur l'internet)",
     "Réseau", "#1565c0"),
    ("-P",         "(majuscule)",           "Publie tous les ports EXPOSE sur ports aléatoires",
     "Réseau", "#1565c0"),
    ("--network",  "mon-reseau",            "Connecte au réseau bridge nommé",
     "Réseau", "#1565c0"),
    ("-v",         "/hôte:/conteneur",      "Bind mount : monte un répertoire hôte",
     "Volumes", "#2e7d32"),
    ("-v",         "monvolume:/app/data",   "Volume nommé Docker (géré par Docker)",
     "Volumes", "#2e7d32"),
    ("--tmpfs",    "/tmp",                  "Système de fichiers temporaire en RAM",
     "Volumes", "#2e7d32"),
    ("-e",         "CLE=valeur",            "Variable d'environnement",
     "Config", "#6a1b9a"),
    ("--env-file", ".env",                  "Fichier de variables d'environnement",
     "Config", "#6a1b9a"),
    ("--name",     "mon-conteneur",         "Nom du conteneur (auto-généré sinon)",
     "Config", "#6a1b9a"),
    ("--restart",  "unless-stopped",        "Politique de redémarrage (no/always/on-failure)",
     "Config", "#6a1b9a"),
    ("--memory",   "512m",                  "Limite mémoire (cgroups)",
     "Ressources", "#c62828"),
    ("--cpus",     "0.5",                   "Limite CPU (0.5 = un demi-cœur)",
     "Ressources", "#c62828"),
    ("--user",     "1000:1000",             "UID:GID du processus (sécurité)",
     "Sécurité", "#e65100"),
    ("--read-only","",                      "Système de fichiers en lecture seule",
     "Sécurité", "#e65100"),
]

cols = 2
col_w = 6.8
row_h = 0.48
margin_x = 0.15

for idx, (flag, valeur, desc, cat, color) in enumerate(options):
    row = idx // cols
    col = idx % cols
    x = margin_x + col * col_w
    y = 9.7 - row * row_h

    bg = FancyBboxPatch((x, y - row_h + 0.04), col_w - 0.15, row_h - 0.07,
                        boxstyle="round,pad=0.05", facecolor=color,
                        edgecolor="white", linewidth=1, alpha=0.82)
    ax.add_patch(bg)

    ax.text(x + 0.1, y - row_h * 0.5,
            f"{flag}", ha="left", va="center",
            color="#ffe082", fontsize=9, fontweight="bold", fontfamily="monospace")
    ax.text(x + 1.0, y - row_h * 0.5,
            valeur if valeur else "", ha="left", va="center",
            color="#b2dfdb", fontsize=8, fontfamily="monospace")
    ax.text(x + 2.8, y - row_h * 0.5,
            desc, ha="left", va="center",
            color="white", fontsize=8)

plt.tight_layout()
plt.savefig("_static/03_docker_run_options.png", dpi=130, bbox_inches="tight")
plt.show()
_images/54163a47f268c8c4c1a43db75f6dece6f41ab2dd3f33462d2762153dcb6c9fbd.png

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.

Hide code cell source

fig, axes = plt.subplots(1, 3, figsize=(15, 7))

titres = ["Bind Mount", "Volume Nommé", "tmpfs"]
couleurs = ["#1565c0", "#2e7d32", "#6a1b9a"]
descriptions = [
    ["Répertoire hôte\nmontée directement",
     "Chemin absolu hôte requis",
     "Dépend de la structure\ndes fichiers hôte",
     "Idéal : développement local,\npartage de code source"],
    ["Volume géré\npar Docker",
     "Stocké dans\n/var/lib/docker/volumes/",
     "Portable entre hôtes,\nbackup facile",
     "Idéal : données de production,\nbases de données"],
    ["Mémoire RAM\n(non persisté)",
     "Perdu à l'arrêt\ndu conteneur",
     "Très rapide, isolé,\nsécurisé",
     "Idéal : fichiers temporaires,\ncaches, /tmp"],
]
exemples = [
    "-v /home/user/data:/app/data",
    "-v monvolume:/app/data",
    "--tmpfs /tmp:size=100m",
]
cas_usage = [
    [("✓ Développement", True),
     ("✓ Config files", True),
     ("✗ Production DB", False),
     ("✗ Multi-hôtes", False)],
    [("✓ Production DB", True),
     ("✓ Backups", True),
     ("✓ Multi-hôtes (avec driver)", True),
     ("✗ Édition directe hôte", False)],
    [("✓ Cache temporaire", True),
     ("✓ Secrets en RAM", True),
     ("✗ Persistance", False),
     ("✗ Partage entre conteneurs", False)],
]

for i, ax in enumerate(axes):
    ax.set_xlim(0, 5)
    ax.set_ylim(0, 9)
    ax.axis("off")
    ax.set_title(titres[i], fontsize=12, fontweight="bold", color=couleurs[i])

    # Boîte principale
    main_box = FancyBboxPatch((0.2, 5.0), 4.6, 3.6, boxstyle="round,pad=0.12",
                              facecolor=couleurs[i], edgecolor="white",
                              linewidth=2, alpha=0.9)
    ax.add_patch(main_box)

    for j, ligne in enumerate(descriptions[i]):
        ax.text(2.5, 8.2 - j * 0.85, ligne, ha="center", va="center",
                color="white", fontsize=8.5, linespacing=1.3)

    # Commande exemple
    cmd_box = FancyBboxPatch((0.2, 4.2), 4.6, 0.65, boxstyle="round,pad=0.08",
                             facecolor="#212121", edgecolor=couleurs[i],
                             linewidth=2)
    ax.add_patch(cmd_box)
    ax.text(2.5, 4.52, exemples[i], ha="center", va="center",
            color="#ffe082", fontsize=7.5, fontfamily="monospace")

    # Cas d'usage
    ax.text(2.5, 3.8, "Cas d'usage :", ha="center", va="center",
            fontsize=9, fontweight="bold", color=couleurs[i])
    for j, (label, ok) in enumerate(cas_usage[i]):
        color_badge = "#a5d6a7" if ok else "#ef9a9a"
        badge = FancyBboxPatch((0.3, 3.35 - j * 0.7), 4.4, 0.55,
                               boxstyle="round,pad=0.06",
                               facecolor=color_badge, edgecolor="white", linewidth=1)
        ax.add_patch(badge)
        ax.text(2.5, 3.62 - j * 0.7, label, ha="center", va="center",
                fontsize=8, fontweight="bold", color="#212121")

fig.suptitle("Les trois types de stockage Docker", fontsize=14, fontweight="bold")
plt.tight_layout()
plt.savefig("_static/03_volumes.png", dpi=130, bbox_inches="tight")
plt.show()
_images/0c8d53890e1eebc372f96ae1606605a531ed306a4e945141ab5ea452c8bfdda7.png

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.

Hide code cell source

fig, ax = plt.subplots(figsize=(13, 7))
ax.set_xlim(0, 13)
ax.set_ylim(0, 8)
ax.axis("off")
ax.set_title("Mapping de ports Docker : du navigateur au conteneur", fontsize=13, fontweight="bold", pad=12)

# Machine hôte
hote_box = FancyBboxPatch((0.3, 1.0), 12.4, 6.5, boxstyle="round,pad=0.2",
                          facecolor="#e8eaf6", edgecolor="#3f51b5",
                          linewidth=2, alpha=0.4)
ax.add_patch(hote_box)
ax.text(6.5, 7.3, "Machine hôte (192.168.1.10)", ha="center", va="center",
        fontsize=11, fontweight="bold", color="#3f51b5")

# Interface réseau hôte
eth_box = FancyBboxPatch((0.5, 5.2), 5.5, 1.5, boxstyle="round,pad=0.1",
                         facecolor="#3f51b5", edgecolor="white", linewidth=2, alpha=0.9)
ax.add_patch(eth_box)
ax.text(3.25, 5.95, "Interface réseau hôte (eth0)\n192.168.1.10", ha="center", va="center",
        color="white", fontsize=9, fontweight="bold")

# iptables NAT
nat_box = FancyBboxPatch((3.5, 3.5), 3.0, 1.4, boxstyle="round,pad=0.1",
                         facecolor="#f57f17", edgecolor="white", linewidth=2, alpha=0.9)
ax.add_patch(nat_box)
ax.text(5.0, 4.2, "iptables DNAT\n:8080 → 172.17.0.2:80", ha="center", va="center",
        color="white", fontsize=8.5, fontweight="bold")

# Conteneur
ct_box = FancyBboxPatch((6.5, 1.5), 5.5, 4.0, boxstyle="round,pad=0.15",
                        facecolor="#2e7d32", edgecolor="white", linewidth=2, alpha=0.85)
ax.add_patch(ct_box)
ax.text(9.25, 5.2, "Conteneur nginx\n172.17.0.2", ha="center", va="center",
        color="white", fontsize=9, fontweight="bold")

# Interface conteneur
eth_ct = FancyBboxPatch((6.8, 3.8), 5.0, 1.0, boxstyle="round,pad=0.08",
                        facecolor="#1b5e20", edgecolor="white", linewidth=1.5)
ax.add_patch(eth_ct)
ax.text(9.3, 4.3, "Interface eth0 du conteneur\n172.17.0.2", ha="center", va="center",
        color="white", fontsize=8.5)

# nginx
nginx_box = FancyBboxPatch((7.2, 1.8), 4.2, 1.5, boxstyle="round,pad=0.08",
                           facecolor="#004d40", edgecolor="white", linewidth=1.5)
ax.add_patch(nginx_box)
ax.text(9.3, 2.55, "nginx\nécoute sur :80", ha="center", va="center",
        color="white", fontsize=9, fontweight="bold")

# docker0 bridge
bridge_box = FancyBboxPatch((3.5, 2.2), 2.8, 0.9, boxstyle="round,pad=0.08",
                            facecolor="#546e7a", edgecolor="white", linewidth=1.5)
ax.add_patch(bridge_box)
ax.text(4.9, 2.65, "docker0 bridge\n172.17.0.1", ha="center", va="center",
        color="white", fontsize=8)

# Flèches
arrows = [
    ((3.25, 5.2), (4.5, 4.9), "port 8080"),
    ((5.0, 3.5), (4.9, 3.1), "172.17.0.2:80"),
    ((5.5, 2.65), (6.8, 4.0), "veth pair"),
    ((9.3, 3.8), (9.3, 3.3), "port 80"),
]
for (x1, y1), (x2, y2), label in arrows:
    ax.annotate("", xy=(x2, y2), xytext=(x1, y1),
                arrowprops=dict(arrowstyle="->", color="#e53935", lw=2.0))
    mx, my = (x1 + x2) / 2, (y1 + y2) / 2
    ax.text(mx + 0.15, my + 0.1, label, ha="left", va="center",
            fontsize=8, color="#e53935", fontweight="bold")

# Client extérieur
ax.text(0.9, 0.55, "Client : curl http://192.168.1.10:8080",
        ha="left", va="center", fontsize=9, color="#1565c0", fontweight="bold",
        bbox=dict(boxstyle="round,pad=0.3", facecolor="#e3f2fd", edgecolor="#1565c0"))

plt.tight_layout()
plt.savefig("_static/03_port_mapping.png", dpi=130, bbox_inches="tight")
plt.show()
_images/86263035ef6d133856079950b1964435231de1b437f23624d0d3d1b95fcea77c.png
# 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 80 dans le Dockerfile est de la documentation — il indique qu’un service écoute sur ce port, mais ne l’ouvre pas.

  • -p 8080:80 dans docker run publie 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 --restart est 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;

Hide code cell source

# Visualisation des politiques de redémarrage
fig, ax = plt.subplots(figsize=(12, 5))
ax.set_xlim(0, 12)
ax.set_ylim(0, 6)
ax.axis("off")
ax.set_title("Politiques de redémarrage (--restart)", fontsize=13, fontweight="bold", pad=12)

politiques = [
    ("no\n(défaut)", "#546e7a",
     "Pas de redémarrage\nauto. Le conteneur\ns'arrête et reste\narrêté.",
     "Tests, commandes\none-shot"),
    ("always", "#1565c0",
     "Redémarre toujours,\nmême si arrêt manuel.\nDémarre aussi au\nboot Docker.",
     "Services critiques\nsans distinction\nd'erreur"),
    ("on-failure", "#2e7d32",
     "Redémarre si le code\nde sortie est non-nul.\nOption :max-retries.",
     "Tâches qui peuvent\néchouer et doivent\nretenter"),
    ("unless-stopped", "#6a1b9a",
     "Comme 'always' mais\nNE redémarre pas si\narrêt explicite\n(docker stop).",
     "Services de production\nrecommandé"),
]

for i, (nom, color, desc, cas) in enumerate(politiques):
    x = 0.3 + i * 2.9
    box = FancyBboxPatch((x, 2.0), 2.5, 3.5, boxstyle="round,pad=0.12",
                         facecolor=color, edgecolor="white", linewidth=2, alpha=0.9)
    ax.add_patch(box)
    ax.text(x + 1.25, 5.1, nom, ha="center", va="center",
            color="white", fontsize=10, fontweight="bold")
    ax.text(x + 1.25, 3.8, desc, ha="center", va="center",
            color="white", fontsize=8, linespacing=1.4)

    # Cas d'usage
    cas_box = FancyBboxPatch((x, 0.5), 2.5, 1.3, boxstyle="round,pad=0.08",
                             facecolor="#e8eaf6", edgecolor=color, linewidth=2)
    ax.add_patch(cas_box)
    ax.text(x + 1.25, 1.15, cas, ha="center", va="center",
            fontsize=7.5, color="#212121", linespacing=1.4)

ax.text(6.0, 0.15, "Cas d'usage", ha="center", va="center",
        fontsize=8.5, color="#546e7a", style="italic")

plt.tight_layout()
plt.savefig("_static/03_restart_policies.png", dpi=130, bbox_inches="tight")
plt.show()
_images/49b3e68c3e7f85818a6669bab69b0204107b5492ee2095cc17e683172be931de.png

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 --restart sont 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. EXPOSE est de la documentation, -p est 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 logs et docker exec sont 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.