Delete old Proxmox VMs without regret
The backup, verify, then destroy-with-purge routine for clearing out stopped guests safely, including the linked-clone check that stops you deleting something a running VM depends on.

Every homelab accumulates guests. The container you built to try something, the VM from a tutorial, the old version of a service you migrated away from. Eventually most of the guest list is grey stopped icons, and the nightly backup job is dutifully copying all of them.
Cleaning house is easy to put off because deleting things is scary. The way to make it not scary is a routine where every single guest you delete is recoverable: back it up, verify the backup, then destroy with purge.
The trap: stopped is not the same as unused
A template is always stopped. A container that was turned into the base of linked clones is always stopped. Delete the base and the clones keep running until the next reboot, and then they do not.
So before you delete anything, find out what is actually unused.
Step 0: check for linked clones
For every stopped guest, check whether any config references its disks as a base:
for id in $(pct list | awk 'NR>1 && $2=="stopped"{print $1}') \
$(qm list | awk 'NR>1 && $3=="stopped"{print $1}'); do
if grep -lq "base-${id}-" /etc/pve/nodes/*/*/*.conf 2>/dev/null; then
echo "KEEP $id: base for linked clones"
else
echo "candidate $id"
fi
done
Tag anything marked KEEP as keep in the GUI and it is out of the discussion. Then walk the candidates with a human eye: the name, the notes, the last-modified date of its config. If you are not sure what a guest is, start it, look, stop it. Ten minutes per mystery guest is cheaper than one wrong destroy.
Step 1: back it up, in stop mode
A stopped guest does not need snapshot mode, and stop mode gives the most consistent archive. Write it to your backup storage with vzdump so the result is one portable file, and give it a note that will mean something in a year:
vzdump 212 --storage nas-backup --mode stop --compress zstd --bwlimit 60000 \
--notes-template 'DELETED {{guestname}} ({{vmid}}) cleanup 2026-10'
Watch the backup storage's free space as you go. Work in order of "would be saddest to lose", and if the share gets tight, prune old nightly backups to make room before carrying on. The bandwidth limit keeps your busy guests from lagging while the dumps run; it costs a few minutes per guest.
Step 2: verify the archive
A backup you have not at least integrity-checked is a rumour. For a container archive:
zstd -t /mnt/pve/nas-backup/dump/vzdump-lxc-212-2026_10_09-21_14_03.tar.zst
VM archives are .vma.zst files and the same zstd -t works. This proves the file is whole and decompresses. It does not prove the guest boots. For anything you would truly mourn, go further: restore it to a new ID and start it with the network unplugged.
Then mark the archive protected, so the nightly prune cannot eat it:
F=vzdump-lxc-212-2026_10_09-21_14_03.tar.zst
pvesh set /nodes/pve1/storage/nas-backup/content/nas-backup:backup/$F \
--protected 1
Step 3: destroy, with purge
pct destroy 212 --purge
# or
qm destroy 212 --purge
--purge removes the guest from every backup job, the replication config and HA. Without it, the nightly backup job keeps a reference to a guest that no longer exists and fails with an error about that ID every night until you edit the job by hand.
The loop
For more than a handful of guests, put the IDs in a file (one per line, after the human pass) and let a script do the three steps:
#!/bin/bash
set -u
STORE=nas-backup
DUMP=/mnt/pve/$STORE/dump
NOTE="DELETED {{guestname}} ({{vmid}}) cleanup $(date +%Y-%m)"
while read -r id; do
if pct config "$id" >/dev/null 2>&1; then kind=lxc; cmd=pct
else kind=qemu; cmd=qm; fi
echo "== $id ($kind)"
vzdump "$id" --storage $STORE --mode stop --compress zstd \
--bwlimit 60000 --notes-template "$NOTE" \
|| { echo "backup FAILED $id"; continue; }
f=$(ls -t $DUMP/vzdump-$kind-$id-*.zst | head -1)
zstd -t "$f" || { echo "verify FAILED $id"; continue; }
vol="$STORE:backup/$(basename "$f")"
pvesh set /nodes/$(hostname)/storage/$STORE/content/$vol --protected 1
$cmd destroy "$id" --purge || echo "destroy FAILED $id (retry later)"
done < /root/cleanup-ids.txt
Every continue means a failure leaves the guest in place. Nothing is ever destroyed without a verified archive ahead of it.
Two gotchas
Cluster quorum. On a two-node cluster, if one node drops off, destroys on the cluster fail with a quorum error while it is out. Re-run the failed IDs once the cluster is quorate again. The proper fix is a QDevice, so the cluster has a third vote.
Leftover locks. Proxmox will not back up or destroy a guest while a lock from an earlier interrupted job is set. pct unlock 212 or qm unlock 212 clears it, once you have checked nothing is actually running against the guest.
Afterwards: the orphans
With the guests gone, there is a second layer of junk:
- Orphan disks: entries in
pvesm list local-zfsthat no config references. Compare againstgrep -h 'local-zfs:' /etc/pve/nodes/*/*/*.conf. Back each one up as a raw image before removing it, same rule. - Old ISOs and container templates in
local. They download again in minutes. - Year-old snapshots on running guests (
qm listsnapshot <id>,pct listsnapshot <id>). A snapshot that old is a backup you forgot, on the same disk as the data, slowing every write.
Checklist
- Every stopped guest checked for linked clones (
base-<id>-in any config); templates tagged keep - A stop-mode vzdump of each candidate with a "DELETED" note, bandwidth-limited
zstd -ton every archive, and the archive marked protecteddestroy --purge, never plain destroy- Failed steps leave the guest in place
- Orphan disks, old ISOs and year-old snapshots cleaned in a second pass


