TL;DR : le 14 mai 2026, ma VM Docker s'est figée. Deux saturations en même temps : la RAM partait en swap thrashing et le thin pool LVM de Proxmox affichait 97,8 % — dmeventd: Insufficient free space, toutes les I/O bloquées. Le NVMe de 250 Go était plein et le groupe de volumes n'avait plus un octet libre : aucune marge pour réparer. La vraie solution tenait en une phrase — un disque plus gros — et en un problème matériel : la carte mère n'a qu'un seul slot M.2. Voici comment j'ai migré un hôte Proxmox vers un NVMe quatre fois plus grand avec dd, un disque USB en relais et zéro réinstallation. Et la leçon discard=on que je n'ai comprise qu'une semaine plus tard.


Un homelab, ça marche très bien jusqu'au jour où ça ne marche plus du tout. Le mien tourne sous Proxmox : deux machines virtuelles — une baie Unraid et une VM Docker qui héberge une soixantaine de conteneurs (Plex, Immich, Paperless, et tout le reste). Pendant des mois, je n'ai pas regardé l'espace disque. C'était l'erreur.

Ce billet est le récit d'une panne, puis d'une migration de disque système menée depuis un live USB, étape par étape. Si vous faites tourner Proxmox sur un NVMe qui se remplit, l'épilogue vous concerne directement.

Un dimanche, la VM 101 se fige

La VM Docker — la 101 dans Proxmox — ne répondait plus. L'interface morte, les conteneurs injoignables. Le diagnostic a révélé non pas une panne, mais deux, simultanées.

La RAM, d'abord. La VM consommait 96 % de sa mémoire — 26 Gio. À ce niveau, le noyau invité s'est rabattu sur le swap, puis a passé tout son temps à faire des allers-retours entre RAM et disque. Le swap thrashing a déclenché des soft-lockups CPU : le processeur attend si longtemps une I/O qu'il se déclare lui-même bloqué. La VM était figée vivante.

Le stockage, ensuite — et c'est le plus grave. Proxmox range ses disques virtuels dans un thin pool LVM nommé pve/data. Il était rempli à 97,8 %. À ce seuil, dmeventd — le démon qui surveille les volumes LVM — a rendu son verdict : Insufficient free space. Toutes les écritures vers le pool ont été bloquées. Une VM dont le disque ne peut plus rien écrire ne fait pas grand-chose.

Un thin pool, c'est un réservoir de blocs. Les disques virtuels — ici 32 Go pour la baie Unraid, 150 Go pour la VM Docker — sont des volumes thin : ils annoncent leur taille maximale mais ne consomment dans le réservoir que les blocs réellement écrits. Pratique pour sur-allouer… jusqu'au jour où le réservoir se vide pour de bon. Et là, ce ne sont pas les volumes qui débordent : c'est le pool.

Les premiers secours ont calmé l'incendie sans l'éteindre. J'ai abaissé les limites mémoire des conteneurs les plus gourmands, réduit le swap de 8 à 2 Gio, supprimé un snapshot fantôme resté bloqué, et récupéré ces quelques gigaoctets pour étendre le thin pool à 159 Go. Les VMs sont reparties. Mais le pool affichait toujours 93,5 %. J'avais déplacé le problème de quelques jours, pas réglé quoi que ce soit.

Le vrai problème : il n'y avait plus aucune marge

Pourquoi je ne pouvais pas simplement agrandir le pool ? Parce qu'il n'y avait plus rien pour l'agrandir.

Voici à quoi ressemblait le NVMe avant migration :

/dev/nvme0n1 — 232,9 GiB (Crucial CT250P5SSD8)
├── nvme0n1p1    1007K   BIOS boot
├── nvme0n1p2       1G   EFI System
└── nvme0n1p3   231,9G   Linux LVM (VG : pve)
    ├── pve-swap       2G
    ├── pve-root      68G   ext4 (15 GiB utilisés)
    ├── pve-data   159,1G   thin pool (93,5 %)
    │   ├── vm-100-disk-0   32G
    │   └── vm-101-disk-0  150G
    └── (libre : 0G)

