Programmation — intermédiaire

Docker — Fiche de révision complète : commandes, Dockerfile, volumes, réseaux et Compose

Tout Docker en une fiche : conteneurs et images, le cycle de vie, docker run option par option, le Dockerfile instruction par instruction, CMD contre…

Par ProfBot

Docker — la fiche de révision complète

Conteneurs, images, Dockerfile, volumes, réseaux et Compose · Niveau intermédiaire

« Ça marche sur ma machine. » Cette phrase a coûté plus d'heures de nuit que n'importe quel bug. Docker la rend caduque : il enferme une application et tout ce dont elle a besoin pour tourner — bibliothèques, binaires, variables, version exacte de l'interpréteur — dans un objet unique, reproductible, qui démarre à l'identique sur un portable, sur un serveur et dans une chaîne d'intégration continue.

🎯 Objectif de la fiche. Savoir lire et écrire un Dockerfile, connaître par cœur les commandes utiles, comprendre ce qui se passe réellement sous le capot (couches, volumes, réseaux), et repérer les douze pièges qui font perdre le plus de temps.


1. Pourquoi Docker : le problème qu'il résout

Avant Docker, on livrait un fichier .jar, un dossier dist/, un script — et on croisait les doigts pour que la machine cible ait la bonne version de Java, de Node ou de libssl. Trois réponses ont existé :

SolutionCe qu'on livrePoidsDémarrageIsolation
Livrer le code seulquelques Moinstantanéaucuneaucune
Machine virtuelleun OS complet + noyauplusieurs Go30 s à 2 minforte (noyau séparé)
Conteneur Dockerl'appli + ses dépendances10 à 300 Moquelques dizaines de msbonne (noyau partagé)

La différence décisive tient en une phrase : une VM virtualise le matériel, un conteneur ne virtualise que le système de fichiers et la vue des processus. Le conteneur utilise le noyau Linux de la machine hôte. D'où sa légèreté — et sa seule vraie limite : un conteneur Linux a besoin d'un noyau Linux. Sur Windows et macOS, Docker Desktop fait tourner discrètement une petite VM Linux pour lui en fournir un.

Docker n'est pas un émulateur. Un processus dans un conteneur est un processus ordinaire de l'hôte. Lancez ps aux sur l'hôte : vous le verrez, avec son PID réel. Ce qui change, c'est ce que lui voit du monde.


2. Le vocabulaire en cinq mots

Tout le reste de la fiche repose sur ces cinq objets. Les confondre est la première source d'erreurs.

Image — un modèle en lecture seule : un système de fichiers figé, plus des métadonnées (commande à lancer, variables, port déclaré). Une image ne « tourne » pas. C'est un gâteau au congélateur.

Conteneur — une instance en cours d'exécution d'une image, avec par-dessus une fine couche inscriptible qui lui est propre. Une image peut donner naissance à mille conteneurs. C'est la part de gâteau dans l'assiette.

Couche (layer) — chaque instruction d'un Dockerfile produit une couche empilée sur la précédente. Les couches sont partagées entre images : dix images basées sur alpine ne stockent qu'une seule fois alpine.

Registre (registry) — le serveur qui héberge les images. Docker Hub par défaut ; il en existe des privés (GitLab, GitHub Container Registry, Harbor, ECR…).

Volume — une zone de stockage gérée par Docker, extérieure au conteneur, qui survit à sa destruction. Sans volume, toute donnée écrite disparaît avec le conteneur.

L'analogie qui tient bon : l'image est à la classe ce que le conteneur est à l'objet. docker run, c'est le new.


3. Ce qui se passe vraiment sous le capot

Trois mécanismes du noyau Linux, aucun inventé par Docker :

Côté outillage, la commande docker n'est qu'un client. Elle parle à un démon (dockerd) via une socket, et c'est lui qui fait le travail, en s'appuyant sur containerd puis runc.


docker (CLI)  →  dockerd  →  containerd  →  runc  →  processus isolé
                   ↑
          /var/run/docker.sock

Conséquence de sécurité, à connaître par cœur. Qui peut écrire dans /var/run/docker.sock peut démarrer un conteneur privilégié qui monte / de l'hôte : appartenir au groupe docker équivaut à être root. Ce n'est pas une faille, c'est le modèle. Ne montez jamais la socket dans un conteneur sans savoir précisément pourquoi.


4. Installer et vérifier


# Installation officielle (Debian/Ubuntu), script pratique
curl -fsSL https://get.docker.com | sh

