Aller au contenu
Post-mortem : quand ha-manager casse Docker — corruption BoltDB en cascade sur mon hôte sysadmin
  1. Posts/

Post-mortem : quand ha-manager casse Docker — corruption BoltDB en cascade sur mon hôte sysadmin

Fabien ALLAMANCHE
Auteur
Fabien ALLAMANCHE
Géomaticien @ Vienne Condrieu Agglomération
Sommaire

Introduction
#

Ce matin-là, tout commence par un geste anodin : redémarrer le LXC 160, mon conteneur sysadmin. Il héberge une petite stack Docker locale (code-server, glances, healthchecks, it-tools, umami, zerobyte) et surtout un accès centralisé, via des montages NFS distants, à ma configuration docker compose de mes autres machines — pratique pour ajuster un réglage depuis VS Code Server sans avoir à se connecter partout. Sauf que le conteneur refuse de repartir depuis l’interface web Proxmox. Premier signe qu’un simple pct start ne suffira pas.

Cet article est un post-mortem à chaud : le diagnostic d’une corruption en cascade de deux bases BoltDB (Docker puis containerd), une fausse piste kernel/ZFS écartée après vérification, une restauration via Proxmox Backup Server, et surtout la vraie cause racine — beaucoup plus prosaïque qu’un bug noyau.
Attention : ce n’est pas un tutoriel idéalisé. Il documente aussi les fausses pistes suivies en cours de route — c’est souvent là que se trouvent les enseignements les plus utiles.

Table des matières
#


Chronologie de l’incident
#

