Aller au contenu principal

Maintenance et dépannage

Procédures de maintenance courante et resolution des problèmes rencontres.


Mises a jour​

DietPi​

# NE PAS lancer directement — voir l'avertissement ci-dessous
systemd-run --unit=dietpi-maj --collect \
--setenv=DEBIAN_FRONTEND=noninteractive \
--setenv=APT_LISTBUGS_FRONTEND=none \
/boot/dietpi/dietpi-update 1
journalctl -u dietpi-maj -f # suivre
danger
dietpi-update lance un apt upgrade COMPLET et peut tuer sa propre session

Vécu le 2026-08-20 en montant de v10.2.3 à v10.6.2.

Deux surprises qui se combinent. D'abord, la phase de patch de dietpi-update lance un apt upgrade sur tous les dépôts, y compris les dépôts tiers — ici 254 Mo : docker-ce, containerd.io, alloy, trivy, tailscale. Le réglage CONFIG_CHECK_APT_UPDATES ne protège pas de ça : il ne gouverne que la branche « aucune mise à jour DietPi disponible ».

Ensuite, le postinst de tailscale redémarre tailscaled. Comme l'accès à penny passe uniquement par Tailscale, la session tombe et emporte dietpi-update par SIGHUP — en pleine phase Setting up.

D'où le systemd-run ci-dessus : il place la mise à jour hors du cgroup de la session, qui peut alors mourir sans rien interrompre.

Un système peut tourner parfaitement avec un dpkg incohérent

Après l'interruption : docker --version annonçait la nouvelle version, les 22 conteneurs tournaient, systemctl --failed était vide. Et pourtant docker-ce et trivy étaient en iU (dépaquetés, postinst jamais joué) et tailscale en iF. Les binaires sont en place, ce sont les scripts de configuration qui manquent.

Aucun contrôle habituel ne voit ça. Le seul qui le détecte :

dpkg -l | awk '$1 !~ /^(ii|hi|rc)/ && NR>5 {print $1, $2}'

hi = held (normal pour rpi-eeprom), rc = désinstallé avec configs résiduelles (normal aussi — ne pas le compter comme une anomalie).

Reprise, toujours détachée :

systemd-run --unit=reprise --collect bash -c \
'dpkg --configure -a && apt-get -f install -y && /boot/dietpi/dietpi-update 1'
L'autopurge supprime le noyau de repli

dietpi-update a purgé linux-image-6.12.75 et son initrd, ne laissant qu'un seul noyau installé. Sur une machine au matériel marginal, c'est un filet en moins. Vérifier après coup, et réinstaller au besoin :

ls -d /usr/lib/modules/*/ # doit en lister au moins deux
apt-get install --no-install-recommends linux-image-<version>+rpt-rpi-v8

Réinstaller un ancien noyau ne touche pas au démarrage : le hook /etc/kernel/postinst.d/z50-raspi-firmware ne recopie vers /boot/firmware/kernel8.img que si le noyau configuré est le plus récent de /boot. Le vérifier quand même par le hash :

sha1sum /boot/firmware/kernel8.img /boot/vmlinuz-*

Basculer sur le noyau de repli​

cp /boot/vmlinuz-6.12.75+rpt-rpi-v8 /boot/firmware/kernel8.img && sync
systemctl reboot

Pour revenir, même commande avec le noyau voulu. /sbin/shutdown échoue en « Failed to connect to bus » sur cette machine : utiliser systemctl reboot.

Docker images​

cd /mnt/ssd/config
docker compose pull # Telecharge les nouvelles images
docker compose up -d # Redeploy avec les nouvelles images
docker image prune -f # Supprime les anciennes images
attention
Les images sont épinglées : pull seul ne met rien à jour

Depuis le retrait de Watchtower (2026-07-06), plus aucune mise à jour n'est automatique et chaque image est épinglée par @sha256 dans le compose. Un docker compose pull ne ramène donc pas une nouvelle version : il retélécharge le digest déjà épinglé. Pour mettre à jour, il faut changer le @sha256 dans docker-compose.yml, puis pull et up -d. Le timer mensuel digest-drift-check signale quand l'amont a bougé.

danger
up -d déploie l'état du fichier, pas l'état du dernier commit

Vérifiez git status avant : des modifications de docker-compose.yml non commitées partiraient en production avec cette commande.

Firmware RPi​

rpi-eeprom-update # Verifie les mises a jour firmware
rpi-update # Met a jour le firmware (attention, peut casser)
Attention

rpi-update installe le firmware bleeding-edge. Préférer apt upgrade pour les mises a jour stables du kernel.


Vérification post-reboot​

Checklist après un reboot :

  • SSD monte sur /mnt/ssd en rw
  • pcie_aspm=off actif (cat /sys/module/pcie_aspm/parameters/policy → [off])
  • USB autosuspend a -1 (cat /sys/bus/usb/devices/*/power/autosuspend)
  • Quirks USB appliques (dmesg | grep "Quirks match")
  • Tous les containers Docker up (docker ps)
  • Pas de throttling (vcgencmd get_throttled = 0x0)
  • Temperature normale (vcgencmd measure_temp < 70°C)

SSD Argon Forty — déconnexions USB​

Symptomes​

  • EXT4-fs (sdX): shut down requested (2) dans dmesg
  • usb 2-2: USB disconnect suivi de re-enumeration
  • device offline error, dev sdX
  • Services Docker inaccessibles (Docker root sur le SSD)
  • Le device change de nom (sda → sdb → sdc) après chaque reconnexion

Cause racine​

Le bridge USB-SATA ASMedia ASM1156 (Argon Forty) est sensible a :

  1. PCIe ASPM — le mode powersave fait tomber le lien PCIe du controleur VL805 USB
  2. Connecteur USB interne — le dongle entre le socle SSD et le board RPi peut avoir un mauvais contact
  3. Port USB 3.0 — "Cannot enable. Maybe the USB cable is bad?" → fallback USB 2.0

Fixes appliques​

Dans /boot/firmware/cmdline.txt :

pcie_aspm=off usbcore.autosuspend=-1 usb-storage.quirks=174c:1156:u
ParamètreEffet
pcie_aspm=offDésactivé le power management PCIe (fix principal)
usbcore.autosuspend=-1Désactivé l'USB autosuspend
usb-storage.quirks=174c:1156:uForce usb-storage au lieu de UAS pour le bridge ASMedia