# Utiliser docker sans sudo (voir l'avertissement ci-dessus)
sudo usermod -aG docker $USER   # puis se déconnecter/reconnecter

docker version    # client ET démon : si le démon manque, il ne tourne pas
docker info       # état global : nb de conteneurs, pilote de stockage, espace
docker run hello-world

docker version échoue avec « Cannot connect to the Docker daemon » ? Le service n'est pas démarré : sudo systemctl start docker.


5. Le cycle de vie d'un conteneur


image    ── docker create ──►   created
created  ── docker start ──►    running
running  ── docker pause ──►    paused
paused   ── docker unpause ──►  running
running  ── docker stop ──►     exited     (SIGTERM, puis SIGKILL après 10 s)
running  ── docker kill ──►     exited     (SIGKILL immédiat)
exited   ── docker start ──►    running    (il repart, il n'est pas perdu)
exited   ── docker rm ──►       le conteneur n'existe plus

docker run = docker create + docker start. Retenir aussi : un conteneur arrêté existe toujours — il occupe du disque et bloque son nom. Il faut le supprimer (docker rm) ou l'avoir lancé avec --rm.

Un conteneur vit exactement aussi longtemps que son processus PID 1. Si ce processus se termine, le conteneur s'arrête, quoi qu'il arrive par ailleurs. C'est la raison pour laquelle docker run ubuntu s'arrête instantanément : le bash qu'il lance n'a pas d'entrée à lire et rend la main.


6. docker run : l'anatomie complète

C'est la commande la plus riche de Docker. Sa forme générale :


docker run [OPTIONS] IMAGE [COMMANDE] [ARGUMENTS]

Tout ce qui suit le nom de l'image n'est plus pour Docker, mais pour le conteneur. Placer -d après le nom de l'image est l'erreur de débutant classique : il devient un argument passé à l'application.

OptionRôleExemple
-d, --detachlance en arrière-plan, affiche l'IDdocker run -d nginx
-it-i garde l'entrée standard ouverte, -t alloue un pseudo-terminaldocker run -it alpine sh
--rmsupprime le conteneur dès qu'il s'arrêtedocker run --rm alpine date
--namenom lisible, unique sur la machine--name api
-p H:Cpublie le port C du conteneur sur le port H de l'hôte-p 8080:80
-Ppublie tous les ports EXPOSE sur des ports aléatoires
-e, --envdéfinit une variable d'environnement-e NODE_ENV=production
--env-filelit les variables dans un fichier--env-file .env
-v, --volumemonte un volume ou un dossier de l'hôte-v data:/var/lib/postgresql/data
--mountmême chose, syntaxe explicite (recommandée)voir §13
-w, --workdirrépertoire de travail dans le conteneur-w /app
-u, --userUID:GID d'exécution-u 1000:1000
--networkréseau à rejoindre--network back
--restartpolitique de redémarrage--restart unless-stopped
--memory, --cpusplafonds de ressources--memory 512m --cpus 1.5
--entrypointremplace l'ENTRYPOINT de l'image--entrypoint sh
--read-onlysystème de fichiers en lecture seule
--cap-drop, --cap-addretire ou ajoute une capacité noyau--cap-drop ALL
--health-cmdtest de santé à la volée
--pull alwaysforce à retirer l'image même si elle est en cache

Trois exemples qui résument presque tout :


# Un serveur web jetable, port 8080 de l'hôte vers le 80 du conteneur
docker run -d --name web -p 8080:80 --restart unless-stopped nginx:1.27-alpine

# Un shell éphémère pour explorer une image
docker run --rm -it alpine:3.20 sh

# Une base de données persistante, mot de passe par variable, volume nommé
docker run -d --name db \
  -e POSTGRES_PASSWORD=secret \
  -e POSTGRES_DB=app \
  -v pgdata:/var/lib/postgresql/data \
  -p 127.0.0.1:5432:5432 \
  postgres:16-alpine

-p 8080:80 se lit de gauche à droite : HÔTE d'abord, CONTENEUR ensuite. Inverser les deux est le bug le plus fréquent de la vie d'un développeur Docker. Et -p 127.0.0.1:5432:5432 publie uniquement sur la boucle locale : sans le préfixe d'adresse, le port est ouvert au monde entier, et Docker écrit ses règles directement dans iptables, en contournant UFW. Une base « protégée par le pare-feu » peut ainsi être publique sans que rien ne le signale.


7. Piloter les conteneurs au quotidien


docker ps                 # conteneurs en cours
docker ps -a              # tous, y compris arrêtés
docker ps -q              # seulement les IDs (pour composer des commandes)
docker logs api           # sortie standard du conteneur
docker logs -f --tail 100 api        # suivre les 100 dernières lignes
docker logs --since 10m api          # depuis 10 minutes
docker exec -it api sh               # un shell DANS un conteneur qui tourne
docker exec -u root -it api sh       # y entrer en root
docker stop api                      # SIGTERM, puis SIGKILL après 10 s
docker stop -t 30 api                # laisser 30 s pour s'arrêter proprement
docker kill api                      # SIGKILL tout de suite
docker restart api
docker rm api                        # supprimer (arrêté)
docker rm -f api                     # forcer (arrête puis supprime)
docker inspect api                   # TOUTE la configuration, en JSON
docker stats                         # CPU/mémoire/réseau en direct
docker top api                       # processus du conteneur
docker port api                      # correspondances de ports
docker cp api:/app/log.txt .         # copier depuis le conteneur
docker cp ./conf.json api:/app/      # copier vers le conteneur
docker diff api                      # fichiers modifiés depuis le démarrage
docker rename api api-old
docker update --memory 1g api        # changer des limites à chaud
docker wait api                      # bloque et renvoie le code de sortie

docker inspect est verbeux ; on le filtre avec un gabarit Go :


docker inspect -f '{{.State.Status}}' api
docker inspect -f '{{.NetworkSettings.IPAddress}}' api
docker inspect -f '{{json .Config.Env}}' api
docker inspect -f '{{.State.ExitCode}}' api

exec n'est pas run. docker exec entre dans un conteneur déjà en marche ; docker run en crée un nouveau. Si docker exec répond « is not running », le conteneur est mort : c'est docker logs qu'il faut lire, pas exec qu'il faut réessayer.

Une nuance qui sauve des sessions : sortir d'un docker run -it avec exit arrête le conteneur ; la séquence Ctrl-P Ctrl-Q le quitte en le laissant tourner.


8. Les images : chercher, tirer, construire, pousser


docker search postgres              # chercher sur Docker Hub
docker pull nginx:1.27-alpine       # tirer une version PRÉCISE
docker images                       # lister les images locales
docker image ls --filter dangling=true   # images orphelines (sans tag)
docker history mon-app:1.0          # les couches et leur poids
docker tag mon-app:1.0 user/mon-app:1.0  # donner un second nom
docker login ghcr.io
docker push user/mon-app:1.0
docker rmi mon-app:1.0              # supprimer une image
docker save -o app.tar mon-app:1.0  # exporter une image en archive
docker load -i app.tar              # réimporter

Le nom complet d'une image se lit ainsi : registre/espace/nom:tag. nginx signifie en réalité docker.io/library/nginx:latest.

latest n'est pas « la dernière version ». C'est un tag par défaut, un simple alias mutable que le mainteneur déplace quand il veut. Construire sur latest signifie que votre build de demain peut différer de celui d'aujourd'hui sans qu'une seule ligne ait changé. Épinglez toujours une version (nginx:1.27-alpine), et en production critique, épinglez le digest :

nginx@sha256:ac2f... — celui-là est immuable par construction.


9. Le Dockerfile, instruction par instruction

Le Dockerfile est la recette qui produit une image. Chaque instruction ajoute une couche.

InstructionCe qu'elle faitLe piège
FROMimage de base ; démarre une étapetoujours taguée, jamais latest
WORKDIRchange (et crée) le répertoire courantà préférer à RUN cd, qui ne persiste pas
COPYcopie depuis le contexte de buildsource relative au contexte, pas au Dockerfile
ADDcomme COPY, mais décompresse les archives et accepte une URLmagie implicite : préférer COPY
RUNexécute une commande au buildchaque RUN = une couche ; à enchaîner
CMDcommande par défaut au démarrageremplaçable en ligne de commande
ENTRYPOINTexécutable fixe au démarragevoir §10
ENVvariable d'environnement persistantevisible dans l'image : jamais de secret
ARGvariable disponible au build seulementreste dans l'historique : jamais de secret
EXPOSEdocumente un portne publie rien : c'est -p qui publie
VOLUMEdéclare un point de montage anonymecrée des volumes fantômes à chaque run
USERutilisateur d'exécutionsans lui, l'appli tourne en root
LABELmétadonnéeutile pour le tri et la traçabilité
HEALTHCHECKtest périodique de bonne santésans lui, depends_on ne sait rien
STOPSIGNALsignal envoyé par docker stop
SHELLchange le shell des RUN en forme shell
ONBUILDdéclencheur pour les images fillesrarement une bonne idée

Un Dockerfile Node complet et correct :


FROM node:22-alpine

# 1. Le répertoire de travail d'abord
WORKDIR /app

# 2. Les dépendances AVANT le code : c'est tout le secret du cache
COPY package*.json ./
RUN npm ci --omit=dev

# 3. Le code ensuite
COPY . .

# 4. Ne jamais tourner en root
RUN addgroup -S app && adduser -S app -G app && chown -R app:app /app
USER app

ENV NODE_ENV=production
EXPOSE 3000

HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
  CMD wget -qO- http://localhost:3000/health || exit 1

CMD ["node", "server.js"]

**Forme exec contre forme shell.** CMD ["node", "server.js"] (tableau JSON, forme exec) lance le processus directement : il devient PID 1 et reçoit les signaux. CMD node server.js (forme shell) lance en réalité /bin/sh -c "node server.js" : c'est sh qui est PID 1, il ne transmet pas SIGTERM, et docker stop attend dix secondes avant de tuer brutalement l'application. Utilisez toujours la forme tableau, avec des guillemets doubles — le JSON n'accepte pas les apostrophes.

Le .dockerignore, à écrire en même temps que le Dockerfile :


node_modules
.git
.env
dist
*.log
**/__pycache__

Sans lui, COPY . . embarque node_modules et l'historique Git : contexte de build de plusieurs centaines de Mo, envoi au démon interminable, cache invalidé à chaque commit.


10. CMD contre ENTRYPOINT : le tableau qui règle la question

La question revient dans tous les entretiens. La règle : ENTRYPOINT = le programme, CMD = ses arguments par défaut.

Dans le Dockerfiledocker run img exécutedocker run img --version exécute
CMD ["ping","localhost"]ping localhost--version (échec : CMD est remplacé)
ENTRYPOINT ["ping"]ping (sans argument)ping --version
ENTRYPOINT ["ping"] + CMD ["localhost"]ping localhostping --version
aucun des deuxcelui de l'image de base

Autrement dit : les arguments de la ligne de commande remplacent CMD, jamais ENTRYPOINT. Pour forcer malgré tout : docker run --entrypoint sh mon-image.

Le patron le plus utile en vrai est le script d'amorçage :


COPY entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
CMD ["node", "server.js"]

#!/bin/sh
set -e
# migrations, attente de la base, substitution de configuration…
npx prisma migrate deploy
exec "$@"     # ← exec : le processus final DEVIENT PID 1 et reçoit les signaux

Le exec "$@" de la dernière ligne n'est pas décoratif : sans lui, le shell reste PID 1 et l'arrêt propre est perdu.


11. Le cache de build, et comment ne pas le gâcher

Au build, Docker réutilise la couche déjà calculée tant que l'instruction et ses entrées sont identiques. Dès qu'une couche est invalidée, toutes les suivantes le sont aussi. D'où la règle d'or : du plus stable au plus volatil.


# ✗ MAUVAIS — un changement d'une ligne de code refait tout le npm ci
COPY . .
RUN npm ci

# ✓ BON — le npm ci n'est refait que si package.json change
COPY package*.json ./
RUN npm ci
COPY . .

Trois autres réflexes :


# Enchaîner les RUN : une seule couche, et le nettoyage a lieu DANS cette couche
RUN apt-get update && apt-get install -y --no-install-recommends curl \
 && rm -rf /var/lib/apt/lists/*

Supprimer un fichier dans une couche ultérieure ne le retire pas de l'image : il reste dans la couche du dessous, toujours téléchargé, toujours extractible. Un secret copié puis effacé est toujours dans l'imagedocker history le prouve.

Commandes de build :


docker build -t mon-app:1.0 .            # le « . » = le CONTEXTE de build
docker build -f docker/Dockerfile.prod -t mon-app:prod .
docker build --no-cache -t mon-app:1.0 . # tout refaire
docker build --build-arg VITE_API_URL=/api -t front .
docker build --target builder -t debug . # s'arrêter à une étape
docker builder prune                     # vider le cache de build

--build-arg n'est pas une variable d'exécution. Elle n'existe qu'au build. Pour un frontend compilé (Vite, Next, CRA), les variables sont figées dans le bundle au moment du build : les passer à docker run n'aura strictement aucun effet. C'est un classique — un VITE_API_URL oublié au build donne un site en ligne qui appelle localhost.


12. Le multi-stage build : diviser la taille par dix

On compile dans une image lourde, on n'emporte que le résultat dans une image légère.


# ── Étape 1 : compilation ─────────────────────────────
FROM node:22 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build          # produit /app/dist

# ── Étape 2 : exécution ───────────────────────────────
FROM nginx:1.27-alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

L'image finale ne contient ni node_modules, ni le compilateur, ni le code source : 40 Mo au lieu de 1,2 Go, et une surface d'attaque réduite d'autant. COPY --from= accepte aussi une image externe : COPY --from=golang:1.23 /usr/local/go/bin/go /usr/bin/go.

Le daemon off; de la dernière ligne illustre une règle générale : dans un conteneur, un service ne doit jamais passer en arrière-plan. S'il le fait, le PID 1 rend la main et le conteneur s'arrête.


13. Les volumes : faire survivre les données

Sans volume, tout ce qu'un conteneur écrit vit dans sa couche inscriptible, et disparaît avec lui. Trois types de montage :

TypeSyntaxe courteGéré parUsage
Volume nommé-v pgdata:/var/lib/postgresql/dataDockerdonnées de production, bases
Bind mount-v /home/kriss/app:/appvousdéveloppement, code en direct
tmpfs--tmpfs /tmpRAMfichiers temporaires, secrets volatils

La syntaxe --mount, plus verbeuse, est explicite et ne crée rien par accident :


docker run --mount type=volume,source=pgdata,target=/var/lib/postgresql/data \
  postgres:16
docker run --mount type=bind,source="$(pwd)",target=/app,readonly node:22

docker volume ls
docker volume create pgdata
docker volume inspect pgdata          # donne le chemin réel sur l'hôte
docker volume rm pgdata
docker volume prune                   # supprime les volumes NON utilisés

Le piège du chemin qui n'existe pas. Avec -v, si la source est un chemin qui n'existe pas sur l'hôte, Docker crée un dossier vide (appartenant à root) au lieu de protester. Avec --mount, il refuse. C'est la raison de préférer --mount dans les scripts.

Deux autres pièges de volumes, très fréquents :

Sauvegarder un volume est un idiome à connaître :


docker run --rm -v pgdata:/data -v "$(pwd)":/backup alpine \
  tar czf /backup/pgdata-$(date +%F).tar.gz -C /data .

14. Les réseaux : faire se parler les conteneurs

PiloteEffet
bridgepar défaut : réseau privé virtuel, isolé de l'hôte, ponté par NAT
hostaucune isolation réseau, le conteneur partage la pile de l'hôte (-p devient inutile)
noneaucun réseau
overlayréseau réparti entre plusieurs machines (Swarm)
macvlanle conteneur reçoit une vraie adresse MAC sur le réseau physique

docker network ls
docker network create back
docker network create --driver bridge --subnet 172.28.0.0/16 back
docker run -d --name db --network back postgres:16-alpine
docker run -d --name api --network back -p 3000:3000 mon-api
docker network connect front api      # un conteneur, PLUSIEURS réseaux
docker network inspect back
docker network prune

La règle qui résout 90 % des « connection refused ». Sur un réseau créé par vous, Docker fournit un DNS interne : un conteneur joint un autre par son nom. L'API se connecte donc à postgres://db:5432, jamais à localhost:5432 — car dans le conteneur api, localhost désigne api lui-même. En revanche, sur le réseau bridge par défaut, cette résolution par nom n'existe pas : c'est pourquoi un réseau dédié (ou Compose, qui en crée un tout seul) est indispensable.

Et depuis un conteneur, l'hôte se joint par host.docker.internal (Docker Desktop ; sous Linux, ajouter --add-host=host.docker.internal:host-gateway).


15. Docker Compose : décrire au lieu de taper

Compose remplace dix commandes docker run par un fichier versionnable. Depuis la v2, c'est un sous-commande (docker compose, sans tiret) écrite en Go ; l'ancien docker-compose en Python est en fin de vie.


services:
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: app
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

  api:
    build:
      context: ./backend
      dockerfile: Dockerfile
      args:
        NODE_ENV: production
    environment:
      DATABASE_URL: postgres://postgres:${DB_PASSWORD}@db:5432/app
    depends_on:
      db:
        condition: service_healthy     # ← attend la SANTÉ, pas le démarrage
    ports:
      - "127.0.0.1:3000:3000"
    restart: unless-stopped

  front:
    build:
      context: ./frontend
      args:
        VITE_API_URL: /api
    ports:
      - "127.0.0.1:8080:80"
    depends_on: [api]

volumes:
  pgdata:

docker compose up -d              # construit si besoin, crée le réseau, démarre
docker compose up -d --build      # force la reconstruction des images
docker compose down               # arrête et supprime conteneurs + réseau
docker compose down -v            # ⚠️ SUPPRIME LES VOLUMES (données perdues)
docker compose ps
docker compose logs -f api
docker compose exec api sh
docker compose restart api
docker compose build --no-cache front
docker compose pull               # met à jour les images distantes
docker compose config             # affiche le YAML final, variables résolues
docker compose stop / start
docker compose run --rm api npm run migrate   # tâche ponctuelle

depends_on seul ne fait qu'ordonner les démarrages, il n'attend rien. Sans condition: service_healthy, l'API démarre pendant que Postgres initialise encore son cluster, et plante sur ECONNREFUSED. Le couple healthcheck + condition: service_healthy est la seule réponse correcte.

Autres points à connaître :


16. Production : redémarrage, santé, ressources, journaux


--restart no                # défaut
--restart on-failure:3      # seulement si code de sortie non nul, 3 fois max
--restart always            # toujours, y compris après reboot du démon
--restart unless-stopped    # always, sauf si VOUS l'aviez arrêté ← le bon

docker run --memory 512m --memory-swap 512m --cpus 1.5 --pids-limit 200 mon-app

Sans --memory, un conteneur peut consommer toute la RAM de la machine et faire intervenir l'OOM killer du noyau — qui tuera peut-être un autre processus. Un conteneur tué pour cette raison sort avec le code 137 (128 + SIGKILL) et docker inspect affiche "OOMKilled": true.

Les journaux, enfin : par défaut Docker écrit dans un fichier JSON sans aucune limite de taille. C'est une cause classique de disque plein en production.


{
  "log-driver": "json-file",
  "log-opts": { "max-size": "10m", "max-file": "3" }
}

(à placer dans /etc/docker/daemon.json, puis systemctl restart docker).


17. Sécurité : les règles non négociables

  1. Ne pas tourner en root. USER app dans le Dockerfile. Par défaut, le PID 1 du conteneur est root, et root dans le conteneur est UID 0 sur l'hôte.
  2. Aucun secret dans l'image. Ni ENV, ni ARG, ni fichier copié puis effacé : tout reste dans les couches et dans docker history. Les secrets passent par variables d'exécution, fichiers montés, ou --secret de BuildKit.
  3. Images de base minimales et épinglées : alpine, -slim, ou distroless. Moins de binaires, moins de CVE, et pas de shell à détourner.
  4. --read-only + --tmpfs /tmp quand l'application n'écrit pas.
  5. --cap-drop ALL, puis rajouter uniquement le nécessaire (--cap-add NET_BIND_SERVICE pour écouter sous le port 1024).
  6. Jamais --privileged — il désactive à peu près toute l'isolation.
  7. --security-opt no-new-privileges pour bloquer l'escalade par binaire setuid.
  8. Ne pas publier sur 0.0.0.0 ce qui n'a pas à l'être : -p 127.0.0.1:5432:5432.
  9. Scanner : docker scout cves mon-app:1.0 (ou Trivy), et reconstruire régulièrement, car une image figée accumule les vulnérabilités de sa base.

18. Nettoyer le disque


docker system df                  # où part la place : images, volumes, cache
docker system df -v               # le détail, ligne par ligne
docker container prune            # supprime les conteneurs arrêtés
docker image prune                # supprime les images sans tag (dangling)
docker image prune -a             # ⚠️ supprime toutes les images NON utilisées
docker builder prune              # vide le cache de build (souvent le plus gros)
docker volume prune               # supprime les volumes non utilisés
docker system prune               # les quatre premiers d'un coup
docker system prune -a --volumes  # ☠️ tout, volumes compris : données PERDUES

prune ne demande pas deux fois. docker system prune -a --volumes sur un serveur, c'est la base de données effacée. La règle : docker system df d'abord pour voir, prune ciblé ensuite. docker builder prune seul règle la majorité des cas de disque plein sans rien risquer.


19. Déboguer : symptôme → cause → commande

SymptômeCause la plus probableQue faire
Le conteneur s'arrête aussitôtle PID 1 a terminé (service en arrière-plan, pas de TTY)docker logs <c> ; passer le service en avant-plan
Cannot connect to the Docker daemondémon arrêté, ou droitssudo systemctl start docker
port is already allocatedle port hôte est déjà prischanger le port, ou docker ps pour trouver le coupable
name is already in useun conteneur (même arrêté) porte ce nomdocker rm -f <nom>
connection refused entre conteneursusage de localhost au lieu du nom de servicejoindre par le nom, sur un réseau créé
no such file or directory au runforme exec et binaire absent, ou fin de ligne CRLFvérifier le chemin ; convertir le script en LF
Code de sortie 137tué par SIGKILL, souvent l'OOM killerdocker inspectOOMKilled ; augmenter --memory
Code de sortie 139segfaultbinaire compilé pour une autre architecture ou libc
Code de sortie 125 / 126 / 127erreur de Docker lui-même / commande non exécutable / introuvablerelire la ligne docker run
Permission denied sur un volumeUID du conteneur ≠ propriétaire du dossier hôte-u $(id -u):$(id -g) ou chown
Le build recompile tout à chaque foisCOPY . . placé avant l'installation des dépendancesréordonner (voir §11)
L'image ne contient pas les fichiers attenduscontexte de build erroné ou .dockerignore trop largedocker build depuis le bon dossier
exec format errorimage d'une autre architecture (arm64 vs amd64)--platform linux/amd64
Modifications invisibles après rebuildl'ancien conteneur tourne encoredocker compose up -d --build, ou rm -f puis relancer

Deux commandes de dernier recours :


docker run --rm -it --entrypoint sh mon-image   # fouiller une image muette
docker events                                   # événements du démon en direct

20. Les douze pièges classiques

  1. -p inversé : c'est hôte:conteneur.
  2. EXPOSE ne publie rien — seul -p (ou ports: en Compose) ouvre un port.
  3. localhost dans un conteneur désigne le conteneur, pas l'hôte ni le voisin.
  4. Écrire sans volume : les données meurent avec le conteneur.
  5. docker compose down -v supprime les volumes. Trois caractères, des mois de données.
  6. latest rend un build non reproductible.
  7. COPY . . avant l'installation des dépendances : cache détruit à chaque commit.
  8. Un secret « effacé » reste dans la couche précédente.
  9. **Forme shell pour CMD** : plus de propagation des signaux, arrêt brutal après 10 s.
  10. depends_on sans condition: service_healthy n'attend pas que le service soit prêt.
  11. Le bind mount masque le contenu de l'image au point de montage.
  12. Docker écrit dans iptables et contourne UFW : un port publié est réellement ouvert.

21. Cas concret : une stack React + API + PostgreSQL

C'est exactement l'architecture de cette plateforme, et elle concentre la moitié des points ci-dessus.


# 1. Le frontend est COMPILÉ : ses variables passent au BUILD, pas au run
docker build --no-cache \
  --build-arg VITE_API_URL=/api \
  --build-arg VITE_BASE_PATH=/plateforme \
  -t front ./frontend

# 2. Un conteneur ne se « met pas à jour » : on le remplace
docker rm -f front
docker run -d --name front -p 127.0.0.1:8087:80 front

# 3. Avec Compose, les deux étapes n'en font qu'une
docker compose up -d --build front

Trois enseignements tirés de cette stack :


22. Aide-mémoire — toutes les commandes utiles


# ── Conteneurs ───────────────────────────────────────────────
docker run -d --name X -p 8080:80 --restart unless-stopped IMG
docker ps [-a] [-q]              docker logs -f --tail 100 X
docker exec -it X sh             docker stop/start/restart/kill X
docker rm [-f] X                 docker inspect [-f '{{.State.Status}}'] X
docker stats                     docker top X          docker port X
docker cp X:/chemin .            docker diff X         docker wait X

# ── Images ───────────────────────────────────────────────────
docker pull IMG:TAG              docker images
docker build -t NOM:TAG .        docker build --build-arg K=V --no-cache .
docker tag SRC DST               docker push / docker login
docker history IMG               docker rmi IMG
docker save -o f.tar IMG         docker load -i f.tar

# ── Volumes ──────────────────────────────────────────────────
docker volume ls / create / inspect / rm / prune
docker run -v nom:/chemin        docker run -v "$(pwd)":/app
docker run --mount type=bind,source=/src,target=/app,readonly

# ── Réseaux ──────────────────────────────────────────────────
docker network ls / create / inspect / connect / disconnect / prune

# ── Compose ──────────────────────────────────────────────────
docker compose up -d [--build]   docker compose down [-v]
docker compose ps / logs -f / exec S sh / restart S
docker compose config            docker compose run --rm S CMD

# ── Ménage ───────────────────────────────────────────────────
docker system df [-v]            docker system prune [-a] [--volumes]
docker image prune -a            docker builder prune     docker volume prune

23. Exercices d'application

Exercice 1 — Lecture de commande. Que fait exactement cette ligne, option par option ?


docker run -d --name cache --restart unless-stopped -p 127.0.0.1:6379:6379 \
  -v redisdata:/data --memory 256m redis:7-alpine redis-server --appendonly yes

Corrigé. Lance en arrière-plan (-d) un conteneur nommé cache, relancé automatiquement sauf arrêt manuel, dont le port 6379 est publié uniquement sur la boucle locale de l'hôte (inaccessible depuis l'extérieur), avec le volume nommé redisdata monté sur /data pour la persistance, plafonné à 256 Mo de RAM, à partir de l'image redis:7-alpine. Enfin, redis-server --appendonly yes remplace le CMD de l'image et active la journalisation sur disque.

Exercice 2 — Corriger un Dockerfile. Trouver les cinq erreurs :


FROM node:latest
COPY . .
RUN npm install
ENV DB_PASSWORD=SuperSecret123
EXPOSE 3000
CMD npm start

Corrigé. (1) FROM node:latest — non reproductible et énorme : node:22-alpine. (2) COPY . . avant npm install détruit le cache et manque de WORKDIR : copier package*.json, installer, puis copier le reste. (3) Le mot de passe en ENV est inscrit dans l'image et visible par docker history : le passer à l'exécution. (4) Aucun USER : l'application tourne en root. (5) CMD npm start en forme shell : le PID 1 est sh, SIGTERM n'est pas propagé — écrire CMD ["node", "server.js"].

Exercice 3 — Diagnostic. Une API dans un conteneur répond ECONNREFUSED 127.0.0.1:5432, alors que le conteneur db tourne bien. Deux causes possibles, et leur correction ?

Corrigé. (a) L'API se connecte à localhost, qui dans son propre espace réseau la désigne elle-même : il faut utiliser le nom du service, postgres://db:5432. (b) Les deux conteneurs ne sont pas sur le même réseau créé par l'utilisateur (ou sont sur le bridge par défaut, qui n'a pas de résolution DNS par nom) : créer un réseau, docker network create back, et y attacher les deux. Cause (c), plus rare mais fréquente au premier démarrage : la base n'a pas fini d'initialiser — c'est le rôle de healthcheck + depends_on: condition: service_healthy.

Exercice 4 — Multi-stage. Écrire un Dockerfile qui compile un binaire Go et produit une image finale de quelques mégaoctets.

Corrigé.


FROM golang:1.23-alpine AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /app/serveur ./cmd/serveur

FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/serveur /serveur
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/serveur"]

CGO_ENABLED=0 produit un binaire statique, seule façon de le faire tourner sur une image sans libc.

Exercice 5 — Question de cours. Expliquer en trois phrases pourquoi supprimer un fichier de secret dans un RUN ultérieur ne le retire pas de l'image.

Corrigé. Chaque instruction crée une couche immuable empilée sur les précédentes, et l'image est la somme de toutes ses couches. Une suppression dans une couche haute n'écrit qu'un marqueur d'effacement (whiteout) : la couche basse, qui contient le fichier, est toujours présente, toujours téléchargée et toujours lisible. La seule solution est de ne jamais y écrire le secret — montage à l'exécution, ou RUN --mount=type=secret de BuildKit.

Exercice 6 — Ménage raisonné. Un serveur est à 98 % de disque. Quelle commande lancer en premier, et laquelle ne surtout pas lancer ?

Corrigé. En premier docker system df -v, pour voir la répartition sans rien détruire ; puis, le plus souvent, docker builder prune (le cache de build est fréquemment le poste le plus lourd) et docker image prune -a si les images non utilisées dominent. À proscrire : docker system prune -a --volumes, qui emporte les volumes — donc les bases de données.


24. Tableau de synthèse — à retenir pour l'oral

QuestionRéponse en une phrase
Conteneur ou VM ?la VM virtualise le matériel et embarque un noyau ; le conteneur partage celui de l'hôte
Image ou conteneur ?l'image est la classe, le conteneur l'instance ; run est le new
Sur quoi repose l'isolation ?namespaces (la vue), cgroups (la consommation), OverlayFS (les fichiers)
CMD ou ENTRYPOINT ?ENTRYPOINT = le programme, CMD = ses arguments par défaut, remplaçables
Forme exec ou shell ?toujours la forme tableau JSON, pour que le processus reçoive les signaux
Comment ne pas casser le cache ?du plus stable au plus volatil : dépendances avant code
Pourquoi un multi-stage ?compiler dans une image lourde,

Tous les cours · coursDeLion · La chaîne YouTube