Avant de plonger dans le détail, une vue d’ensemble de la chaîne d’événements — du geste initial jusqu’à la résolution.
flowchart TD
    A["🔧 ha-manager node-maintenance enable morpheus
(avant MAJ Proxmox)"] --> B["📦 Relocalisation forcée du LXC 160
vers neo, puis retour vers morpheus"] B --> C["⬆️ Mise à jour Proxmox 9.2.11
+ reboot manuel (sans HA)"] C --> D["❌ LXC 160 refuse de redémarrer
depuis l'UI Proxmox"] D --> E["💥 dockerd : stack overflow BoltDB
local-kv.db corrompu"] E --> F["🩹 Suppression/recréation de local-kv.db"] F --> G["💥 containerd : panic freepages
meta.db corrompu (page 26)"] G --> H["🧭 Piste kernel/ZFS explorée
puis écartée (zpool status sain)"] H --> I["♻️ Restauration PBS
backup de dimanche soir"] I --> J["✅ Docker + containerd stables"] J --> K["🎯 Cause racine identifiée :
relocalisation HA, pas le kernel"] style D fill:#7f1d1d,stroke:#f87171,stroke-width:2px,color:#fff style E fill:#7f1d1d,stroke:#f87171,stroke-width:2px,color:#fff style G fill:#7f1d1d,stroke:#f87171,stroke-width:2px,color:#fff style I fill:#1e3a8a,stroke:#60a5fa,stroke-width:2px,color:#fff style K fill:#14532d,stroke:#4ade80,stroke-width:2px,color:#fff

Premiers symptômes : Docker qui ne répond plus
#

Le LXC 160 (sysadmin) fait tourner une petite stack Docker Compose locale — code-server, glances, healthchecks, it-tools, umami, zerobyte. Il monte aussi en NFS les répertoires docker-compose.yml de mes autres machines (storage, mediaserver, nextcloud, immich, webapps, frontend, smarthome, webgis, forge…) sous /mnt/docker/appdata/, uniquement pour pouvoir les éditer depuis code-server sans me connecter à chaque hôte séparément. Ces montages ne servent qu’à l’édition ; les stacks elles-mêmes tournent chacune sur leur propre machine.

Après un pct stop --force puis un redémarrage forcé depuis le shell de morpheus, le conteneur redémarre bien — mais docker ps ne répond plus :

systemctl status docker
● docker.service - Docker Application Container Engine
     Active: activating (start) since ...
   Main PID: 4853 (dockerd)

Le service reste bloqué en activating, en boucle de redémarrage (Scheduled restart job, restart counter is at 17).

Info : environnement — Debian 13, Docker CE 29.7.2, containerd v2.3.3, LXC non privilégié avec nesting=1,fuse=1, disque racine sur ZFS mirroré (rpool).

dockerd panique : corruption du local-kv.db
#

Un examen plus large du journal (journalctl -u docker -n 300) révèle la vraie cause, masquée derrière des dizaines de goroutines en attente :

runtime: goroutine stack exceeds 1000000000-byte limit
fatal error: stack overflow

goroutine 1 gp=... [running, locked to thread]:
go.etcd.io/bbolt.(*Bucket).pageNode(...)
...3947407 frames elided...
github.com/moby/moby/v2/daemon/libnetwork/internal/kvstore/boltdb.(*BoltDB).List(...)

Signature limpide : un cycle dans le B-tree BoltDB du fichier de configuration réseau local de Docker. Le driver bridge de libnetwork tente de charger l’état des réseaux au démarrage, boucle indéfiniment sur des pages qui se référencent en cercle, jusqu’à faire exploser la pile Go.

Correction — sauvegarde puis suppression du fichier corrompu, pour forcer Docker à en recréer un vierge :

sudo systemctl stop docker docker.socket
sudo mv /var/lib/docker/network/files/local-kv.db \
        /var/lib/docker/network/files/local-kv.db.corrupt
sudo systemctl start docker.socket
sudo systemctl start docker
systemctl status docker
Succès (partiel) : dockerd passe en active (running), API listen on /run/docker.sock. Mais démarrer les conteneurs échoue avec dial unix:///run/containerd/containerd.sock: timeout.

containerd panique à son tour : le meta.db
#

/run/containerd/containerd.sock n’existe même pas — aucun process containerd ne tourne. Direct sur son propre journal :

sudo journalctl -u containerd -n 100 --no-pager
panic: freepages: failed to get all reachable pages (page 26: multiple references (stack: [26]))

goroutine 34 [running]:
go.etcd.io/bbolt.(*DB).freepages.func2()
        .../vendor/go.etcd.io/bbolt/db.go:1248 +0x8d

Même famille de corruption BoltDB, cette fois sur le fichier de métadonnées de containerd — celui qui référence images et conteneurs connus :

sudo systemctl stop docker docker.socket containerd
sudo mv /var/lib/docker/containerd/daemon/io.containerd.metadata.v1.bolt/meta.db \
        /var/lib/docker/containerd/daemon/io.containerd.metadata.v1.bolt/meta.db.corrupt
sudo systemctl start containerd

Problème : le panic se reproduit à l’identique (même page 26) après la suppression — alors qu’un fichier BoltDB fraîchement recréé ne devrait pas contenir de page 26 dès le premier accès. De quoi douter d’une simple corruption ponctuelle.


La fausse piste kernel/ZFS
#

Coïncidence troublante : cette panne survient deux jours après une mise à jour de Proxmox vers la version 9.2.11, avec passage au kernel 7.0.14-14-pve. Deux bases BoltDB indépendantes corrompues au même moment, sur un système qui utilise mmap intensivement (comme le fait bbolt) — l’hypothèse d’une régression kernel/ZFS/mmap semblait plausible.

Vérifications menées avant de conclure :

zpool status -v rpool
  pool: rpool
 state: ONLINE
  scan: scrub repaired 0B in 00:11:43 with 0 errors on Sun Aug 9 00:35:44 2026
errors: No known data errors

Aucune erreur de checksum, aucun événement I/O suspect côté ZFS. Le pool est parfaitement sain.

Info : un proxmox-boot-tool kernel pin <ancien-kernel> --next-boot était prêt pour tester un retour à 6.17.13-21-pve. Ce test n’a finalement pas été nécessaire — la vraie cause a été trouvée avant.

Restauration via Proxmox Backup Server
#

Plutôt que de continuer à tirer sur le fil kernel/ZFS sans certitude, la solution la plus sûre et la plus rapide était de restaurer le LXC depuis une sauvegarde PBS antérieure à l’incident — dimanche soir, avant toute manipulation HA ou mise à jour :

pct stop 160

Puis restauration depuis l’interface Proxmox (Backup → sélection du snapshot → Restore, avec écrasement du conteneur existant).

Succès : après restauration, dockerd et containerd démarrent proprement, sans aucun panic BoltDB. La stack locale (docker compose up -d) redémarre normalement.

La vraie cause racine : ha-manager node-maintenance
#

C’est en reconstituant la chronologie complète que la véritable origine apparaît. Avant la mise à jour Proxmox, un ha-manager crm-command node-maintenance enable morpheus avait été lancé pour préparer le redémarrage du nœud — dans l’idée, erronée, que le mode maintenance n’allait pas déplacer les LXC.

Or c’est précisément l’inverse : node-maintenance a pour but explicite d’évacuer les ressources HA vers les autres nœuds disponibles. Pour un conteneur LXC, cette relocalisation déclenche un arrêt normal — mais avec timeout. Passé ce délai, Proxmox force l’arrêt.

Le LXC 160, avec sa stack Docker locale et ses montages NFS multiples à démonter proprement, a de bonnes chances d’avoir dépassé ce timeout — d’autant que la maintenance a ensuite été désactivée pour « tout récupérer », doublant le nombre d’allers-retours stop/start. Résultat : au moins un de ces cycles a probablement interrompu une transaction BoltDB en plein vol, sur Docker ou sur containerd.

graph LR
    A["node-maintenance enable"] --> B["Évacuation HA du LXC 160
vers neo"] B --> C["Arrêt avec timeout
(montages NFS + stack Docker locale)"] C --> D["⚠️ Timeout dépassé →
arrêt forcé possible"] D --> E["node-maintenance disable"] E --> F["Retour vers morpheus
(second cycle stop/start)"] F --> G["💥 Transaction BoltDB
interrompue"] style D fill:#92400e,stroke:#fbbf24,stroke-width:2px,color:#fff style G fill:#7f1d1d,stroke:#f87171,stroke-width:2px,color:#fff

reboot simple vs node-maintenance : ce qui change vraiment
#

Les deux mécanismes sont souvent confondus alors qu’ils se comportent très différemment vis-à-vis des ressources HA :

Mécanisme Comportement sur les CT HA Risque pour un hôte Docker
reboot simple (sans maintenance) Le LRM local reçoit l’ordre de freeze : arrêt gracieux (vzshutdown) avec attente, puis reprise sur le même nœud après redémarrage Faible — pas de relocalisation, pas de timeout inter-nœud
ha-manager crm-command node-maintenance enable Évacuation des ressources HA vers un autre nœud, arrêt avec timeout puis démarrage ailleurs Élevé si le CT met du temps à s’arrêter (montages réseau, conteneurs multiples)

Un simple reboot du nœud suffit largement pour charger un nouveau kernel sans bouger les conteneurs — le mécanisme freeze/thaw de pve-ha-lrm s’en charge tout seul, sans risque d’évacuation. node-maintenance n’est utile que pour une vraie maintenance matérielle nécessitant de vider le nœud.

Attention : après un reboot, vérifiez que pve-ha-lrm est bien revenu actif (systemctl status pve-ha-lrm). Sur cette version de Proxmox, il n’est pas garanti de redémarrer automatiquement — un service disabled au niveau systemd, piloté normalement par le watchdog-mux, peut rester inactive (dead) après reboot et laisser les ressources HA en freeze jusqu’à un démarrage manuel.

Leçons apprises
#

Ce post-mortem a permis de clarifier un point que je maîtrisais mal : la différence entre un simple reboot HA-aware et une vraie mise en maintenance.

Ce que je retiens :

  • Pour recharger un kernel sans déplacer les CT, un reboot simple suffit — pas besoin de node-maintenance.
  • node-maintenance évacue réellement les ressources HA, avec un risque de timeout/arrêt forcé pour un CT lourd (nombreux montages, plusieurs services à stopper).
  • Une corruption BoltDB (Docker local-kv.db, containerd meta.db) après un arrêt brutal se corrige en général par suppression/recréation du fichier — mais ne garantit pas contre une récidive si la cause n’est pas traitée.
  • Face à une double corruption qui persiste après correction manuelle, restaurer depuis une sauvegarde antérieure (PBS) reste souvent plus rapide et plus sûr que de continuer à chasser une hypothèse incertaine (ici, le kernel/ZFS, écarté après vérification du pool).
  • Vérifier pve-ha-lrm après chaque reboot d’un nœud HA — son redémarrage automatique n’est pas garanti sur toutes les versions.
Astuce : si une vraie évacuation de nœud est nécessaire, arrêtez manuellement et proprement les CT lourds (pct shutdown) avant d’activer node-maintenance, plutôt que de laisser HA gérer l’arrêt sous contrainte de timeout.

Articles connexes