~/mesha-vegas$
$ cd ../

2026-08-01

Mon site a affiché zéro projet pendant neuf jours et je ne l'ai pas vu

Un disque plein, un Postgres en PANIC, et un portfolio qui affichait « 0 apps livrées » sans lever la moindre erreur. Autopsie d'une panne silencieuse et de ce qu'elle m'a appris sur la dépendance à une base.

  • #resilience
  • #supabase
  • #postgres
  • #devops
  • #incident
  • #architecture
$8 min read28 reads
Mon site a affiché zéro projet pendant neuf jours et je ne l'ai pas vu

Le 31 juillet à 15h44, mon serveur a écrit cette ligne :

PANIC: could not write to file "pg_logical/replorigin_checkpoint.tmp":
No space left on device

Le 1ᵉʳ août, mon portfolio affichait fièrement : 0 apps livrées · 0 expériences · Aucun commit pour ce filtre.

Entre les deux, neuf jours. Neuf jours pendant lesquels toute personne arrivant sur mon site professionnel voyait une page vide. Aucune alerte, aucun mail, aucune page d'erreur. Juste un site parfaitement fonctionnel qui ne disait plus rien de moi.

La panne silencieuse est la pire

Un 500 est brutal mais honnête. Tu le vois, tu le corriges.

Mon code faisait ceci :

const { data, error } = await supabase.from("projects").select("*");
if (error) {
  console.error("listProjects", error);
  return { projects: [] };   // ← le crime est ici
}

En apparence, c'est de la bonne pratique : on gère l'erreur, on ne fait pas planter la page. En réalité, on a transformé une panne d'infrastructure en résultat métier valide. Le composant reçoit un tableau vide et fait consciencieusement son travail : il affiche « aucun projet ».

Le console.error part dans les journaux d'un conteneur que personne ne lit. La page renvoie 200. Le monitoring de disponibilité — s'il existait — serait vert.

C'est le piège du repli silencieux : un tableau vide est indistinguable d'un tableau vide légitime. Rien dans le type ne dit « ceci est un échec ».

L'enquête

Les symptômes vus de l'extérieur formaient un tableau incohérent :

