Chapitre 12.4 — Docker en production
⏱️ TL;DR — Faire tourner
docker compose upsur ton portable, c’est une chose. Le faire tenir en production en est une autre. Quatre chantiers : un reverse proxy devant les conteneurs (le Nginx de l’hôte qui proxifie le port publié en local, ou un proxy conteneurisé type Traefik qui fait le HTTPS tout seul) ; la résilience (restart: unless-stopped+ Docker qui démarre au boot, pour que tout remonte après un reboot ou un crash) ; le déploiement par registre (pullla nouvelle image →up -d) ; et l’hygiène (logs bornés pour ne pas saturer le disque,docker system prunepour vider les images mortes, mises à jour prudentes). Fil rouge sécurité : ne jamais exposer le socket Docker, qui vaut root sur l’hôte.
🎯 Objectifs
- Placer un reverse proxy devant les conteneurs (Nginx hôte ou proxy conteneurisé).
- Garantir le redémarrage au boot et après crash (
restart+ servicedockeractivé). - Déployer une nouvelle version via un registre (
pullpuisup -d). - Borner les logs des conteneurs pour ne pas remplir le disque.
- Nettoyer les images/volumes orphelins (
docker system prune). - Comprendre les mises à jour automatiques (Watchtower) et leurs risques.
- Ne jamais exposer le socket Docker.
Un reverse proxy devant, toujours
On ne publie jamais un conteneur applicatif directement sur le port 80/443 d’Internet. Comme en Partie 7, un reverse proxy se tient en façade : il termine le TLS (HTTPS), route selon le domaine, et relaie vers le conteneur. Deux approches, selon que le proxy vit sur l’hôte ou dans un conteneur.
Option A — Le Nginx de l’hôte (le plus simple)
Tu gardes le Nginx installé sur le VPS (celui des Parties 7-8, avec Certbot pour le HTTPS). Ton compose.yaml publie le front sur 127.0.0.1:3000 (localhost seulement), et Nginx proxifie vers ce port local — exactement comme il le ferait pour une app lancée par systemd. Docker devient un détail d’implémentation invisible pour Nginx.
server {
listen 443 ssl;
server_name formacampus.fr;
# ... configuration TLS gérée par Certbot ...
location / {
proxy_pass http://127.0.0.1:3000; # le conteneur publié en local
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}💡 Réflexe — Si tu as déjà un Nginx hôte maîtrisé (config, HTTPS, logs), garde-le : conteneuriser l’app ne t’oblige pas à conteneuriser le proxy. Tu réutilises tout ton acquis des Parties 7-8, et le conteneur n’est qu’un
proxy_passde plus. C’est l’option la plus progressive pour un dev qui débute avec Docker.
Option B — Un proxy conteneurisé (Traefik, Nginx Proxy Manager)
Ici, le proxy est lui-même un conteneur dans le compose.yaml. Traefik est le plus connu : il découvre automatiquement les autres conteneurs via leurs labels, génère les routes et obtient les certificats Let’s Encrypt tout seul. On configure le routage dans le compose.yaml, pas dans un fichier Nginx séparé :
web:
build: .
restart: unless-stopped
labels:
- "traefik.enable=true"
- "traefik.http.routers.web.rule=Host(`formacampus.fr`)"
- "traefik.http.routers.web.tls.certresolver=letsencrypt"
networks:
- internalAvantage : tout (routage et HTTPS) vit dans la stack, versionné avec le reste, idéal quand tu as plein de services à exposer. Inconvénient : une courbe d’apprentissage (les labels Traefik, ça se dompte) et un composant de plus à comprendre. Nginx Proxy Manager offre une alternative avec interface web pour ceux qui préfèrent cliquer.
⚠️ Piège — Deux proxys qui se marchent dessus. Si tu lances un proxy conteneurisé (Traefik) alors que le Nginx de l’hôte écoute déjà sur 80/443, le second ne pourra pas démarrer (« port déjà utilisé »). Choisis une stratégie : soit Nginx hôte + conteneurs publiés en local, soit proxy conteneurisé qui prend 80/443 et un hôte qui ne fait pas tourner Nginx dessus. Pas les deux.
La résilience : tout doit remonter tout seul
Deux mécanismes complémentaires garantissent qu’après un incident, la stack revient d’elle-même.
1. La politique de redémarrage des conteneurs. Dans chaque service, restart: unless-stopped dit à Docker : « redémarre ce conteneur s’il crashe ou au démarrage du démon, sauf si je l’ai arrêté explicitement ». C’est l’équivalent conteneur du Restart=always de systemd (Partie 9). Sans ça, un conteneur qui plante reste mort.
2. Docker lui-même doit démarrer au boot. Le démon Docker est un service systemd : s’il n’est pas activé, aucun conteneur ne remontera après un reboot du VPS — même avec la meilleure politique restart.
# Vérifier que Docker démarre au boot (souvent déjà le cas après l'install officielle)
sudo systemctl enable --now docker
sudo systemctl is-enabled docker # doit répondre : enabled⚠️ Piège —
restart: unless-stoppedsans le servicedockeractivé au boot = illusion de résilience. Le conteneur redémarrerait après un crash de l’app, mais pas après un reboot du serveur, car Docker lui-même ne serait pas relancé. Vérifiesystemctl is-enabled docker: les deux conditions doivent être vraies. C’est le pendant conteneur du piège « j’ai oubliéenable» de systemd.
Déployer une nouvelle version
En prod, on ne builde généralement pas sur le serveur : on construit l’image en CI, on la pousse dans un registre (Docker Hub, GHCR…), et le serveur la tire. Le déploiement se résume alors à :
# Sur le VPS, dans le dossier de la stack
docker compose pull # tire les nouvelles versions des images depuis le registre
docker compose up -d # recrée seulement les conteneurs dont l'image a changé
docker image prune -f # supprime les anciennes images désormais inutiliséesCompose est malin : up -d ne recrée que les services dont l’image (ou la config) a réellement changé, et laisse les autres tourner. Postgres n’est pas touché parce que tu redéploies le front. La Partie 13 automatisera exactement cette séquence à chaque merge.
💡 Réflexe — Tague tes images avec une version (
formacampus-web:1.4.0), pas seulementlatest. En prod,latestest ambigu (« la dernière quoi ? ») et rend les rollbacks impossibles : si la1.4.0casse, tu redéploies la1.3.0en une ligne. Aveclatest, tu ne sais même plus ce qui tournait.
Les logs : les borner avant qu’ils ne saturent le disque
Par défaut, Docker écrit les logs de chaque conteneur dans un fichier json-file qui grossit sans limite. Un conteneur bavard peut remplir le disque en quelques jours et faire tomber tout le VPS. On borne les logs — par conteneur, ou globalement via le fichier de config du démon.
// /etc/docker/daemon.json — configuration globale du démon Docker
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}Ici, chaque conteneur garde au plus 3 fichiers de 10 Mo (rotation automatique), soit 30 Mo max par conteneur. Après modification, sudo systemctl restart docker applique la config (elle ne s’applique qu’aux conteneurs recréés ensuite).
⚠️ Piège — le disque qui se remplit en silence. C’est la panne Docker sournoise. Entre les logs non bornés, les images accumulées à chaque build/pull, les volumes orphelins et le cache de build, le disque du VPS se remplit progressivement, sans alerte, jusqu’au jour où plus rien ne démarre (« no space left on device »). Surveille
df -h(Partie 14), borne les logs, et nettoie régulièrement (ci-dessous).
Le nettoyage : garder le disque sain
Docker accumule. Chaque build laisse des images intermédiaires, chaque pull d’une nouvelle version rend l’ancienne inutile, des volumes et réseaux orphelins traînent. On fait le ménage :
# Voir combien Docker occupe (images, conteneurs, volumes, cache de build)
docker system df
# Supprimer conteneurs arrêtés, réseaux inutilisés, images ORPHELINES, cache
docker system prune
# Version plus agressive : supprime aussi les images non utilisées par un conteneur
docker system prune -a
# Nettoyer uniquement les images inutilisées
docker image prune -a🔒 Sécurité —
pruneet les volumes.docker system prunene touche pas aux volumes par défaut (heureusement — tes données de base y sont). Il faut--volumespour les inclure, et là tu risques d’effacer des données si un volume utile est temporairement détaché. Ne mets jamais--volumesdans unpruneautomatisé sans savoir précisément ce qui va disparaître. Le ménage des images est sûr ; celui des volumes demande de la vigilance.
Les mises à jour automatiques : Watchtower, avec prudence
Watchtower est un conteneur qui surveille les registres et met à jour automatiquement tes conteneurs quand une nouvelle image paraît. Séduisant sur le papier, risqué en prod : une mise à jour non testée peut casser ton service sans que tu l’aies décidé, en pleine nuit. On le cite pour la culture ; en production maîtrisée, on préfère un déploiement explicite (via la CI), où tu choisis quand et quelle version part. Watchtower peut convenir pour des services non critiques ou du homelab, pas pour la prod d’une plateforme comme FormaCampus.
Le socket Docker : à ne jamais exposer
Le socket Docker (/var/run/docker.sock) est le point d’entrée du démon. Qui y a accès contrôle Docker, donc l’hôte entier (souviens-toi : le groupe docker vaut root, chapitre 12.1).
🔒 Sécurité — le socket Docker vaut root. Ne monte jamais
/var/run/docker.sockdans un conteneur exposé au public « pour qu’il pilote Docker », et ne l’expose jamais sur le réseau. Un conteneur qui a le socket peut lancer n’importe quel autre conteneur, monter le disque de l’hôte, et prendre le contrôle total du VPS. Certains outils (Traefik, portainer, Watchtower) le demandent en lecture seule pour découvrir les conteneurs — ne l’accorde qu’à des composants de confiance, jamais à ce qui traite des requêtes publiques, et jamais en écriture si tu peux l’éviter.
🧭 Sur FormaCampus — En prod, FormaCampus retient l’option A : le Nginx de l’hôte (déjà en place depuis les Parties 7-8, avec Certbot) proxifie vers le front conteneurisé publié sur
127.0.0.1:3000et l’API sur127.0.0.1:8000. Chaque service porterestart: unless-stopped, etdockerest activé au boot : un reboot du VPS remonte toute la plateforme seule. Le déploiement (piloté par la Partie 13) faitdocker compose pull && docker compose up -davec des images taguées par version (rollback possible). Les logs sont bornés (daemon.json, 10 Mo × 3), undocker system prunehebdomadaire (sans--volumes) tient le disque au propre, surveillé par ledf -hdu monitoring (Partie 14). Watchtower ? Écarté : les mises à jour partent de la CI, quand l’équipe le décide.
✏️ Exercices
Exercice 1 — Le site ne remonte pas après reboot. Après un redémarrage du VPS, aucun conteneur ne tourne, alors que tous ont restart: unless-stopped. Quelle est la cause probable, et comment la vérifier/corriger ?
✅ Solution
Le service docker lui-même n’est pas activé au boot. La politique restart ne s’applique que si le démon Docker démarre ; s’il ne se lance pas au boot, rien ne remonte. Vérification : systemctl is-enabled docker (probablement disabled). Correction : sudo systemctl enable --now docker. Les deux conditions — restart: unless-stopped et service docker activé — sont nécessaires.
Exercice 2 — « No space left on device ». Le VPS ne démarre plus de conteneur, le disque est plein. Docker occupe 40 Go. Quelles commandes pour diagnostiquer puis récupérer de l’espace sans risquer les données ?
✅ Solution
Diagnostic : df -h (confirme le disque plein) puis docker system df (répartition : images / conteneurs / volumes / cache). Récupération sûre : docker image prune -a (images inutilisées) et docker system prune (conteneurs arrêtés, réseaux, cache) — sans --volumes, pour ne pas toucher aux données de la base. Prévention : borner les logs (daemon.json, max-size) et planifier un prune régulier. Jamais --volumes dans un nettoyage automatique.
Exercice 3 — Choisir le proxy. Ton VPS fait déjà tourner un Nginx hôte avec Certbot pour un autre site. Tu ajoutes une app conteneurisée. Traefik conteneurisé ou Nginx hôte ? Justifie.
✅ Solution
Nginx hôte (option A). Il occupe déjà les ports 80/443 ; y lancer un Traefik conteneurisé créerait un conflit de port et t’obligerait à démanteler ta config existante. Le plus simple : publier le conteneur sur 127.0.0.1:<port> et ajouter un proxy_pass dans Nginx, en réutilisant ta chaîne Certbot/HTTPS déjà en place. Traefik serait pertinent sur un serveur neuf, dédié aux conteneurs, avec beaucoup de services à exposer.
🧠 Quiz de révision
1. Quelles sont les deux stratégies de reverse proxy devant des conteneurs ?
(A) Le Nginx de l’hôte proxifie vers le conteneur publié en local (127.0.0.1:port) — réutilise l’acquis des Parties 7-8. (B) Un proxy conteneurisé (Traefik, Nginx Proxy Manager) qui découvre les conteneurs par labels et fait le HTTPS automatique. On choisit l’un ou l’autre, jamais les deux sur les mêmes ports.
2. Quelles deux conditions pour qu’une stack remonte après un reboot ?
(1) restart: unless-stopped sur chaque service (redémarrage après crash/boot du démon). (2) Le service docker activé au boot (systemctl enable docker). Sans le second, le démon ne se lance pas au reboot et rien ne remonte, même avec restart.
3. Comment déploie-t-on une nouvelle version avec un registre ?
L’image est construite en CI et poussée dans un registre. Sur le serveur : docker compose pull (tire la nouvelle image) puis docker compose up -d (recrée seulement ce qui a changé). On tague par version pour permettre le rollback, et on prune les vieilles images.
4. Pourquoi et comment borner les logs des conteneurs ?
Parce que le driver json-file par défaut grossit sans limite et peut saturer le disque. On borne via /etc/docker/daemon.json avec max-size et max-file (rotation), ou par conteneur. Sinon, panne « no space left on device ».
5. Pourquoi ne jamais exposer le socket Docker ?
Parce qu’il vaut root sur l’hôte : qui contrôle le socket peut lancer n’importe quel conteneur, monter le disque et prendre le VPS. On ne le monte que dans des composants de confiance (idéalement en lecture seule), jamais dans un conteneur exposé au public ni sur le réseau.
Chapitre suivant : Docker ou bare-metal — l’arbitrage honnête, sans hype.