Regardez la dernière ligne. Le groupe de volumes pve avait 0 Go de libre. Tout l'espace du disque avait été distribué : 2 Go de swap, 68 Go de racine, et tout le reste versé dans le thin pool. Le « moi » qui avait installé ce Proxmox des mois plus tôt avait coché l'option par défaut et tout alloué. Il ne restait pas un octet pour respirer.

C'est exactement la marge qui manque le jour d'un incident. Pas d'espace libre dans le VG, pas d'extension possible du pool ; un disque de 250 Go plein, pas de disque plus grand. Toutes les portes étaient fermées sauf une : remplacer le NVMe par un modèle plus grand. Un Crucial P5 de 1 To — quatre fois la place.

Un seul slot M.2 — la contrainte qui complique tout

Remplacer un disque système, sur le papier, c'est trivial : on branche le neuf à côté de l'ancien, on clone de l'un vers l'autre, on débranche l'ancien. Sauf que la carte mère de ce serveur — une Gigabyte B450M DS3H — n'a qu'un seul emplacement M.2. Impossible d'avoir l'ancien et le nouveau NVMe branchés en même temps. Impossible de cloner directement de disque à disque.

La solution : un disque relais. On clone le NVMe vers un disque externe, on éteint, on permute physiquement les deux NVMe, on rallume, et on restaure le clone depuis le disque externe vers le NVMe neuf. Deux passes au lieu d'une, mais ça contourne la contrainte. J'avais sous la main un WD Elements de 12 To en USB, vide — parfait comme relais.

NVMe 250 Go source — plein WD Elements 12 To relais USB NVMe 1 To cible — neuf 1. dd — clone 2. dd borné Entre les deux passes : extinction, retrait du 250 Go, insertion du 1 To dans l'unique slot M.2.

