Two backups had been running against the same data, and I only noticed when the numbers moved. The nightly whole-VM snapshot — the entire 32 GB virtual disk compressed down to a single file at 2am, the disaster-recovery copy — had gone 4.77 GB, then 4.79 GB, then 6.17 GB in three days. The second backup was the hosted application's own compressed archives, the ones it keeps inside the VM for quick undo.
The shape of the problem is obvious once it's written down. Those archives live inside the very machine the snapshot captures, so every nightly snapshot was swallowing them too: the same bytes stored twice, sharing one point of failure.
Where the growth came from
You cannot compress the same data twice. Compression works by finding patterns, and once a file has been compressed there are none left, so an archive of about 820 MB lands in the snapshot at its full 820 MB. Two of those is roughly 1.6 GB added to every nightly snapshot, and the application was keeping seven.
Two costs made it urgent. The object storage free tier was 10 GB and the nightly dump was closing on it, at which point uploads start costing money. And 4.8 GB already took 34 minutes to upload, so 10 GB would be over an hour every night, on a link that had already failed twice. The redundancy was the part that bothered me most — a second copy living inside the thing it is supposed to protect.
The fix that isn't available
The obvious answer is to tell the hypervisor to skip that folder. I suggested exactly that, and I was wrong. The backup copies disk images, not files; it never looks inside the filesystem, so there is nothing to skip. File-level exclusion exists for container-type guests. For a virtual machine the only lever is at the disk level.
So: give the guest a second virtual disk, added with its backup option off, and move the archives onto it.
Mechanically that's four operations people tend to lump into one. Carve 16 GB out of the host's single 256 GB NVMe and present it to the guest as a whole new disk. Partition it — GPT, one partition spanning the disk, because even a single region covering the whole disk has to be declared. Format it — ext4, and this is the destructive step, which is why the setup script refuses to format a disk that already carries a filesystem unless a force flag is set. Mount it over the folder the archives already use, and write the mount table entry so it comes back by itself.
Before any of it, the flag. The disk's "don't back this up" marker had to be confirmed present in the guest's disk configuration before the setup script ran. Without it, the whole exercise would have added 16 GB that was still being backed up — strictly worse than doing nothing. Verify the thing that makes the work worthwhile before doing the work.
The bug I actually hit
This is the part worth coming back to. The backups folder already held about 1.7 GB of archives on the original disk. Mounting the new, empty filesystem over that folder does not delete them. It hides them, the new filesystem laid on top like a sheet over furniture.
The listing looked almost empty. df reported about 2.1 MB used, which was true about the new disk and worthless as a diagnosis. The 1.7 GB was still on the original disk, still consuming space, invisible.
The tell is the thing to remember: the files appear to be gone and the old disk's free space does not go up. Data does not vanish silently — files can't disappear without space being reclaimed, so if the listing lost them and the space didn't come back, something is covering them. To look underneath, unmount, or bind-mount the root filesystem somewhere else and read it there. Mine were intact the whole time.
Recovery went: unmount, find the originals, copy them onto the new disk, verify with checksums, fix ownership, and keep the originals in a staging folder rather than deleting them. Checksums rather than sizes, because size only catches truncation while a copy can report success and still be corrupt. Ownership, because anything a sudo-run setup script creates belongs to root while the application runs as a different, non-root user — a mismatch that doesn't fail at setup, it fails at the next backup, days later, when nobody is watching.
Then the reboot, which is the whole point. A wrong mount table entry looks perfect until the machine comes up, and then it doesn't. That's why a snapshot came first and why rebooting to test was mandatory rather than optional. The reboot came back with the mount in place.
Where it settled: 16 GB filesystem, about 1.7 GB used, roughly 14 GB free, 11%. One detail I nearly missed — the staging copy had to be deleted too, because it was still sitting on the original disk and still counting toward the exact dump growth I was trying to stop. Staging is safety, not storage.
Still open
The dump should fall from 6.17 GB back toward 4.8 GB. That was an expectation to confirm the next day, not something I had measured. Memory sat at 78% (6.27 of 8 GiB) after four days uptime, some of it disk cache, and whether the application's resident set is climbing is genuinely unresolved. Two network outages, both fixed by reseating a new cable, cause still undetermined — next time, note whether the guest is unreachable too and whether the router admin page still loads over WiFi, which at least narrows it to cable, router port or NIC.
The backlog I didn't touch is mostly security: staged SSH hardening, and the still-open decision about encrypting backups before upload, which now has a written reason behind it — the snapshot contains credential and identity material plus API keys for several providers. There's also an unexplained older access token with broad read-only permissions to chase down and a leftover installer ISO still attached to the guest's virtual CD drive.
If I keep only two lines from this session: verify the flag that makes the work worthwhile before starting, and when files disappear but the space doesn't come back, look under the mount instead of believing the listing.