Procédure de recovery manuelle​

# 1. Verifier l'etat
dmesg | tail -20
lsblk
mount | grep ssd

# 2. Stopper Docker
systemctl stop docker docker.socket

# 3. Demonter le mount mort
umount -l /mnt/ssd

# 4. Verifier le filesystem
fsck.ext4 -y /dev/sdXX # adapter au device actuel (lsblk)

# 5. Remonter
mount /mnt/ssd

# 6. Relancer Docker
systemctl start docker

Auto-recovery (homelab_monitor.sh)​

Le script homelab_monitor.sh intégré une recovery automatique en cas de déconnexion SSD :

  1. Stop Docker en premier (libéré les file handles sur le SSD)
  2. Double unmount (umount -f + umount -l) pour nettoyer les mounts stale
  3. Attend que le device reapparaisse par UUID jusqu'a 60s + 3s de stabilisation
  4. fsck.ext4 -y sur le nouveau device (pas l'ancien)
  5. Vérifie le code retour fsck (abort si >= 4)
  6. Remonte /mnt/ssd via fstab (UUID)
  7. Redémarre Docker, attend 10s pour les containers
  8. Notification ntfy "SSD RECOVERED" ou "RECOVERY FAILED"
Changement de device name (sda → sdb)

Le bridge ASMedia re-enumere le SSD avec un nouveau nom après chaque déconnexion (sda → sdb → sdc). La recovery utilisé l'UUID (pas le nom de device) pour retrouver le SSD quel que soit son nouveau nom. Le fstab utilisé aussi l'UUID.

Rate limit : max 3 tentatives par heure. Si les 3 echouent, alerte "SSD Recovery EPUISE — intervention manuelle requise."

Investigation historique​

Le support Argon a recommandé :

  1. Tester avec un cable USB 3.0 A-A court au lieu du dongle intégré Teste — même problème de déconnexion qu'avec le dongle
  2. Tester avec un SSD différent (fanxiang S201 128 Go commande)
  3. Vérifier les logs pour ecarter un problème logiciel Éliminé — aucun processus particulier au moment du disconnect

Conclusion provisoire : le bridge ASMedia ASM1156 est la cause, ni le cable ni le dongle. Le SSD de remplacement sera le test definitif.

Données SMART : UDMA_CRC_Error_Count = 4 (erreurs de communication SATA), pas de secteurs realloues.


Proxmox VE 9 — installation sur eMMC​

Symptome​

L'installeur Proxmox ne propose pas le device eMMC (mmcblk0) comme cible d'installation.

Cause​

Proxmox ne supporte pas les devices mmcblk dans sa logique de partitionnement.

Workaround​

Patcher l'installeur avant de lancer l'installation. Voir le guide complet.


Proxmox VE 9 — repos et popup​

"No valid subscription"​

Popup a chaque connexion a l'interface web.

Fix : le script proxmox-post-install.sh patche le fichier JS de l'interface web et redémarre pveproxy.

"Some suites are misconfigured"​

Les fichiers .sources enterprise sont encore présents.

Fix : le script supprimé les fichiers /etc/apt/sources.list.d/pve-enterprise.sources et ceph.sources.

"Warning: old suite bookworm"​

Un fichier .list legacy avec la suite bookworm au lieu de trixie.

Fix : le script supprimé /etc/apt/sources.list.d/pve-no-subscription.list (ancien format).


Docker containers — DNS interne et OIDC​

Symptome​

Les containers ne peuvent pas résoudre *.home.gabin-simond.fr (ex: auth.home.gabin-simond.fr). Erreur typique : dial tcp: lookup auth.home.gabin-simond.fr on 127.0.0.11:53: no such host

Cause​

Les containers sur le réseau Docker proxy utilisent le DNS Docker interne (127.0.0.11) qui ne connait pas les rewrites AdGuard.

Fix​

Ajouter dns: 192.168.1.28 dans le docker-compose.yml pour chaque container qui a besoin de résoudre des domaines locaux. Au 2026-11-04, seul homepage le porte dans le compose.

services:
mon-service:
dns:
- 192.168.1.28

Authelia — redirect_uri mismatch​

Symptome​

Erreur invalid_request : The 'redirect_uri' parameter does not match any of the OAuth 2.0 Client's pre-registered 'redirect_uris'.

Cause​

L'URI de callback du service ne correspond pas exactement a ce qui est configuré dans Authelia. Attention aux :

  • Trailing slash (/ vs pas de /)
  • Chemins spécifiques (/api/oauth2-redirect, /identity/connect/oidc-signin)

Diagnostic​

docker logs authelia | grep "redirect_uri"

Le log indiqué l'URI attendue par le client → copier cette URI exacte dans la config Authelia.

URIs correctes par service​

Serviceredirect_uri
Proxmoxhttps://galahad.home.gabin-simond.fr (et lancelot)
Grafanahttps://logs.home.gabin-simond.fr/login/generic_oauth
PBShttps://backup.home.gabin-simond.fr
Pulsehttps://pulse.home.gabin-simond.fr/api/oidc/callback (et deux variantes)
Forgejohttps://git.home.gabin-simond.fr/user/oauth2/authelia/callback
Outlinehttps://wiki.home.gabin-simond.fr/auth/oidc.callback

Relevé dans authelia/configuration.yml le 2026-11-04 ; c'est ce fichier qui fait foi.


Services inaccessibles via Tailscale VPN​

Symptome​

Certains services (ex: auth.home.gabin-simond.fr, vault.home.gabin-simond.fr) ne chargent pas depuis un client Tailscale, alors qu'ils fonctionnent en local.

Cause​

Des DNS Rewrites statiques dans AdGuard (Filters > DNS Rewrites) ecrasent les user_rules conditionnelles. Les rewrites statiques sont appliquees en premier et renvoient toujours l'IP LAN (192.168.1.28), même aux clients Tailscale qui ont besoin de l'IP Tailscale (100.97.239.90).

Fix​

Supprimer toutes les entrees dans Filters > DNS Rewrites pour les domaines *.home.gabin-simond.fr. Le wildcard dans user_rules géré déjà tous les sous-domaines avec le bon routage conditionnel (LAN vs Tailscale).

Voir Comment fonctionne le DNS pour le détail des règles.


Tailscale SSH — atterrissage dans le container Alpine au lieu du RPi​

Symptôme​

En se connectant via ssh root@homelab (port 22), le shell affiche :

  • Motd Alpine Linux (setup-alpine, wiki.alpinelinux.org)
  • Shell ash au lieu de bash
  • Historique .ash_history dans /root
  • Utilisateur = identité Tailscale (ex: gabin-simond) et non root
  • Aucune trace de DietPi, Docker ou des services du homelab

Cause​

L'image Docker tailscale/tailscale est basée sur Alpine Linux. Quand Tailscale SSH est géré par le container (et non par le daemon host), la connexion ssh root@homelab atterrit dans le container Alpine — pas sur le RPi.

Le container Tailscale intercepte la connexion sur le port 22 Tailscale et mappe l'identité Tailscale à un utilisateur local du container.

Fix — contourner le container via OpenSSH​

Utiliser l'IP Tailscale du RPi avec le port SSH custom (OpenSSH du host, pas Tailscale SSH) :

ssh -p 2806 [email protected]

Dans Termius, modifier le host :

ChampValeur incorrecteValeur correcte
IP or Hostnamehomelab100.97.239.90
Port22 (Default)2806
Usernamerootroot
Auth—Clé SSH

Cette connexion va directement sur OpenSSH de DietPi via le tunnel WireGuard Tailscale, sans passer par le container.


Tailscale SSH — shell minimaliste à la connexion​

Symptôme​

En se connectant via ssh root@homelab (Tailscale SSH), le shell est différent de celui obtenu via OpenSSH (ssh -p 2806 [email protected]) :

  • Pas de bannière DietPi
  • PATH incomplet (commandes introuvables)
  • Prompt basique sans couleurs
  • Variables d'environnement manquantes (LANG, TERM, etc.)

Cause​

Tailscale SSH est une implémentation SSH distincte d'OpenSSH. Il n'utilise pas /etc/ssh/sshd_config et n'invoque pas le shell comme un login shell. Concrètement :

MécanismeOpenSSH (port 2806)Tailscale SSH
/etc/profileChargé (login shell)Non chargé
~/.bash_profileChargéNon chargé
PAM / pam_execActif (DietPi banner)Non actif
/etc/ssh/sshrcExécutéNon exécuté
Mode check (MFA)NonValidation navigateur

Le shell démarre donc en mode non-interactif minimal, sans l'environnement habituel de DietPi.

Fix — forcer un login shell​

Option 1 : Termius (mobile)

Dans les paramètres du host Termius :

  1. Tap Startup Snippet → créer un nouveau snippet
  2. Contenu du snippet : . /etc/profile; . ~/.bashrc
  3. Sélectionner ce snippet dans les paramètres du host → sauvegarder

C'est la méthode recommandée depuis mobile — aucune commande à retenir, actif à chaque connexion.

attention
Ne pas utiliser exec bash -l

exec remplace le shell courant par le nouveau processus. Quand bash -l termine son initialisation sans commande interactive, la session se ferme immédiatement. Sourcer les fichiers directement (. /etc/profile) charge l'environnement sans remplacer le shell.

Option 2 : à la connexion (terminal classique)

ssh root@homelab -t 'bash -l'

Le flag -t force un pseudo-TTY et -l invoque bash comme login shell — identique à une connexion OpenSSH classique.

Option 3 : alias permanent (côté client desktop)

Dans ~/.ssh/config sur la machine cliente :

Host homelab
RequestTTY yes
RemoteCommand bash -l

Vérification​

Après correction, les deux méthodes doivent donner le même résultat :

ssh root@homelab "echo \$SHELL; echo \$PATH"
ssh -p 2806 [email protected] "echo \$SHELL; echo \$PATH"

PVE — page blanche / chargement infini (Trixie)​

Symptome​

L'interface web Proxmox VE (galahad.home.* / lancelot.home.*) affiche une page blanche ou tourne a l'infini.

Causes possibles​

1. DNS pointe vers l'IP directe Proxmox au lieu de Traefik

dig galahad.home.gabin-simond.fr @192.168.1.28
# Si renvoie 192.168.1.18 au lieu de 192.168.1.28 → le navigateur
# essaie :443 sur Proxmox (qui n'ecoute que sur :8006) → timeout

Fix : les rewrites AdGuard doivent pointer vers penny (192.168.1.28) pour que Traefik proxy vers :8006.

2. Fichier ExtJS manquant (symlink ext6-all.js)

Proxmox 9 sur Trixie : libjs-extjs installe ext-all.js mais pve-manager cherche ext6-all.js.

# Sur chaque node :
cd /usr/share/javascript/extjs/
ln -sf ext-all.js ext6-all.js
ln -sf ext-all-debug.js ext6-all-debug.js

3. Security headers incompatibles (CSP, COOP)

Content-Security-Policy ou Cross-Origin-Opener-Policy: same-origin appliques globalement cassent ExtJS + WebSocket. Les headers de sécurité ne doivent PAS être appliques aux routes PVE.


Stack down après docker compose down + reboot​

Symptome​

Après un reboot (ou un docker compose down suivi d'une session qui n'a pas pu up), aucun container ne remonte même si Docker tourne :

docker ps # vide
docker ps -a # tous Exited (0) il y a N heures
systemctl is-active docker # active

Cause racine​

La restart policy du compose est unless-stopped. Docker n'auto-redémarre pas les containers arretes via docker stop ou docker compose down — même a travers un reboot daemon. Seuls les containers crashes (restart: always) ou arretes par panne remontent.

C'est voulu (sécurité contre boucle de redémarrage), mais ca veut dire qu'un down oublie = stack morte jusqu'au prochain up.

Auto-repair depuis 2026-04-19

check_docker_autorepair dans homelab_monitor.sh détecté stack vide + daemon actif et lance docker compose up -d après 2 min. Circuit breaker 3 tentatives / 24h : au 3e échec ntfy urgent "autorepair-capped" et stop. Reset : rm /var/lib/homelab_monitor/autorepair-docker-attempts.

Fix​

cd /mnt/ssd/config/docker && docker compose up -d

Attendre 15-30 s et vérifier healthchecks :

docker ps --format 'table {{.Names}}\t{{.Status}}'

Prevention​

  • Toujours terminer une session de maintenance par docker compose up -d avant de fermer le terminal.
  • En cas de session interrompue (outil bash cassé, etc.), noter l'état avec /checkpoint pour que la session suivante sache qu'il faut redémarrer.
  • Utiliser homelab_monitor.sh (hook systemd) pour alerter si docker ps renvoie vide après boot.

CrowdSec — crash-loop "read-only file system" sur local_api_credentials.yaml​

Symptome​

Error: failed copying from /tmp/tempXXXXX to /etc/crowdsec/local_api_credentials.yaml:
open /etc/crowdsec/local_api_credentials.yaml: read-only file system

Container crowdsec en état Restarting (1) en boucle, même après nettoyage du volume crowdsec-data.

Cause​

L'entrypoint CrowdSec reecrit local_api_credentials.yaml et online_api_credentials.yaml a chaque boot (config_set sur machine id/URL), même quand la machine est déjà registered. Si le bind-mount est en :ro, le mv /tmp/xxx → /etc/crowdsec/... échoué.

Typique quand on activé le declassement sops→tmpfs et qu'on mount les credentials read-only par reflex sécurité.

Fix​

Monter les deux fichiers en :rw :

volumes:
- /run/homelab/crowdsec/online_api_credentials.yaml:/etc/crowdsec/online_api_credentials.yaml:rw
- /run/homelab/crowdsec/local_api_credentials.yaml:/etc/crowdsec/local_api_credentials.yaml:rw

Puis :

cd /mnt/ssd/config/docker && docker compose up -d crowdsec

Pourquoi c'est safe​

  • Chiffrement at-rest : les fichiers sont stockes chiffrés (sops) dans le repo config. Le .yaml en clair ne vit qu'en RAM sur /run/homelab/ (tmpfs).
  • Tmpfs runtime : pas de persistance sur disque, efface au shutdown.
  • Scope limité : seul le container crowdsec peut ecrire sur son propre bind-mount, pas d'escalade.

Les fichiers sops contiennent l'ID machine + une URL — sensibles mais pas aussi critiques qu'une clé privee. L'intérêt du sops-declassement reste : pas de secret en clair dans git.

Diagnostic​

docker logs --tail 20 crowdsec # voir la boucle d'erreur
docker inspect crowdsec --format '{{json .Mounts}}' | python3 -m json.tool
# Verifier que les deux mounts credentials sont "RW": true
ls -la /run/homelab/crowdsec/ # confirmer que les fichiers sont decryptes

Docker daemon crash loop (SIGBUS journald)​

Symptome​

systemctl show docker --property=NRestarts > 3 dans la journee, containers exit 0 massivement en cascade, auto-repair plafonne :

dockerd[PID]: SIGBUS: bus error
dockerd[PID]: github.com/moby/moby/v2/daemon/logger/journald/internal/sdjournal._Cfunc_sd_journal_next

Cause racine​

Bug ARM-specific du log-driver journald de dockerd. Le reader sdjournal (cgo binding) fait un SIGBUS quand journald rotate les files pendant un read mmap. Typique sur Pi avec SystemMaxUse agressif.

journalctl --verify passe (pas corruption journal) — c'est le reader qui panique.

Fix​

/etc/docker/daemon.json :

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

Puis systemctl restart docker && docker compose up -d.

Impact​

  • Les logs containers ne sont plus dans journalctl (ni journalctl CONTAINER_NAME=X).
  • docker logs X --tail N continue de fonctionner (lit json-file direct).
  • Alloy/Promtail pour shipping : doit lire /var/lib/docker/containers/*/*-json.log au lieu de journald.
  • Historique pre-switch perdu (Loki l'a déjà ingere si actif).

Alloy crashe a chaque :17 (SIGBUS journald, RAMlog DietPi)​

Symptome​

alloy.service sur penny crashe a chaque XX:17 (NRestarts grimpe en continu). systemd le relance (Restart=always) donc le service parait "actif", mais : trous horaires dans les metriques/logs, et le journal persistant ne retient qu'~1h.

alloy[PID]: SIGBUS: bus error
alloy[PID]: go-systemd/v22/sdjournal._Cfunc_my_sd_journal_next
alloy[PID]: loki/source/journal.newTailerWithReader
alloy.service: Failed with result 'exit-code'.

Indice cle : journalctl --header montre une first-entry = derniere :17 (le journal est reinitialise chaque heure). Le journalctl -u alloy --since peut afficher "1 crash" trompeur car la preuve elle-meme se fait vacuumer.

Cause racine​

DietPi RAMlog (mode -1) lance dietpi-logclear via cron.hourly (/etc/crontab, tick a :17) qui fait find /var/log -type f | truncate -cs0. Ce find descend dans le mount persistant /var/log/journal (SSD) et tronque le system.journal ACTIF a 0 octet. Le fichier etant mmap'd par loki.source.journal d'Alloy (sd_journal_next via cgo/libsystemd), la troncature invalide les pages mappees → SIGBUS.

A ne pas confondre avec le SIGBUS dockerd ci-dessus (log-driver journald) : ici c'est le reader Alloy + une troncature externe par DietPi, pas une rotation interne.

Fix​

  1. Journal en RAM, hors perimetre de dietpi-logclear — /etc/systemd/journald.conf.d/10-alloy-sigbus-volatile.conf :

    [Journal]
    Storage=volatile
  2. Pin Alloy sur la RAM (sinon il mappe encore les fichiers residuels /var/log/journal) — dans loki.source.journal "system" de config.alloy :

    path = "/run/log/journal"
  3. systemctl restart systemd-journald && systemctl restart alloy.service

Verification (sans attendre :17) : relancer /boot/dietpi/func/dietpi-logclear 1 a la main → systemctl show alloy -p MainPID doit etre inchange et NRestarts rester stable (avant le fix : crash systematique au meme test).

Impact​

  • Plus de journal local persistant apres reboot (l'historique central reste dans Loki). Aligne avec la philosophie RAMlog de DietPi.
  • Fichiers /var/log/journal/* desormais vestigiaux (tronques sans effet, nettoyables).
  • Lecon : sur DietPi RAMlog, ne jamais poser le journald persistant sous /var/log/journal. Voir projet/decisions.md → "Journald penny : volatile".

pmxcfs stuck read-only après recovery node cluster​

Symptome​

Sur un cluster 2-node, après qu'un node tombe puis revient, corosync-quorumtool -s montre Quorate: Yes mais :

sudo pvecm status # ipcc_send_rec[1] failed: Unknown error -1
sudo pct config 103 # meme erreur
sudo touch /etc/pve/.x # Read-only file system

Cause racine​

pmxcfs (fuse mount de /etc/pve) est démarre AVANT que corosync reforme le quorum, et ne retransitionne pas automatiquement vers RW une fois le quorum retrouve. État transitoire coince.

Fix​

sudo systemctl restart pve-cluster
sleep 3
sudo touch /etc/pve/.x && sudo rm /etc/pve/.x && echo OK

Zero impact sur les LXC running — /etc/pve n'est utilisé qu'a la reconfig, pas au runtime container.

Prevention​

Qdevice (3e vote) : le survivant ne perd jamais quorum → pmxcfs reste RW tout du long → pas de transition coincee. Voir architecture/cluster.md.


/etc, /usr, /boot read-only via SSH sur nodes PVE​

Symptome​

Sur galahad ou lancelot, via SSH :

sudo apt install X # "Read-only file system" sur /etc/*.dpkg-new
sudo pct set N --onboot 1 # idem sur /etc/pve/nodes/X/lxc/N.conf.tmp.PID
findmnt /etc # /etc /dev/mapper/pve-root[/etc] ext4 ro,relatime

Pas d'entree fstab, pas de mount-unit systemd.

Cause racine​

Services systemd actifs avec ProtectSystem=strict ou full sans PrivateMounts=yes : leur namespace mount est shared → leurs bind-mounts RO (via ProtectSystem) se propagent au namespace host global. Chaque restart accumule des leaks.

Scan pour identifier :

for svc in $(systemctl list-units --type=service --state=active --no-legend | awk '{print $1}'); do
ps=$(systemctl show "$svc" -p ProtectSystem --value)
pm=$(systemctl show "$svc" -p PrivateMounts --value)
if [ "$ps" = "strict" ] || [ "$ps" = "full" ]; then
[ "$pm" != "yes" ] && echo "LEAK: $svc"
fi
done

Fix​

  1. Drop-in PrivateMounts=yes sur chaque service listed :
mkdir -p /etc/systemd/system/<svc>.service.d
cat > /etc/systemd/system/<svc>.service.d/private-mounts.conf <<EOF
[Service]
PrivateMounts=yes
EOF
systemctl daemon-reload
systemctl restart <svc>
  1. Remount RW immédiate pour enlever les leaks déjà présents :
mount -o remount,rw /etc
mount -o remount,rw /usr
mount -o remount,rw /boot

Le restart ssh.service appliquant le drop-in risque de kill la session — scheduler via systemd-run --on-active=30s systemctl restart ssh pour préserver la connexion activé.

Services concernes (2026-04-19)​

beszel-agent (retiré depuis, le 2026-11-03), chrony, fail2ban, postfix, ssh, systemd-logind. Tous fixes via drop-ins /etc/systemd/system/<svc>.service.d/private-mounts.conf.


Réseau Docker — ETIMEDOUT entre conteneurs du même réseau (règles nft orphelines)​

Symptôme​

Un conteneur n'atteint pas un autre conteneur du même réseau Docker, alors que la cible est déclarée healthy :

SequelizeConnectionError: connect ETIMEDOUT 172.20.0.4:5432

ETIMEDOUT et non ECONNREFUSED : les paquets sont jetés en silence, ce n'est pas un service absent. Le conteneur boucle en redémarrage et autoheal tourne à vide.

Cause racine​

Docker 28+ installe une protection anti-accès-direct sous forme de règles nftables natives dans table ip raw, chaîne PREROUTING. Un docker network rm ne les nettoie pas. Si un réseau est recréé sur le même sous-réseau, il reçoit un nouveau bridge (br-<id>) tandis que les règles de l'ancien subsistent :

iifname != "br-50afc5752635" ip daddr 172.20.0.4 counter packets 44 drop # bridge DISPARU
iifname != "br-4c9e0a9dabb3" ip daddr 172.20.0.4 counter packets 0 drop # bridge actuel

Tout trafic arrivant par le bridge actuel satisfait != ancien bridge, donc il est jeté. Un compteur non nul sur une règle qui cite un bridge inexistant est la preuve.

danger
Rien de tout cela n'apparaît dans iptables

Ces règles agissent avant conntrack et avant la table filter. Les compteurs d'iptables -L restent à zéro et la politique FORWARD semble saine. Dès qu'un blocage réseau ne s'explique par aucun compteur iptables, passer à nft list ruleset.

Détection​

# Tout bridge cité par nft mais absent du noyau est un fantôme
EXIST=$(ip -br link show type bridge | awk '{print $1}' | tr '\n' '|')
nft -a list ruleset | grep -oE 'br-[0-9a-f]{12}' | sort -u |
while read b; do echo "$EXIST" | grep -q "$b" || echo "FANTOME: $b"; done

Fix — les deux étages, sinon il revient​

Nettoyer le noyau ne suffit pas : les règles sont persistées

Corrigé une première fois le 2026-08-16 par nft delete rule. Le 2026-08-20, au premier redémarrage, le même bridge fantôme et les mêmes six règles étaient de retour. Elles vivaient aussi dans /etc/iptables/rules.v4, que netfilter-persistent réinjecte à chaque démarrage. Le correctif était en sursis, et le sursis a duré jusqu'au reboot suivant — quatre jours.

OLD=br-50afc5752635 # le bridge fantôme
cp -a /etc/iptables/rules.v4 /etc/iptables/rules.v4.bak-$(date +%Y%m%d)

# 1. noyau
for ip in 2 3 4 5 6 7; do
iptables -t raw -D PREROUTING -d 172.20.0.$ip/32 ! -i $OLD -j DROP 2>/dev/null
done
iptables -t nat -D POSTROUTING -s 172.20.0.0/16 ! -o $OLD -j MASQUERADE 2>/dev/null

# 2. fichier persisté — l'étape oubliée
sed -i "/$OLD/d" /etc/iptables/rules.v4

# 3. vérifier les deux
iptables-save | grep -c "$OLD" # doit renvoyer 0
grep -c "$OLD" /etc/iptables/rules.v4 # doit renvoyer 0

Vérifier ensuite que le bridge vivant a bien gardé ses règles, puis relancer le conteneur. Le retour à healthy prend une minute.

Prévention​

Après tout docker network rm, chercher les bridges fantômes — surtout si un réseau est recréé sur le même sous-réseau. Et considérer que toute suppression de règle netfilter se fait aux deux étages, noyau et fichier persisté :

grep <motif> /etc/iptables/rules.v4
Cause de fond

rules.v4 contient des règles générées par Docker, capturées par un netfilter-persistent save à un instant donné. C'est fragile par construction : Docker régénère ses règles à chaque démarrage du daemon et ne connaît pas les bridges d'une capture antérieure. Le bon état serait que rules.v4 ne porte que les règles maison (EGRESS-PHASE2). Chantier ouvert.


Commandes utiles​

Containers et services​

docker ps -a # Etat de tous les conteneurs
docker logs <container_name> --tail 50 # Derniers logs
docker compose up -d <service_name> # Relancer un service specifique
docker system df # Espace utilise par Docker
docker system prune -f # voir l'avertissement ci-dessous
attention
prune jette le cache qui sert au retour arrière

Au 2026-09-25, penny porte 139 images pour 31 Go, dont 26 Go récupérables. C'est beaucoup, et la tentation de nettoyer est légitime — le SSD n'est qu'à 17 %, donc rien ne presse.

Mais toutes les images de ce parc sont épinglées par digest et rien ne se met à jour tout seul. Ces 26 Go, ce sont en grande partie les digests précédents. Les jeter transforme un retour arrière — remettre l'ancien digest dans le compose et relancer — en opération qui dépend d'Internet et du registre amont. Sur une machine qu'on répare justement parce que quelque chose vient de casser, ce n'est pas le moment de découvrir qu'il faut retélécharger.

Si l'espace manque vraiment, viser plutôt que balayer :

docker image prune -a --filter "until=720h" # non utilisees depuis 30 jours

Et jamais --volumes sans regarder d'abord : docker volume ls rend les mêmes noms sous deux préfixes, config_ et docker_, et CrowdSec est la seule exception qui vit sur un docker_*. Voir l'inventaire des services.

Temperature et alimentation​

vcgencmd measure_temp # Temperature actuelle
vcgencmd get_throttled # 0x0 = tout va bien
Valeur throttledSignification
0x0OK
0x50000Throttling dans le passe
0x50005Throttling actif + sous-voltage

Espace disque​

df -h # Vue d'ensemble
docker system df # Espace utilise par Docker
docker system prune -f # Nettoyer images/volumes inutilises

Dashboard Homepage sans onglets, en anglais, ou sans alarme​

Panne discrete : les services, les favoris et le theme s'affichent normalement, mais la barre d'onglets a disparu, le titre est redevenu « Homepage », et un service tombe ne colore plus sa carte. Cause : la page statique Next.js n'a pas ete regeneree depuis le dernier demarrage du conteneur, donc Homepage sert celle compilee dans l'image, sans configuration.

# Diagnostic : le titre doit etre celui de settings.yaml, pas "Homepage"
IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}} {{end}}' homepage | awk '{print $1}')
curl -s -H "Host: home.gabin-simond.fr" "http://$IP:3000/" | grep -oE "<title[^>]*>[^<]*</title>"

# Correctif immediat
systemctl start homepage-revalidate.service

L'unite est declenchee par docker.service, donc au boot et au restart docker nocturne de dietpi-backup. Si elle a echoue : journalctl -u homepage-revalidate -n 20.

Pieges de diagnostic : custom.css n'est pas servi a la racine mais sous /api/config/custom.css (un 404 sur /custom.css ne veut rien dire), et le theme ne discrimine pas — dark et slate sont aussi les valeurs par defaut de Homepage. Details : services/homepage.md.


Nœud PVE mort sans laisser de trace — Oops kernel​

Un nœud disparaît d'un coup, en pleine santé : dernière ligne de log à 13:11:59, corosync signale link is down à 13:12:03, et plus rien. Aucune séquence d'arrêt, aucun avertissement kernel, aucun précurseur matériel. Le nœud ne répond plus ni au ping ni à l'ARP, et ne redémarre jamais tout seul.

Le réflexe naturel — « coupure d'alimentation » — est presque toujours faux ici.

Le seul témoin est pstore, pas le journal​

Un Oops kernel n'atteint jamais le journal sur disque : la machine meurt avant l'écriture. Chercher dans journalctl ne donne donc rien, et cette absence de trace se lit à tort comme « perte de courant ».

# INUTILE — retourne toujours 0, même après un vrai crash kernel
journalctl -b -1 | grep -c Oops

# LE BON ENDROIT — dumps EFI récupérés par systemd-pstore au démarrage suivant
ls -1 /var/lib/systemd/pstore/
for d in /var/lib/systemd/pstore/*/; do date -d @"$(basename "$d")"; done

Un répertoire dont l'horodatage correspond à la seconde de la mort prouve que le kernel est passé par le chemin panic : une coupure d'alimentation ne peut pas produire d'enregistrement pstore.

Pour extraire la signature :

grep -aiE "BUG:|Oops:|RIP:|Call Trace|Hardware name" \
/var/lib/systemd/pstore/<timestamp>/*/dmesg.txt
attention
Ne jamais purger /var/lib/systemd/pstore/

C'est la seule preuve exploitable d'un crash, et la vider ne libère rien : systemd-pstore fait déjà Unlink=yes sur /sys/fs/pstore, donc la NVRAM EFI est purgée à chaque démarrage. Sur ZimaBoard, les dumps ressortent parfois à 0 octet — garder les anciens est alors la seule façon d'avoir une signature lisible.

Cause racine constatée (2026-07-10 et 2026-08-05)​

BUG: unable to handle page fault for address: 0000000100000028
Oops: 0000 [#2] SMP NOPTI
Hardware name: IceWhale ZimaBoard2, BIOS 5.27 07/22/2025
RIP: 0010:update_sd_lb_stats.constprop.0+0x93/0xbe0
puis cpuidle_enter_state+0xc7/0x460

Crash dans le load-balancer du scheduler CFS, sur un pointeur corrompu de forme 0xN00000028, avec des Oops en cascade sur la même seconde. Kernel 6.17.13-2-pve, récurrence d'environ 2 à 3 semaines.

Ce qui coûtait le plus cher : panic_on_oops=0​

Avec le défaut Debian, un Oops laisse la machine morte indéfiniment au lieu de redémarrer. Et sur ces cartes il n'existe aucune voie de récupération à distance : pas de WoL configuré, pas de BMC ni d'IPMI sur une ZimaBoard, pas de prise pilotable. Un crash de 5 secondes coûte donc plus d'une heure d'indisponibilité et un déplacement physique — la ZimaBoard n'a pas de bouton d'alimentation, le cycle se fait au jack DC.

Correctif déployé le 2026-08-05 sur galahad et lancelot (system/99-panic-on-oops.conf dans homelab-config) :

kernel.panic_on_oops=1 # un Oops déclenche un panic...
kernel.panic=10 # ...qui redémarre au bout de 10 s

Soit ~2 min d'indisponibilité auto-résolue au lieu d'un trajet. C'est un filet de sécurité, pas un correctif : la régression sched reste à traiter par un upgrade kernel.

Deux pièges de diagnostic rencontrés​

Le journal local peut être moins complet que le replica réseau. Ici le journal on-disk s'arrêtait à 13:11:26 (system.journal corrupted or uncleanly shut down, renaming and replacing) alors qu'Alloy avait déjà expédié jusqu'à 13:11:59. Sur coupure franche, commencer par Loki, pas par la machine.

Et un double démarrage rapproché dans journalctl --list-boots n'est pas forcément une boucle de reboot : vérifier s'il existe un enregistrement pstore à cet horodatage. S'il n'y en a pas, c'est une coupure externe — typiquement le jack DC débranché deux fois.

Vérifier après le retour du nœud​

touch /etc/pve/.rwtest && rm /etc/pve/.rwtest # piège pmxcfs read-only
pvecm status | grep -E "Quorate|Total votes"
pct list # guests redémarrés ?
pvesm status # storage PBS repassé active ?
systemctl --failed

Voir aussi pmxcfs stuck read-only après recovery node cluster.


penny reset en dur, sans rien dans le journal​

Le symptôme se présente rarement comme un redémarrage. Il ressemble d'abord à une panne de tout autre chose : « Impossible d'accéder à votre ordinateur » dans l'app Claude, le DNS du LAN qui ne répond plus, Traefik, Firefly et PBS injoignables d'un coup. C'est penny qui a disparu, et le watchdog BCM2835 qui l'a redémarrée après 15 secondes de silence kernel.

Le piège de cette panne n'est pas de la réparer, c'est de savoir ce qui l'a causée : le reset efface sa propre preuve. Trois obstacles se combinent, et ils font mentir le diagnostic dans cet ordre.

Le journal persistant de penny n'a jamais été persistant — constaté le 2026-09-25

Après l'incident du 2026-04-17, le journal a été passé en persistant « pour les analyses futures ». Il ne l'a jamais été. Deux causes se cumulent, et chacune suffirait.

Deux directives Storage= se contredisent, la mauvaise gagne :

$ grep -rn '^\s*Storage=' /etc/systemd/journald.conf /etc/systemd/journald.conf.d/
/etc/systemd/journald.conf:18:Storage=persistent
/etc/systemd/journald.conf.d/10-alloy-sigbus-volatile.conf:20:Storage=volatile

Un drop-in l'emporte sur le fichier principal : la valeur effective est volatile. Elle a été posée exprès — c'est le correctif du SIGBUS journald qui tuait dockerd. Le correctif d'avril et celui de juillet se sont annulés sans que personne ne le voie, parce qu'aucun des deux n'a été vérifié par l'effet.

Et /var/log est un tmpfs de 50 Mo. Même si persistent l'emportait, le journal « persistant » s'écrirait en RAM et disparaîtrait au redémarrage. La destination invalide la promesse indépendamment du réglage.

Preuve par l'effet, en une commande :

$ journalctl --list-boots --no-pager | wc -l
2 # deux, pas des dizaines
$ du -sh /run/log/journal /var/log/journal
17M /run/log/journal # le vrai journal, volatile
16K /var/log/journal # la coquille vide

Conséquence pratique : la cause de chaque redémarrage de penny est inconnaissable après coup. Le 2026-09-25, la machine s'est réinitialisée vers 09:16 — activité parfaitement normale jusqu'à 09:16:38 dans Loki, puis plus rien ; pstore vide, bootstatus du watchdog à 0, aucune mise à jour en cours. Impossible de distinguer un reboot propre dont les messages d'arrêt n'ont pas eu le temps de partir vers Loki, d'un reset matériel.

Corriger n'est pas « remettre persistent » : ça ressusciterait le SIGBUS. Il faudrait une destination persistante hors du /var/log que dietpi-logclear tronque chaque heure — un répertoire sur le SSD, par exemple. Décision ouverte.

1. Dater le boot — journalctl est faux pendant ~46 min​

Le RPi 4 n'a pas d'horloge temps réel. Au démarrage, fake-hwclock restaure la dernière heure sauvegardée, puis systemd-timesyncd corrige d'un coup quelques dizaines de secondes plus tard.

Or cette sauvegarde se fait à deux moments seulement :

MomentMécanismeConséquence
Arrêt propreExecStop=/sbin/fake-hwclock savel'heure au boot est juste
Toutes les heures, à :17run-parts /etc/cron.hourly (défaut Debian)l'heure au boot a jusqu'à 1 h de retard

Après un reset brutal, seul le second a joué. Le 2026-08-27, penny a redémarré à 10:03:38 et journald a horodaté le boot 09:17:03 — comme ExecMainStartTimestamp de tous les services. tailscaled l'a dit tout seul : time jump detected (slept 46m47s).

Tous les « HH:17 » sont un artefact, pas un horaire

Une session d'analyse antérieure a conclu « 13 h sans redémarrage, systemd voit la machine saine » alors que penny venait de rebooter. systemctl show a menti sans le savoir. Un redémarrage propre ne produit pas ce décalage, ce qui rend l'artefact d'autant plus traître : il n'apparaît que sur les boots qu'on cherche justement à dater.

# LA méthode fiable, la seule
date -d "-$(awk '{print int($1)}' /proc/uptime) seconds" '+%Y-%m-%d %H:%M:%S'

# À ne PAS croire dans les ~46 min qui suivent un reset brutal
journalctl --list-boots
systemctl show <unit> -p ExecMainStartTimestamp
who -b

2. Le journal local ne peut pas répondre — deux couches indépendantes​

Réparer l'une ne suffit pas, il faut savoir que les deux existent :

  • /var/log est un tmpfs de 50 MiB purgé chaque heure par dietpi-ramlog ;
  • journald.conf déclare Storage=persistent, mais le drop-in 10-fish-alloy-sigbus-volatile.conf le remplace par Storage=volatile. C'est un contournement voulu des SIGBUS ARM sur journald (voir Docker daemon crash loop) — ne pas le retirer sans arbitrage.

Les seuls témoins qui survivent vivent hors de penny, et c'est ce qui a tranché les incidents d'août :

TémoinOùRétentionCe qu'il donne
PrometheusLXC 101 (lancelot)30 jnode_memory_MemAvailable_bytes{host="penny"} au pas de 30 s
LokiLXC 101 + réplica sur penny720 h{job="journald", host="penny"}

Le label est host=, pas instance=~".*penny.*". Et sur une coupure franche, commencer par Loki : Alloy a souvent expédié plus loin que ce que le journal local a eu le temps d'écrire.

3. ramoops — le seul témoin qui n'a besoin de rien de vivant​

Une zone de RAM réservée garde le tampon dmesg glissant et survit au reset du SoC ; systemd-pstore l'archive au démarrage suivant. Armé le 2026-08-27 dans /boot/firmware/config.txt :

dtoverlay=ramoops-pi4,total-size=131072,console-size=65536

console-size est l'essentiel : son défaut est 0, et sans lui ramoops ne capture que les panics — donc rien du tout sur un simple figeage, qui est justement le cas fréquent ici.

Sa vraie valeur est d'être discriminant, là où tout le reste est muet :

Après un reset brutalVerdict
dmesg-* / pmsg-* présentwatchdog ou hang kernel — la DRAM a gardé son contenu
aucun dumpcoupure secteur — la DRAM a tout perdu

C'est exactement la question restée ouverte sur le reset du 2026-08-26.

ls -1 /var/lib/systemd/pstore/ # dumps archivés
ls -1 /sys/fs/pstore/ # non encore archivés (vide en régime normal)

Deux familles de fichiers, à ne pas confondre : dmesg-*/pmsg-* sont une preuve de crash ; console-* est un tampon glissant présent après n'importe quel redémarrage, propre inclus. check_pstore dans homelab_monitor.sh n'alerte que sur les premiers, et seulement sur les dumps jamais vus (identité = chemin + mtime) : l'archive persistant indéfiniment, alerter sur « archive non vide » réarmerait toutes les 30 minutes pour un événement unique.

Arbre de décision​

Une fois le boot correctement daté, trois mesures suffisent à trancher :

Dump pstoreMemAvailable avant le trouthrottledVerdict
présent——Oops ou hang kernel : lire le dump
absents'effondre0x0épuisement mémoire (cas des 25/08 et 27/08)
absentplat et sain0x0figeage non expliqué — voir SSD, charge, eth0
absentplat≠ 0x0sous-tension : alimentation ou câble
absentpas de donnée du tout—coupure secteur probable (Prometheus s'arrête net)

Une ligne de plus, valable pour les trois machines : un uptime identique sur penny, galahad et lancelot désigne un événement électrique, pas trois pannes indépendantes.

La cause déjà rencontrée deux fois : l'épuisement mémoire​

penny tourne sans swap ; claude-remote.service tournait en MemoryMax=infinity. Une session emballée n'avait donc aucun garde-fou :

DateMemAvailableDurée
2026-08-25 18:183829 → 1017 MiB12 min
2026-08-27 10:034152 → 1440 MiB90 s

La machine se fige, le watchdog la reset, et le reset emporte la preuve. Trois correctifs déployés le 2026-08-27 :

  1. Bornes cgroup sur claude-remote.service (MemoryHigh=3500M, MemoryMax=4500M) : à la limite dure, le noyau tue le plus gros processus du groupe — la session fautive — au lieu de laisser mourir la machine. Appliquer par daemon-reload seul : un restart tuerait toutes les sessions vivantes.
  2. zram 2 GiB (/boot/dietpi/func/dietpi-set_swapfile 2048 zram). Effet de bord à défaire : la fonction recalcule le tmpfs /tmp à la hausse parce que le swap entre dans sa formule — remettre la valeur de system/fstab.
  3. Enregistreur de vol dans check_ram : TOP-5 RSS, MemAvailable, charge et swap écrits à chaque passage dans /mnt/ssd/log-homelab/ram-flight-recorder.log.
Pourquoi l'alerte porte sur la vitesse et non sur un niveau

RAM_WATCH=75 % est un seuil de niveau, et un niveau ne voit pas une rampe. Le 27/08 à 10:01, penny était à 73 % — un point sous le seuil — donc le TOP-5 n'a jamais été écrit et le coupable de ce matin-là reste inconnu.

L'alerte ram-collapse combine désormais deux conditions : plus de 1000 MiB/min perdus et moins de 3000 MiB restants. La vitesse seule crierait sur toute grosse allocation bénigne d'une machine reposée ; le niveau seul arrive trop tard. Rejouée sur les chiffres du 27/08 (~1800 MiB/min, 2355 MiB restants), elle serait partie ~90 s avant le figeage — la seule alerte de cette famille qui laisse une fenêtre d'action.

Vérifier après le retour de penny​

date -d "-$(awk '{print int($1)}' /proc/uptime) seconds" # dater le boot, d'abord
ls -1 /var/lib/systemd/pstore/ # le kernel a-t-il parlé ?
vcgencmd get_throttled # 0x0 attendu
systemctl --failed
docker ps --format '{{.Names}}\t{{.Status}}' | grep -v Up # cf. stack down après reboot
tail -50 /mnt/ssd/log-homelab/ram-flight-recorder.log # que montait-il avant ?
systemctl show claude-remote -p MemoryMax -p MemoryHigh # bornes toujours actives ?

Voir aussi Watchdog hardware (BCM2835) pour la configuration, et Nœud PVE mort sans laisser de trace pour l'équivalent sur les ZimaBoard (pstore EFI, mécanisme différent).