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é :
| Solution | Ce qu'on livre | Poids | Démarrage | Isolation |
|---|---|---|---|---|
| Livrer le code seul | quelques Mo | instantané | aucune | aucune |
| Machine virtuelle | un OS complet + noyau | plusieurs Go | 30 s à 2 min | forte (noyau séparé) |
| Conteneur Docker | l'appli + ses dépendances | 10 à 300 Mo | quelques dizaines de ms | bonne (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 auxsur 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
Dockerfileproduit une couche empilée sur la précédente. Les couches sont partagées entre images : dix images basées suralpinene stockent qu'une seule foisalpine.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 :
- **Les namespaces** cloisonnent la vue :
pid(le processus se croit seul, il est PID 1),net(sa propre pile réseau et ses propres interfaces),mnt(son propre arbre de fichiers),uts(son propre nom d'hôte),ipc,user. - **Les cgroups** cloisonnent la consommation : combien de mémoire, quelle part de CPU, quel débit disque. C'est ce que pilotent
--memoryet--cpus. - Le système de fichiers en couches (OverlayFS) empile les couches en lecture seule et pose au-dessus une couche inscriptible propre au conteneur. Écrire dans un fichier existant le copie d'abord vers la couche du haut (copy-on-write).
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.sockpeut démarrer un conteneur privilégié qui monte/de l'hôte : appartenir au groupedockeré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.
| Option | Rôle | Exemple |
|---|---|---|
-d, --detach | lance en arrière-plan, affiche l'ID | docker run -d nginx |
-it | -i garde l'entrée standard ouverte, -t alloue un pseudo-terminal | docker run -it alpine sh |
--rm | supprime le conteneur dès qu'il s'arrête | docker run --rm alpine date |
--name | nom lisible, unique sur la machine | --name api |
-p H:C | publie le port C du conteneur sur le port H de l'hôte | -p 8080:80 |
-P | publie tous les ports EXPOSE sur des ports aléatoires | |
-e, --env | définit une variable d'environnement | -e NODE_ENV=production |
--env-file | lit les variables dans un fichier | --env-file .env |
-v, --volume | monte un volume ou un dossier de l'hôte | -v data:/var/lib/postgresql/data |
--mount | même chose, syntaxe explicite (recommandée) | voir §13 |
-w, --workdir | répertoire de travail dans le conteneur | -w /app |
-u, --user | UID:GID d'exécution | -u 1000:1000 |
--network | réseau à rejoindre | --network back |
--restart | politique de redémarrage | --restart unless-stopped |
--memory, --cpus | plafonds de ressources | --memory 512m --cpus 1.5 |
--entrypoint | remplace l'ENTRYPOINT de l'image | --entrypoint sh |
--read-only | système de fichiers en lecture seule | |
--cap-drop, --cap-add | retire ou ajoute une capacité noyau | --cap-drop ALL |
--health-cmd | test de santé à la volée | |
--pull always | force à 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:80se 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:5432publie 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 dansiptables, 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
execn'est pasrun.docker execentre dans un conteneur déjà en marche ;docker runen crée un nouveau. Sidocker execrépond « is not running », le conteneur est mort : c'estdocker logsqu'il faut lire, pasexecqu'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.
latestn'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 surlatestsignifie 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.
| Instruction | Ce qu'elle fait | Le piège |
|---|---|---|
FROM | image de base ; démarre une étape | toujours taguée, jamais latest |
WORKDIR | change (et crée) le répertoire courant | à préférer à RUN cd, qui ne persiste pas |
COPY | copie depuis le contexte de build | source relative au contexte, pas au Dockerfile |
ADD | comme COPY, mais décompresse les archives et accepte une URL | magie implicite : préférer COPY |
RUN | exécute une commande au build | chaque RUN = une couche ; à enchaîner |
CMD | commande par défaut au démarrage | remplaçable en ligne de commande |
ENTRYPOINT | exécutable fixe au démarrage | voir §10 |
ENV | variable d'environnement persistante | visible dans l'image : jamais de secret |
ARG | variable disponible au build seulement | reste dans l'historique : jamais de secret |
EXPOSE | documente un port | ne publie rien : c'est -p qui publie |
VOLUME | déclare un point de montage anonyme | crée des volumes fantômes à chaque run |
USER | utilisateur d'exécution | sans lui, l'appli tourne en root |
LABEL | métadonnée | utile pour le tri et la traçabilité |
HEALTHCHECK | test périodique de bonne santé | sans lui, depends_on ne sait rien |
STOPSIGNAL | signal envoyé par docker stop | |
SHELL | change le shell des RUN en forme shell | |
ONBUILD | déclencheur pour les images filles | rarement 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 Dockerfile | docker run img exécute | docker 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 localhost | ping --version |
| aucun des deux | celui 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'image — docker 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-argn'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 runn'aura strictement aucun effet. C'est un classique — unVITE_API_URLoublié au build donne un site en ligne qui appellelocalhost.
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 :
| Type | Syntaxe courte | Géré par | Usage |
|---|---|---|---|
| Volume nommé | -v pgdata:/var/lib/postgresql/data | Docker | données de production, bases |
| Bind mount | -v /home/kriss/app:/app | vous | développement, code en direct |
| tmpfs | --tmpfs /tmp | RAM | fichiers 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--mountdans les scripts.
Deux autres pièges de volumes, très fréquents :
- Le bind mount masque le contenu de l'image. Monter
./appsur/appcache le/appconstruit dans l'image — y comprisnode_modulesinstallé au build. Le remède habituel : ajouter un volume anonyme par-dessus,-v /app/node_modules, qui « re-perce » le trou. - Les permissions. Le conteneur écrit avec son UID, pas avec votre nom. Si l'appli tourne en UID 1000 et que le dossier hôte appartient à root, c'est
Permission denied. Solutions :-u $(id -u):$(id -g), ouchowndu dossier hôte.
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
| Pilote | Effet |
|---|---|
bridge | par défaut : réseau privé virtuel, isolé de l'hôte, ponté par NAT |
host | aucune isolation réseau, le conteneur partage la pile de l'hôte (-p devient inutile) |
none | aucun réseau |
overlay | réseau réparti entre plusieurs machines (Swarm) |
macvlan | le 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 conteneurapi,localhostdésigneapilui-même. En revanche, sur le réseaubridgepar 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_onseul ne fait qu'ordonner les démarrages, il n'attend rien. Sanscondition: service_healthy, l'API démarre pendant que Postgres initialise encore son cluster, et plante surECONNREFUSED. Le couplehealthcheck+condition: service_healthyest la seule réponse correcte.
Autres points à connaître :
docker compose configest l'outil de débogage n°1 : il montre le fichier après fusion desoverride, résolution des${VARIABLES}et application desprofiles.- Le fichier
docker-compose.override.ymlest fusionné automatiquement avec le principal : c'est la façon propre de séparer développement et production. - La clé
container_namefige le nom : pratique, mais elle interdit de monter en échelle (--scale) et provoque un conflit « name is already in use » au recreate. Le remède :docker rm -f <nom>puis relancer. - La clé
version:en haut du fichier est obsolète en Compose v2 ; elle déclenche un avertissement.
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
- Ne pas tourner en root.
USER appdans le Dockerfile. Par défaut, le PID 1 du conteneur est root, et root dans le conteneur est UID 0 sur l'hôte. - Aucun secret dans l'image. Ni
ENV, niARG, ni fichier copié puis effacé : tout reste dans les couches et dansdocker history. Les secrets passent par variables d'exécution, fichiers montés, ou--secretde BuildKit. - Images de base minimales et épinglées :
alpine,-slim, oudistroless. Moins de binaires, moins de CVE, et pas de shell à détourner. --read-only+--tmpfs /tmpquand l'application n'écrit pas.--cap-drop ALL, puis rajouter uniquement le nécessaire (--cap-add NET_BIND_SERVICEpour écouter sous le port 1024).- Jamais
--privileged— il désactive à peu près toute l'isolation. --security-opt no-new-privilegespour bloquer l'escalade par binaire setuid.- Ne pas publier sur
0.0.0.0ce qui n'a pas à l'être :-p 127.0.0.1:5432:5432. - 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
prunene demande pas deux fois.docker system prune -a --volumessur un serveur, c'est la base de données effacée. La règle :docker system dfd'abord pour voir,pruneciblé ensuite.docker builder pruneseul règle la majorité des cas de disque plein sans rien risquer.
19. Déboguer : symptôme → cause → commande
| Symptôme | Cause la plus probable | Que faire |
|---|---|---|
| Le conteneur s'arrête aussitôt | le 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 daemon | démon arrêté, ou droits | sudo systemctl start docker |
port is already allocated | le port hôte est déjà pris | changer le port, ou docker ps pour trouver le coupable |
name is already in use | un conteneur (même arrêté) porte ce nom | docker rm -f <nom> |
connection refused entre conteneurs | usage de localhost au lieu du nom de service | joindre par le nom, sur un réseau créé |
no such file or directory au run | forme exec et binaire absent, ou fin de ligne CRLF | vérifier le chemin ; convertir le script en LF |
| Code de sortie 137 | tué par SIGKILL, souvent l'OOM killer | docker inspect → OOMKilled ; augmenter --memory |
| Code de sortie 139 | segfault | binaire compilé pour une autre architecture ou libc |
| Code de sortie 125 / 126 / 127 | erreur de Docker lui-même / commande non exécutable / introuvable | relire la ligne docker run |
Permission denied sur un volume | UID du conteneur ≠ propriétaire du dossier hôte | -u $(id -u):$(id -g) ou chown |
| Le build recompile tout à chaque fois | COPY . . placé avant l'installation des dépendances | réordonner (voir §11) |
| L'image ne contient pas les fichiers attendus | contexte de build erroné ou .dockerignore trop large | docker build depuis le bon dossier |
exec format error | image d'une autre architecture (arm64 vs amd64) | --platform linux/amd64 |
| Modifications invisibles après rebuild | l'ancien conteneur tourne encore | docker 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
-pinversé : c'esthôte:conteneur.EXPOSEne publie rien — seul-p(ouports:en Compose) ouvre un port.localhostdans un conteneur désigne le conteneur, pas l'hôte ni le voisin.- Écrire sans volume : les données meurent avec le conteneur.
docker compose down -vsupprime les volumes. Trois caractères, des mois de données.latestrend un build non reproductible.COPY . .avant l'installation des dépendances : cache détruit à chaque commit.- Un secret « effacé » reste dans la couche précédente.
- **Forme shell pour CMD** : plus de propagation des signaux, arrêt brutal après 10 s.
depends_onsanscondition: service_healthyn'attend pas que le service soit prêt.- Le bind mount masque le contenu de l'image au point de montage.
- 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 :
- Un fichier ajouté dans
public/n'est servi qu'après reconstruction de l'image : ce dossier est copié au build, pas lu à chaud. Un lot d'images « déjà déposées » qui renvoie des 404 s'explique toujours ainsi. - Une base de données sur l'hôte, en écoute sur
127.0.0.1, est injoignable depuis un réseau bridge. Depuis le conteneur,127.0.0.1c'est lui-même. Il faut--network host, ouhost.docker.internalavec--add-host, ou déplacer la base dans la stack. - Toujours étiqueter l'image précédente avant un rebuild :
docker tag <id> front:rollback-2026-08-26. Sans tag, l'ancienne image devient dangling et le premierprunel'efface — le retour arrière n'existe plus.
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
| Question | Ré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, |