The 99% Root Disk: Common Homelab Storage Issues and Fixes

The morning my Proxmox root disk hit 99%, and the thin-pool traps and backup bloat behind most homelab storage crises — diagnosis, real fixes, prevention.

On this page
  1. Issue 1: The root disk that eats itself
  2. Issue 2: The thin pool’s one-way door
  3. Issue 3: Backup bloat, in its three disguises
  4. Disguise 1: The job that died and told no one
  5. Disguise 2: The ghost data your backups faithfully preserve
  6. Disguise 3: The backup that will never rotate out
  7. The five-minute audit
  8. What’s next

One July morning I checked a cluster node and found its root filesystem at 99% — 403 MB free on a 40 GB root volume that had nothing wrong with it except my own defaults. Nothing had crashed yet; that’s the unsettling part. Storage problems in a homelab are almost never loud — they’re a slow creep you discover one df too late.

This is Part 1 of the Common Homelab Issues series, and it starts with the three storage classics: the root disk that fills itself, the thin pool with a one-way door, and backup bloat in its several disguises. Each one comes with the diagnosis, the actual fix I used, and the prevention that makes it a non-event next time.

First: make these values your own

The examples below use my values — swap them for yours before copying anything:

  • pve — the name of the volume group, LVM’s pot of raw disk space that everything else is carved from (Proxmox VE’s installer default). Check yours with vgs.
  • pvelab01/pvelab03, CT 102 — my node names and an example container ID.
  • /mnt/backup, backup — where I mount the dedicated backup volume, and my name for it and its storage entry; call yours anything.
  • Sizes and thresholds (100G, +15G, 80%) — sized to my disks. Fit them to yours.

Rule of thumb: if a value looks specific to one machine, it’s a placeholder to change — not a literal to copy.


Issue 1: The root disk that eats itself

Here’s the trap, and it ships enabled: a stock Proxmox install has a storage called local that lives at /var/lib/vzon the root filesystem — and it’s the default home for backups, ISOs, and templates. Meanwhile the big storage, the local-lvm thin pool — a stretch of disk that only hands out space as guests actually write to it, covered in my storage guide — holds only guest disks. So every nightly backup lands on the same small filesystem the operating system needs to breathe, and the two quietly race each other to zero.

Why the small tank fills firstnightly vzdump jobroot filesystem /40 GB — the OS lives here/var/lib/vz/dump — backups pile up HERElocal-lvm thin poolthe big storage — guest disks onlysitting nearly empty the whole timethe default writes backups to the small tank the OS lives in — not the big one next to it
1Diagnose: find out who's eating the disk5 min
Confirm it's root, then find the culprit