Il existe une variante plus rapide : glisser le NVMe 1 To neuf dans un boîtier USB-M.2 (une douzaine d'euros), cloner directement de NVMe interne à NVMe externe en une seule passe à vitesse SSD, puis permuter physiquement les deux disques. Plus de disque relais, plus de double copie. Je ne l'ai pas fait — j'avais déjà le WD Elements — mais si vous prévoyez ce genre d'opération, le boîtier vaut largement son prix.

Cloner le disque, pas réinstaller

J'aurais pu réinstaller Proxmox à neuf sur le 1 To et tout reconfigurer. Je ne l'ai pas fait, et c'est volontaire.

Un clone bloc à bloc avec dd copie tout : la table de partition, la partition EFI, le chargeur GRUB, et le volume physique LVM avec ses UUID. Après restauration, le nouveau disque est un jumeau parfait de l'ancien. Proxmox démarre, les VMs démarrent, la configuration réseau, les utilisateurs, les certificats — rien n'a bougé, parce que du point de vue du système rien n'a changé. On n'a fait que poser les mêmes octets sur un disque plus grand.

Le prix à payer : les VMs sont arrêtées pendant toute l'opération (un clone à froid, pas de migration à chaud), et il faut deux passes dd à travers l'USB. Dans mon cas, environ 17 minutes par passe à 235 Mo/s en USB 3. Un downtime d'un après-midi, planifiable.

La procédure, étape par étape

Toute la chirurgie se fait depuis un live USB. Pas l'ISO d'installation de Proxmox : son environnement de secours est trop minimal — pas de lsblk, pas de parted, /dev/disk/by-id/ non peuplé. J'ai utilisé SystemRescue, qui embarque tout l'outillage disque et ouvre directement un shell root.

Étape 1 — Arrêter les VMs proprement

Depuis l'interface Proxmox, avant de toucher au matériel :

qm shutdown 101 --timeout 120
qm shutdown 100 --timeout 120
qm list

Étape 2 — Booter sur SystemRescue

Clé USB SystemRescue et disque relais branchés, on redémarre, on entre dans le BIOS (touche DEL sur Gigabyte) et on amorce sur la clé. SystemRescue ouvre directement un shell root. Clavier français : loadkeys fr.

Étape 3 — Identifier les disques

L'étape la plus dangereuse de toute la procédure. Les lettres /dev/sdX changent d'un démarrage à l'autre — on ne s'y fie pas. On désigne le disque USB par son chemin stable dans /dev/disk/by-id/ ; le NVMe, seul de la machine, garde son nom fixe /dev/nvme0n1.

lsblk -d -o NAME,SIZE,MODEL,SERIAL
ls -l /dev/disk/by-id/ | grep -E 'nvme|usb'

Sur ce serveur, l'inventaire réservait une surprise : Proxmox étant arrêté, les huit disques de 4 To de la baie Unraid (la VM 100) apparaissaient comme des disques nus, sda à sdh. Les inclure dans une commande dd aurait pulvérisé le NAS. D'où la règle absolue : on définit des variables nommées, et on ne tape jamais un sdX brut.

SRC=/dev/nvme0n1
DST=/dev/disk/by-id/usb-WD_Elements_XXXX-0:0
lsblk "$SRC" "$DST"
# $SRC doit montrer ~232G (nvme0n1p1/p2/p3) — $DST doit montrer ~10,9T.
# Si les tailles semblent inversées : STOP, on ne lance rien.

Étape 4 — Cloner le NVMe vers le disque relais

dd if="$SRC" of="$DST" bs=4M conv=fsync status=progress

Environ 17 minutes à 236 Mo/s. On vérifie ensuite le clone — mais en comparant uniquement la taille du NVMe : le disque relais est bien plus grand, comparer les deux disques entiers donnerait un faux échec à coup sûr.

SIZE=$(blockdev --getsize64 "$SRC")
echo "$SIZE"          # noter cette valeur — réutilisée à l'étape 6
cmp -n "$SIZE" "$SRC" "$DST" && echo "CLONE OK"

Étape 5 — Permuter physiquement les NVMe

On éteint (poweroff), on débranche l'alimentation. On retire le Crucial 250 Go — on l'étiquette et on le met de côté : c'est le rollback complet — et on insère le 1 To dans le même slot M.2. La clé SystemRescue et le disque relais restent branchés.

Étape 6 — Restaurer le clone sur le NVMe neuf

On redémarre sur SystemRescue. Le NVMe affiche désormais ~931 Go. La restauration va dans le sens inverse de l'étape 4 — et elle doit être bornée :

NVME=/dev/nvme0n1
DST=/dev/disk/by-id/usb-WD_Elements_XXXX-0:0
lsblk "$NVME" "$DST"

CLONE_SIZE=250059350016   # la taille relevée à l'étape 4
dd if="$DST" of="$NVME" bs=4M iflag=count_bytes count="$CLONE_SIZE" conv=fsync status=progress
sync
cmp -n "$CLONE_SIZE" "$NVME" "$DST" && echo "RESTORE OK"

Sans le count=, dd recopierait l'intégralité du disque relais de 12 To et saturerait le NVMe bien avant d'avoir fini — des centaines de gigaoctets pour rien.

Étape 7 — Réparer la GPT, étendre la partition et le pool

Le clone fait 233 Go ; posé sur un disque de 1 To, sa table GPT de secours se retrouve au mauvais endroit et masque l'espace libre. On la replace, puis — point crucial — on débranche le disque relais avant toute opération LVM : le clone étant bit-à-bit, le relais et le NVMe portent le même UUID de volume physique. Tant que les deux sont branchés, LVM voit un PV dupliqué et peut opérer sur le mauvais disque.

vgchange -an pve
sgdisk -e "$NVME"            # replace la GPT de secours en fin de disque

# >>> débrancher physiquement le câble USB du disque relais <<<
pvscan --cache
pvs                          # un seul PV pour le VG « pve », sans avertissement « duplicate »

growpart "$NVME" 3           # étend la partition 3 à tout l'espace
partprobe "$NVME"
pvresize /dev/nvme0n1p3
vgchange -ay pve

lvextend --poolmetadatasize +1G pve/data   # le pool quintuple — les métadonnées doivent suivre
lvextend -l +95%FREE pve/data              # +95 %, surtout pas +100 % : on garde une marge
lvs -o lv_name,lv_size,data_percent,metadata_percent pve
vgs pve

Note : parted peut être absent du PATH sous SystemRescue ; growpart fait le travail aussi bien, et gère lui-même la relocalisation de la GPT.

Étape 8 — Redémarrer

On retire la clé SystemRescue et on reboot. Comme dd a recopié à l'identique la partition EFI, GRUB et tous les UUID, le serveur démarre sur le NVMe neuf sans rien savoir du changement.

Étape 9 — Vérifier

pveversion
qm list
qm status 100
qm status 101

Si les VMs ont l'option onboot, elles sont déjà reparties toutes seules.

Un détail qui peut inquiéter : proxmox-boot-tool status renvoie E: /etc/kernel/proxmox-boot-uuids does not exist. C'est normal sur une installation classique LVM + GRUB — proxmox-boot-tool ne sert que pour les setups ZFS ou systemd-boot. Le démarrage réussi suffit à prouver que la chaîne d'amorçage est intacte.

Les pièges qui transforment une migration en sinistre

La première version de cette procédure ne tenait pas debout. C'est la relecture, puis le test terrain, qui l'ont durcie. Voici les huit pièges qui font la différence entre une migration propre et un disque effacé.

  • Vérifier le clone sur la bonne taille. Un cmp sur les deux disques entiers est un faux échec garanti — le disque relais est plus grand. On compare avec cmp -n et le nombre d'octets exact du clone.
  • Borner la restauration avec count=. Le relais de 12 To est bien plus grand que le clone de 250 Go ; un dd sans limite recopie tout le relais jusqu'à saturer le NVMe.
  • Débrancher le disque relais avant toute commande LVM. Un clone bit-à-bit, c'est un UUID de PV identique. LVM voit un PV dupliqué et peut lancer pvresize/lvextend sur le mauvais disque.
  • Relocaliser la GPT de secours. Le clone fait 233 Go ; sur un disque de 1 To, sa table GPT de secours atterrit au mauvais endroit et l'extension de partition ne voit pas l'espace libre. sgdisk -e la remet en place.
  • Désigner les disques USB par /dev/disk/by-id/. Les lettres sdX changent d'un boot à l'autre. Le NVMe, seul de la machine, garde son /dev/nvme0n1 stable.
  • Connaître son live USB. parted peut manquer du PATH sous SystemRescue ; growpart ou sgdisk font le travail. Et l'ISO d'installation Proxmox est trop minimale pour de la chirurgie disque.
  • Laisser une marge dans le groupe de volumes. Jamais de lvextend -l +100%FREE. Ce « 0 Go libre », c'est précisément ce qui a bloqué la résolution de l'incident. J'ai laissé ~5 %.
  • Inventorier tous les disques avant le moindre dd. Proxmox arrêté, les huit disques de 4 To de la baie Unraid apparaissaient comme des disques nus. En attraper un dans un dd aurait détruit le NAS. Variables nommées, jamais de sdX brut.

Le résultat

Le serveur a redémarré du premier coup sur le NVMe neuf. Voici le layout final :

/dev/nvme0n1 — 931,5 Go (Crucial CT1000P5SSD8)
├── nvme0n1p1    1007K   BIOS boot
├── nvme0n1p2       1G   EFI System
└── nvme0n1p3  ~930,5G   Linux LVM (VG : pve, VFree 34,83 GiB)
    ├── pve-swap        2G
    ├── pve-root      ~68G
    ├── pve-data   820,91G   thin pool (18,12 %)
    │   ├── vm-100-disk-0   32G
    │   └── vm-101-disk-0  150G
    └── (libre VG : 34,83G — marge de sécurité)
  • Thin pool LVM — de 159 Go saturés à 93,5 %, à 820,91 Go occupés à seulement 18,12 %.
  • Métadonnées du pool — étendues de 1,40 à 2,40 Go, pour suivre un pool cinq fois plus grand.
  • Espace libre du groupe de volumes — de 0 à 34,83 Go. C'est la marge de sécurité qui manquait le 14 mai.
  • Démarrage — Proxmox VE 9.1.1 (noyau 6.17.2-1-pve) a redémarré sans broncher, les deux VMs relancées automatiquement par onboot.

Aucune reconfiguration. dd ayant recopié EFI, GRUB et les UUID à l'identique, le système n'a tout simplement pas vu qu'il avait changé de disque.

Épilogue — le « 99 % » qui n'en était pas un

En clôturant le ticket, j'avais noté un point de suivi : le disque virtuel de la VM 101 semblait « rempli à 99 % ». Une semaine plus tard, en voulant l'agrandir, je me suis rendu compte que je m'étais trompé de métrique.

Ce « 99 % » n'était pas le remplissage du système de fichiers invité. C'était le Data% du volume thin vm-101-disk-0 — la part du réservoir que ce volume avait consommée. À l'intérieur de la VM, le système de fichiers / était en réalité à 72 % : 99 Go utilisés sur 146, 41 Go libres. Aucun agrandissement n'était nécessaire. Mais alors, pourquoi l'allocation thin avait-elle gonflé jusqu'à 99 % ?

La réponse tient en un mot : discard. Le disque scsi0 de la VM n'avait pas l'option discard=on. Sans elle, les commandes TRIM de l'invité — qui signalent les blocs libérés — n'atteignaient jamais le thin pool. L'invité avait beau lancer son fstrim hebdomadaire, le pool ne récupérait jamais ces blocs. L'allocation ne pouvait que croître, jamais diminuer. Un disque qui se vide côté invité mais reste « plein » côté pool : voilà le piège du thin provisioning sans discard.

Le correctif :

qm set 101 --scsi0 local-lvm:vm-101-disk-0,size=150G,discard=on,ssd=1

Un reboot de la VM (l'option discard est posée à la création du périphérique bloc), puis un fstrim -av dans l'invité — et 48 Go sont rendus au pool d'un coup. Le Data% de vm-101-disk-0 tombe de 99,17 % à 70,46 %, le thin pool de 18,12 % à 12,88 %. Le fstrim.timer hebdomadaire entretient désormais l'allocation tout seul.

Et le dernier maillon manquant : je n'avais aucune sauvegarde automatique. Le WD Elements de 12 To qui avait servi de relais a été reconverti en disque de sauvegarde — effacé, reformaté en ext4, monté en /mnt/wd-backup, déclaré comme stockage Proxmox. Un job vzdump hebdomadaire sauvegarde maintenant les deux VMs tous les dimanches à 2 h du matin, en mode snapshot, compression zstd. L'incident du 14 mai aurait pu coûter une soixantaine de conteneurs et leurs données. La prochaine fois, il y aura un filet.

Ce que je retiens

  • Ne jamais allouer 100 % d'un groupe de volumes. Gardez ~5 % de libre. Cette marge est exactement ce qui permet de réparer un incident au lieu de le subir.
  • Le thin provisioning exige discard=on de bout en bout. Sans lui, l'allocation du pool ne fait que monter, même quand les invités se vident. TRIM doit pouvoir traverser toute la chaîne.
  • Surveillez le thin pool, pas seulement les disques. Un volume invité à 72 % et un pool à 97 % racontent deux histoires différentes — et c'est le pool qui vous bloque les I/O.
  • Un clone dd est un outil de migration sous-estimé. Il transporte EFI, GRUB et les UUID LVM à l'identique : le système ne voit pas la différence.
  • Connaissez votre live USB avant d'en avoir besoin. Quels outils sont présents, comment les disques sont nommés, ce que l'ISO de secours embarque vraiment.
  • Pas de sauvegarde, pas de filet. Un homelab sans vzdump planifié n'est pas un homelab, c'est un pari.

Le NVMe de 250 Go est étiqueté et rangé. Il reste intact une à deux semaines — le temps de valider la stabilité du nouveau disque — comme chemin de retour arrière. Parce qu'une migration n'est jamais vraiment terminée tant qu'on n'a pas de quoi l'annuler.