← tous les articles

La couche données ne tient plus à un seul nœud : Patroni, et tout se met à flotter

20 juin 2026 · 5 min de lecture

postgrespatronihaute-disponibilitedocker-swarmhomelab

Le billet précédent se terminait sur un aveu : 3 managers, ingress actif/actif, nœuds nomades… mais la couche données restait épinglée à rpi51. Quorum ≠ HA des données. Déplacer ou perdre rpi51 coupait toutes les apps à base de données. C’était le « prochain chantier ». Le voici.

Le principe : surtout pas un FS distribué

Le réflexe pour qu’un volume « suive » un service qui bouge, c’est un système de fichiers réseau partagé (GlusterFS, Ceph…). Pour une base de données, j’ai fait le calcul et il dit non : mettre Postgres sur du FS réseau, c’est échanger la latence locale du NVMe contre la latence réseau et un domaine de panne partagé.

La bonne approche est l’inverse : rendre le service mobile en sortant son état vers une couche déjà répliquée. Deux familles :

  • L’état lourd à fort fsync (les bases) → réplication applicative : un cluster Postgres qui réplique lui-même, sur disque local rapide. C’est Patroni.
  • L’état léger (config, clés, comptes) → on le régénère au démarrage depuis un secret / une base. J’appelle ça l’auth-as-code.

Patroni : chaque base devient un duo

Chaque base applicative est désormais un cluster Patroni à 2 membrespg-a sur rpi51, pg-b sur rpi52 — avec le leader élu via un etcd à 3 membres (un par manager). Devant chaque cluster, un HAProxy qui garde l’alias que l’app utilisait déjà (vw-postgres, n8n-postgres…) et route vers le membre que Patroni déclare leader (health-check = l’API Patroni :8008/leader). Résultat : les chaînes de connexion des apps ne changent pas.

app ──▶ haproxy (alias inchangé) ──▶ pg-a (rpi51) ⇄ pg-b (rpi52)
                  health = /leader        leader élu via etcd (quorum 2/3)

Un déplacement planifié de rpi51 devient un switchover de quelques secondes ; un crash, un failover automatique dès l’expiration du bail etcd. Migration par dump/restore dans des volumes neufs, l’ancien gardé en rollback le temps d’un soak.

Puis : faire flotter tout le reste

Patroni règle les bases. Restait à dé-épingler les apps elles-mêmes (db==trueapps==true). Trois surprises en chemin :

n8n. Je voulais externaliser ses données binaires vers R2 (S3). Sauf que le mode S3 est une fonction sous licence entreprise — le conteneur communautaire la refuse net. Repli sur le mode default : les binaires vivent inline dans les données d’exécution, donc dans le Postgres HA, bornés par la purge à 14 jours. Le volume local devient jetable.

ntfy. Son seul état durable est un auth.db SQLite (utilisateurs + ACL). Piège : la CLI de ntfy ne sait pas créer cet auth.db — seul le serveur le fait au premier lancement. La parade : un provision.sh qui démarre le serveur, attend l’auth.db, puis reconstruit les comptes depuis des secrets Docker à chaque démarrage. Sur un nœud frais, ntfy se reprovisionne à l’identique. Auth-as-code.

Vaultwarden — le gros chantier qui n’existait pas. Je redoutais celui-là : un gestionnaire de mots de passe, avec (croyais-je) des pièces jointes à pousser sur S3 et une clé à manipuler. J’ouvre le volume… et il ne contient que la clé de signature RSA (1,6 Ko), un cache d’icônes jetable, et un config.json. Zéro pièce jointe. Tout le « chantier S3 » était imaginaire. La clé RSA part dans un secret Docker (RSA_KEY_FILENAME pointe dessus), la config était déjà en variables d’env, le volume devient jetable. Au passage, un contre-piège : chez Vaultwarden, config.json écrase les variables d’env (l’inverse de l’intuition) — il a donc fallu le supprimer pour que l’env redevienne la source de vérité. Et perdre la clé RSA ? Ça force une reconnexion de tous, jamais une perte de données : le coffre est chiffré côté client avec la clé maître de chaque utilisateur ; la clé RSA ne signe que les jetons d’auth.

Les drills : la seule preuve qui compte

Une archi HA non testée est une hypothèse. Trois drills :

  1. Déplacement gracieux de rpi51make move bascule les leaders, les apps suivent. ✅
  2. Crash brutal de rpi51 (reboot -f, sans évacuation) — failover etcd, les apps reschedulent. ✅
  3. Crash brutal de rpi52 — symétrique. ✅

Bilan : perdre n’importe lequel des deux nœuds de données, à chaud ou brutalement, ne coupe plus aucune app. Mais les drills ont surtout révélé ce que je ne cherchais pas.

Un SPOF caché. En préparant le crash de rpi52, je découvre que mes deux répliques de ce site s’étaient empilées sur rpi52. La cause : spread: node.id n’est qu’une préférence de placement, et Swarm ne rééquilibre jamais au retour d’un nœud — après les drills précédents, les deux répliques avaient atterri du même côté. Un crash de rpi52 aurait coupé tout le site. Correctif : max_replicas_per_node: 1 (anti-affinité dure). Le drill l’a ensuite validé en direct — le site est resté à 200 pendant toute la fenêtre.

Une fausse panne que je me suis infligée. Pendant le crash, je monitorais en bouclant des curl toutes les 3 secondes. Stickers s’est mis à répondre 429. Panique brève… puis les logs tranchent : zone bot_limit, client = mon IP, user-agent = curl. Mon propre monitoring avait épuisé le rate-limiter anti-bot. La détection de vraie IP marchait parfaitement, les vrais utilisateurs n’ont rien vu. Le rate-limiter faisait son travail — c’est moi qui jouais au bot.


Bilan : la couche données promise au billet précédent est livrée. Chaque base est un cluster Patroni, chaque app flotte, et trois drills le prouvent. Plus aucun point unique de défaillance applicatif : on déplace ou on perd un nœud, le service tient. Le seul état encore épinglé est de l’infra reproductible (Prometheus, Portainer). Le code est public.


📬 La newsletter homelab

Les nouveaux articles + retours d'expérience self-hosting, sans spam. Désinscription en un clic.