/auth/v1/health     → 200   GoTrue en pleine forme
/storage/v1/status  → 200   stockage OK
/rest/v1/**         → 503   PGRST002

Deux services sur trois répondaient. Seul PostgREST échouait, avec un message inhabituel : « Could not query the database for the schema cache. Retrying. »

Les journaux du conteneur ont donné la réponse en une ligne. Disque plein. Postgres n'arrive plus à écrire son checkpoint, il panique, il redémarre en mode récupération, il repanique. Boucle infinie.

Le plus vicieux : PostgREST, lui, tournait. Il acceptait les connexions, il répondait poliment, il expliquait même qu'il réessayait. Un service qui répond correctement qu'il est cassé est plus difficile à diagnostiquer qu'un service mort.

Le coupable était dans ma CI depuis le début

Le disque faisait 58 Go. docker system df a livré le verdict en une seconde :

Images        41 total, 19 actives    38.43 GB   ← 20.49 GB récupérables
Build Cache   28 entrées, 0 active     8.93 GB

47 Go d'images Docker sur 57 utilisés.

D'où venaient-elles ? De mon propre pipeline de déploiement. Chaque build tague l'image avec le SHA du commit :

docker build -t "$IMAGE_NAME:$CI_COMMIT_SHA" -t "$IMAGE_NAME:latest" .

Et à la fin, le nettoyage :

docker image prune -f

Sans -a. Cette option, absente, change tout : prune -f ne supprime que les images « dangling », celles qui n'ont plus aucun tag. Or les miennes en ont un — le SHA du commit. Elles ne sont donc jamais supprimées.

Une image par commit. Quatre projets déployés sur le même serveur. Trois mois. J'avais huit versions de la même application à 1,45 Go chacune.

Le mécanisme est presque élégant dans sa perversité : plus je travaillais, plus je remplissais le disque. Un pipeline sain aurait été inoffensif ; c'est mon rythme de livraison qui a armé la bombe.

Sept jours d'accumulation, et un carton rouge

En football, tu ne prends pas un carton rouge d'un coup. Tu prends un jaune à la 20ᵉ, un autre à la 70ᵉ, et c'est le second qui te sort. Aucun des deux n'était grave. C'est le cumul.

Mon disque a fonctionné pareil. Chaque déploiement ajoutait quelques centaines de mégaoctets — invisible, indolore. Puis un jour le pourcentage passe de 99 % à 100 % et tout s'arrête d'un coup.

Les systèmes qui dégradent linéairement puis cassent brutalement sont les plus dangereux, parce que l'expérience quotidienne ne prépare à rien. Il n'y a pas eu de « ça devient lent » avant le « ça ne marche plus ».

Ce que j'ai changé

1. Le contenu ne dépend plus de la base

C'est le changement de fond. Le parcours et les études de cas de ce site vivent maintenant dans le dépôt, en TypeScript, versionnés avec le code.

Les lectures interrogent toujours la base d'abord, et retombent sur ce contenu si elle ne répond pas :

try {
  const { data, error } = await supabase.from("projects").select(...);
  if (error) throw new Error(error.message);
  if (data?.length) return { projects: data };
} catch (err) {
  console.error("[listProjects] base indisponible, repli local:", err);
}
return { projects: localProjectRows(lang) };

Deux différences avec l'ancien code, toutes deux essentielles :

  • On throw sur erreur au lieu de retourner un tableau vide. L'échec reste un échec.
  • On teste data?.length : une réponse vide est traitée comme une non-réponse, pas comme une vérité.

Le résultat, testé contre la base réellement en panne : le site affiche l'intégralité de son contenu. La panne devient invisible pour le visiteur et bruyante dans les journaux. C'est le bon sens de la relation.

2. Le contenu critique n'a rien à faire dans une base

Voilà la leçon inconfortable. Mon parcours professionnel change trois fois par an. Mes études de cas, deux fois. Pourquoi diable étaient-ils dans une base de données interrogée à chaque requête ?

Parce qu'un tableau de bord d'administration, c'est satisfaisant à construire. Parce que « les données vont en base », c'est le réflexe. Personne ne s'est demandé si le contenu quasi statique d'un site vitrine méritait une dépendance réseau à chaque affichage.

Le critère que j'applique maintenant : si le contenu change moins souvent qu'on ne déploie, il appartient au dépôt. Un commit est un mécanisme de publication parfaitement valable — avec revue de code, historique et retour arrière gratuits.

3. Le pipeline nettoie vraiment

docker image prune -af --filter "until=168h"
docker builder prune -af --filter "until=168h"

Le -a fait le travail. Le filtre garde une semaine d'historique pour pouvoir revenir en arrière rapidement.

Et le plafond sur les journaux, qui n'existait pas non plus :

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

Par défaut, Docker laisse les journaux d'un conteneur grossir sans aucune limite. Mes conteneurs Supabase en avaient accumulé près d'un gigaoctet, dont une bonne partie écrite pendant la panne — Postgres qui journalise en boucle son propre échec, ce qui remplit le disque, ce qui aggrave l'échec.

Les valeurs par défaut ne sont pas neutres

C'est le vrai sujet, et il dépasse mon incident.

Docker choisit des journaux illimités. prune sans -a choisit de tout garder. Supabase choisit d'être toujours joignable. Ces défauts sont raisonnables pris isolément — et catastrophiques une fois combinés sur un VPS de 58 Go.

Nous héritons de dizaines de décisions prises par d'autres, pour des contextes qui ne sont pas le nôtre. Le développeur qui déploie sur son propre serveur assume, souvent sans le savoir, un rôle d'exploitant.

Ce que je fais désormais sur toute infrastructure que je gère :

  • Une alerte à 80 % de disque, pas à 95 %. À 95 %, tu répares dans l'urgence ; à 80 %, tu planifies.
  • Une supervision qui vérifie le contenu, pas le code HTTP. « La page d'accueil contient-elle au moins une fiche projet ? » aurait détecté ma panne en cinq minutes. Un simple contrôle de disponibilité l'a manquée pendant neuf jours.
  • Une sauvegarde avant toute intervention. J'ai pris un dump complet avant de toucher quoi que ce soit. Il faisait 1,2 Mo. Mon site était à l'arrêt à cause de 47 Go d'images Docker, alors que la totalité de mes données tenait dans un fichier plus léger qu'une photo.

Ce dernier chiffre m'a fait réfléchir plus que tout le reste.


Le repli décrit ici est en place sur ce site — le détail est sur sa fiche projet. Si tu veux un audit de ce qui casserait silencieusement chez toi, écris-moi.