df -P /                    # how bad is it?
du -xsh /var/lib/vz/*      # -x stays on this filesystem
du -xsh /var/log /var/cache /root

On my two worst nodes — pvelab01 and pvelab03 — the answer was immediate: 29 GB and 31 GB of backups from vzdump, Proxmox’s built-in backup tool (the thing your scheduled backup job actually runs), sitting in /var/lib/vz/dump. The -x flag matters — it stops du from wandering into mounted storage and blaming the wrong disk.

2Fix: give backups their own home (my actual fix)20 min

The elegant part: the thin pool that was sitting nearly empty is the spare space — you just can’t hand it back to the root filesystem directly. What you can do is carve a thin volume out of it and move backups there:

A dedicated backup volume from the thin pool

lvcreate -V 100G --thin -n backup pve/data   # thin: only consumes what's written
mkfs.ext4 /dev/pve/backup
mkdir -p /mnt/backup
cp /etc/fstab /etc/fstab.bak                 # always, before editing fstab
echo '/dev/pve/backup /mnt/backup ext4 defaults,nofail 0 2' >> /etc/fstab
mount /mnt/backup && mkdir -p /mnt/backup/dump
mv /var/lib/vz/dump/* /mnt/backup/dump/      # keep the existing backups

Two of those lines earn a why: /etc/fstab is the file Linux reads at boot to decide what gets mounted where — hence the backup copy first — and nofail means a missing backup volume won’t stop the node from booting. Then register it as a storage and point the backup job at it — with one flag doing the quiet heavy lifting:

Register the storage — is_mountpoint is the safety net

pvesm add dir backup --path /mnt/backup --content backup --is_mountpoint yes

The is_mountpoint option tells Proxmox this path is only valid when something is mounted there. If the volume ever fails to mount, the storage goes inactive and backups fail loudly — instead of silently writing to the empty directory on the root filesystem and re-creating this whole problem behind your back. Finally, edit your backup job (Datacenter → Backup) to target the new storage.

3Reclaim: grow root if your volume group has headroom5 min
Check for spare space, then extend root online

vgs -o vg_name,vg_free        # any unallocated space in the VG?
lvextend -r -L +15G pve/root  # -r grows the filesystem too; no reboot

My volume groups each had 16 GB never allocated to anything — installer leftovers. Two commands later the worst node went from 99% to 13%, the other from 78% to 14%, online, without a reboot.


Issue 2: The thin pool’s one-way door

The fix above leans on how thin provisioning works, so let’s make the mental model explicit — because the thin pool has two properties that surprise everyone at the worst time.

You can’t shrink it. LVM’s thin-provisioning documentation covers extending pools in a dozen ways and shrinking them in none — there is no supported reduction. If the installer gave local-lvm most of your disk (it did), you can’t hand that space back to the root filesystem later. That’s exactly why Issue 1’s fix creates a thin volume inside the pool instead of resizing anything: thin volumes claim space only as data is actually written, so a “100 GB” backup volume costs the pool nothing until backups land on it.

Promises can exceed reality. Thin volumes let you promise guests more space than physically exists — and the Proxmox storage docs are blunt about the failure mode: “If a storage runs full, all guests using volumes on that storage receive IO errors. This can cause file system inconsistencies and may corrupt your data.” Per the LVM docs, when the pool’s data space runs out, writes are queued for about 60 seconds hoping for an extension, then start erroring. Every guest on the pool gets hurt at once — it’s the closest thing a homelab has to a shared single point of failure hiding inside “free space”.

Over-provisioning: promises vs the poolCT 101 — “32 GB”promisedCT 102 — “32 GB”promisedCT 103 — “32 GB”promisedphysical pool — 64 GB real80% — extend before here100% — writes queue ~60 s, then error96 GB promised against 64 GB real — fine, until the promises get used

The defenses are simple and boring, which is the best kind:

Watch the two numbers that matter

lvs pve/data -o lv_name,data_percent,metadata_percent

The LVM docs say to extend the pool before either value reaches 100% — and note it’s two values: the pool also keeps metadata, its own bookkeeping of which blocks belong to which guest, and a full metadata volume bites just as hard as full data while almost nobody watches it. For a self-tending setup, LVM can auto-extend the pool into unallocated volume-group space once usage crosses a threshold (thin_pool_autoextend_threshold in lvm.conf; leaving it at 100 turns the feature off). That’s a good reason to deliberately leave a slice of your volume group unallocated, like the 16 GB per node that saved me in Issue 1.

That covers the failures you can watch coming on a graph. The next one hides in the directory that always looks healthiest.


Issue 3: Backup bloat, in its three disguises

Backups are where storage problems go to hide, because a backup directory almost never looks broken. Three different mechanisms, all found on my own cluster.

Disguise 1: The job that died and told no one

During the Issue-1 cleanup I noticed something worse than full disks: one node’s newest backup was eleven days old, and the nightly job had “run” every night since. The cause: a backup had crashed mid-snapshot and left a half-deleted snapshot — the snapshot volume was gone, but the guest’s config still carried a [vzdump] snapshot section and a snapshot-delete lock. Every backup after that failed instantly with “CT is locked”, and the old backups just sat there, looking reassuring.

Eleven nights of silent failurelast good backupcrash leaves snapshot + lock“CT is locked” — every night, for eleven nightsfound it — by accidentthe alert that would have caught it: newest backup AGE, not job statusa dead backup job doesn’t delete your old backups — it just stops making new ones

The fingerprint and the fix (commands documented in pct(1)):

Diagnose and clear a stale vzdump snapshot lock

grep -H '^lock:' /etc/pve/lxc/*.conf          # who's locked?
grep -l '^\[vzdump\]' /etc/pve/lxc/*.conf    # who has a leftover snapshot?

pct unlock 102
pct delsnapshot 102 vzdump --force            # clears the orphaned entry
vzdump 102 --storage backup --mode snapshot --compress zstd   # prove it works NOW

The --force flag is what handles the half-deleted state — it removes the config entry even though the snapshot volume behind it is already gone (the volume-removal error it prints is expected). In the final command, --mode snapshot backs the container up while it keeps running and zstd is the fast compressor — both simply match what the nightly job does, so this test proves the same path the job takes. And the durable lesson sits in that diagram: “I have backups” is worthless without “and the newest one is from last night.” Alert on backup ageUptime Kuma can do it with a push monitor, and the playbook below checks it for you.

Disguise 2: The ghost data your backups faithfully preserve

The day after the root-disk rescue, a backup volume was flagged at 87% and the culprit was stranger: 22 GB backups of a container that held 9.5 GB. I had just cleaned a pile of orphaned AI model weights out of that container — but every backup taken before the cleanup still carried them, and model weights are incompressible, so compression didn’t blunt it. A backup preserves the mess exactly as it was. The fix is pleasantly dull: after any big cleanup inside a guest, take one fresh manual backup (so the reclaim is actually reflected), verify it, then delete the bloated pre-cleanup ones — pvesm free <volume-id> (pvesm is Proxmox’s storage-management command) or the web UI, your choice.

Disguise 3: The backup that will never rotate out

Subtler still: retention (keep-last and friends, see the vzdump docs) prunes per guest, per storage, when that guest’s backup runs there. Migrate a container to another node and its old backups on the previous node’s storage are orphans — no job ever touches them again, so no rotation ever removes them. I found one quietly holding space weeks after its container had moved. When a backup volume creeps upward for no visible reason, look for backups of guests that no longer live on that node.


The five-minute audit

Everything above condenses into one read-only script: the Proxmox storage health check playbook checks your root filesystem, both thin-pool percentages, whether backups are living on the root filesystem, the age of your newest backup, and the stale-snapshot/lock fingerprints from Disguise 1 — printing OK/WARN per check and exiting non-zero if anything’s wrong, so cron can run it and complain on your behalf. I ran it against my own cluster while writing this post; it’s how I know the July fixes stuck.

The prevention checklist, in full:

  • Backups live anywhere but the root filesystem — and the storage is registered with is_mountpoint yes so a mount failure fails loud.
  • An alert on newest-backup age, not just job status.
  • Eyes on data_percent and metadata_percent — extend before either nears 100.
  • A deliberate slice of unallocated space in the volume group, for autoextend or a rainy day.
  • One fresh manual backup after any big cleanup inside a guest, then prune the pre-cleanup ones.
  • After migrating a guest, check its old node for orphaned backups.
  • The real endgame: backups that leave the node entirely — Proxmox Backup Server or a NAS target — because a backup volume sharing physical disks with the guests it protects is a compromise, not a destination.

What’s next

This is Part 1 of the Common Homelab Issues series — Part 2 tackles the networking failures (the ones where everything is “up” and nothing can talk), and Part 3 covers the day a service actually goes down — and takes the rescue tools with it. And whatever your storage layout, none of it counts until you’ve watched a restore actually complete — a lesson that deserves its own post, and got one.

One last nudge before you go: every name, path, size, and threshold above was an example. If a value looks specific to one machine, it’s a placeholder to change — not a literal to copy.


Interactive: describe your symptom, get a diagnosis

For issues beyond these classics, the Homelab Troubleshooter takes a plain-English description of what’s broken and returns ranked likely causes with the exact commands to check them, and Log Triage does the same for a wall of log output. Both need a model behind them: run them free on claude.ai with the account you already have, or point them at your own provider — OpenAI, Gemini, DeepSeek and others — or at a model on your own hardware. Whichever you pick, what you type goes to that provider and never to me.

Related posts:

Comments

Comments are powered by GitHub Discussions — sign in with a GitHub account to join the conversation.