Un carton rouge direct, ça arrive rarement. Ce qui arrive, c'est un jaune à la 20ᵉ minute pour une faute anodine, puis un second à la 70ᵉ, tout aussi anodin. Aucun des deux ne méritait qu'on en parle. C'est le cumul qui te sort du terrain.
Mon VPS a joué ce match pendant trois mois. Le 31 juillet à 15h44, il a pris le second jaune :
PANIC: could not write to file "pg_logical/replorigin_checkpoint.tmp":
No space left on device
Postgres s'arrête. Quatre applications tombent. Et la cause tient dans deux caractères manquants dans un fichier de configuration que j'avais écrit moi-même.
L'autopsie
df -h : 58 Go, 100 % utilisés, 0 octet libre.
Premier réflexe, chercher les journaux. Bonne intuition, mauvaise cible : 996 Mo. Réel, mais loin du compte.
docker system df a donné la réponse en une seconde, là où un du sur tout le disque tournait depuis dix minutes et faisait tomber ma session SSH :
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 41 19 38.43 GB 20.49 GB (53%)
Containers 19 19 6.07 MB 0 B
Local Volumes 5 5 425.8 MB 0 B
Build Cache 28 0 8.93 GB 2.48 GB
47 Go d'images et de cache Docker sur 57 utilisés. Les données applicatives, elles, pesaient 426 Mo.
Le détail qui pique : mon dump complet de la base Supabase, pris juste après la remise en route, faisait 1,2 Mo. Toute ma production tenait dans un fichier plus léger qu'une photo de téléphone, et elle était à l'arrêt à cause de couches d'images obsolètes.
Les deux caractères
Voici l'étape de nettoyage de mon pipeline de déploiement, écrite en toute bonne foi :
docker image prune -f
Et voici ce qu'elle fait réellement : supprimer les images dangling — celles qui n'ont plus aucun tag associé.
Or mon pipeline tague chaque build :
docker build -t "$IMAGE_NAME:$CI_COMMIT_SHA" -t "$IMAGE_NAME:latest" .
Chaque image porte le SHA du commit. Aucune n'est jamais dangling. Le nettoyage ne nettoyait rien.
Il manquait -a :
docker image prune -af --filter "until=168h"
Avec -a, la commande supprime toutes les images qu'aucun conteneur n'utilise, taguées ou non. Le filtre until=168h garde une semaine, ce qui permet de revenir en arrière rapidement sans accumuler indéfiniment.
Quand j'ai lancé la version corrigée : 974 Mo libres → 23 Go. Vingt-deux images supprimées, dont huit versions de la même application à 1,45 Go pièce.
Ce qui rend cette panne intéressante
Plus je travaillais, plus je me rapprochais du mur
C'est la propriété la plus perverse. Un pipeline qui tourne rarement n'aurait jamais rempli le disque. C'est mon rythme de livraison qui a armé la bombe. Quatre projets déployés régulièrement sur le même serveur, chacun laissant une image derrière lui.
Les incidents qui punissent l'activité sont les plus difficiles à anticiper, parce que rien dans l'expérience quotidienne ne signale la dérive.
Aucune dégradation avant l'arrêt
Il n'y a pas eu de « ça devient lent ». Le disque est passé de 97 % à 98 % à 99 %, sans effet perceptible, puis à 100 % et tout s'est arrêté d'un coup.
Les ressources à seuil se comportent ainsi : linéaires jusqu'à la falaise. C'est pour ça qu'une alerte à 95 % ne sert quasiment à rien — quand elle se déclenche, tu as déjà les deux pieds dans le vide. 80 %, c'est le bon seuil : il te laisse le temps de traiter le problème comme une tâche, pas comme une urgence.
Docker s'est menti à lui-même
Détail troublant : après la panne, docker ps -a affichait supabase-db | Up 2 months (unhealthy). Le conteneur était pourtant bel et bien arrêté — docker exec répondait « container is not running ».
L'explication est logique une fois posée : le démon Docker n'arrivait plus à écrire ses fichiers d'état, faute de place. Il affichait donc le dernier état qu'il avait pu enregistrer.
Quand le disque est plein, tes outils de diagnostic mentent aussi. Un docker compose restart a suffi à débloquer la situation une fois la place libérée, mais il fallait d'abord ne pas croire ce que Docker affichait.
Les valeurs par défaut décident à ta place
C'est le sujet réel de cet incident, et il dépasse largement mon serveur.
Trois défauts se sont combinés :
- Docker ne limite pas les journaux. Sans
max-size, le fichier d'un conteneur grossit sans fin. Pendant la panne, Postgres journalisait son propre échec en boucle — ce qui remplissait le disque, ce qui aggravait l'échec. prunesans-ane supprime presque rien. Le nom de la commande suggère un nettoyage ; le comportement par défaut est de tout garder.- Aucune surveillance de disque n'est activée par défaut sur une installation standard.
Pris isolément, chaque défaut se justifie. Docker ne veut pas perdre tes journaux. prune ne veut pas supprimer une image dont tu pourrais avoir besoin. Combinés sur un VPS de 58 Go qui héberge quatre projets, ils produisent une panne certaine — la seule inconnue étant la date.
Nous héritons en permanence de décisions prises par d'autres, pour des contextes qui ne sont pas les nôtres. Le développeur qui déploie sur son propre serveur devient exploitant, généralement sans l'avoir décidé.
La configuration que je pose maintenant partout
Plafonner les journaux, dans /etc/docker/daemon.json :
{ "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" } }
Trente mégaoctets par conteneur au lieu de l'infini. Suivi de systemctl restart docker.
Nettoyer réellement, dans chaque pipeline :
docker image prune -af --filter "until=168h"
docker builder prune -af --filter "until=168h"
Surveiller le contenu, pas seulement le code HTTP. Mon site répondait 200 avec une page vide pendant neuf jours. Un contrôle de disponibilité classique n'a rien vu. Une vérification du type « la page d'accueil contient-elle au moins une fiche projet ? » aurait détecté la panne en cinq minutes.
Alerter à 80 %, et sur la tendance plutôt que sur le seuil seul : « le disque a pris 5 Go cette semaine » est une information plus utile que « le disque est à 82 % ».
Ce que je ferais différemment dès le départ
Avec le recul, l'erreur de fond n'est pas le -a manquant. C'est d'avoir traité le serveur comme un détail d'exécution.
J'ai soigné mon architecture applicative, mes tests, mes types. Le serveur, lui, a été configuré une fois, en vitesse, puis oublié. Il a tenu quinze mois — assez longtemps pour que je cesse d'y penser, pas assez pour qu'il tienne indéfiniment.
Trois choses que j'aurais dû faire au premier jour :
- Un fichier
INFRA.mddans le dépôt : ce qui tourne, où, avec quelles limites, et ce qu'il faut surveiller. Écrit pour la version de moi qui aura tout oublié. - Une sauvegarde automatisée, pas manuelle. J'ai eu de la chance : mes données étaient intactes. Un PANIC sur écriture de checkpoint est exactement le scénario pour lequel le WAL existe. La prochaine fois, ce sera peut-être un secteur défectueux.
- Un plafond de ressources par projet. Quatre applications qui partagent 58 Go sans quota, c'est une course à celui qui remplira le disque en premier.
Rien de tout cela n'est difficile. C'est simplement invisible tant que ça marche.
Cette panne a aussi révélé une fragilité applicative, que je raconte dans « Mon site a affiché zéro projet pendant neuf jours ». Si tu veux un audit de ce qui casserait silencieusement chez toi, écris-moi.
