TL;DR: Power off all VMs → (optionally) stop or mask libvirt sockets so nothing auto-starts → rsync both /etc/libvirt and /var/lib/libvirt to /mnt/usb → (optional) generate checksums → (optional) do incrementals with --link-dest.
Scope: Backup-only. No restore steps.
What this backs up (and why)
This is a file-copy cold backup (no virsh dumpxml):
- All libvirt config: /etc/libvirt/
 (domains inqemu/, networks, storage,secrets/,hooks/,nwfilter/, …)
- All libvirt data: /var/lib/libvirt/- Disk images: /var/lib/libvirt/images/(.qcow2,.img,.raw, ISO)
- UEFI NVRAM: /var/lib/libvirt/qemu/nvram/(critical for UEFI VMs)
- Snapshots/boot: /var/lib/libvirt/qemu/snapshot/,/var/lib/libvirt/boot/
- Runtime state: e.g., dnsmasq/(harmless in a full copy)
 
- Disk images: 
If any VM disks live outside /var/lib/libvirt, add those paths to your backup.
Prerequisites
- Ubuntu 24.04 with KVM/Libvirt
- External drive mounted at /mnt/usb- Prefer ext4/xfs to preserve ACLs, xattrs, and sparse files (NTFS/FAT will mangle these).
 
- Confirm mount:
mountpoint -q /mnt/usb && df -h /mnt/usb
- Power off all VMs (cold backup)
Graceful first; force off if needed.
# Graceful shutdown
for d in $(virsh list --state-running --name); do
  virsh shutdown "$d"
done
# Wait up to 60s
for i in {1..60}; do
  [ -z "$(virsh list --state-running --name)" ] && break
  sleep 1
done
# Force off if still running
for d in $(virsh list --state-running --name); do
  virsh destroy "$d"
done
# Sanity: no qemu processes (OK if empty)
pgrep -a qemu-system || true
- Prevent auto-start via socket activation
Ubuntu 24.04 uses systemd socket activation. Stopping services alone may not be enough. Stop sockets first; for a “no surprises” window, mask them temporarily.
# Stop sockets so libvirt can't respawn
sudo systemctl stop libvirtd.socket libvirtd-ro.socket libvirtd-admin.socket 2>/dev/null || true
sudo systemctl stop virtlogd.socket virtlogd-admin.socket virtlockd.socket virtlockd-admin.socket 2>/dev/null || true
# Stop services (ignore "not loaded")
sudo systemctl stop libvirtd virtlogd virtlockd 2>/dev/null || true
# (Stricter) temporarily mask sockets during backup
sudo systemctl mask libvirtd.socket libvirtd-ro.socket libvirtd-admin.socket
sudo systemctl mask virtlogd.socket virtlogd-admin.socket virtlockd.socket virtlockd-admin.socket
Quick checks (optional):
systemctl is-active libvirtd virtlogd virtlockd || true
systemctl is-active libvirtd.socket libvirtd-ro.socket libvirtd-admin.socket || true
systemctl is-active virtlogd.socket virtlogd-admin.socket virtlockd.socket virtlockd-admin.socket || true
ss -lntp | egrep 'libvirt|virt(log|lock|qemu)' || true
After your backup finishes, unmask + start to resume normal operation:
sudo systemctl unmask libvirtd.socket libvirtd-ro.socket libvirtd-admin.socket
sudo systemctl unmask virtlogd.socket virtlogd-admin.socket virtlockd.socket virtlockd-admin.socket
sudo systemctl start libvirtd
- Create a dated destination
DEST_ROOT="/mnt/usb"
DATE="$(date +%F_%H%M%S)"
DEST="$DEST_ROOT/kvm-full-backup-$DATE"
sudo mkdir -p "$DEST"/{etc-libvirt,var-libvirt}
sudo chmod 700 "$DEST"
- Full copy with rsync
Use -aHAX to preserve ownership/ACL/xattrs and --sparse to keep qcow2/raw “holes” compact.
# 1) All libvirt definitions & config
sudo rsync -aHAX --info=progress2 \
  /etc/libvirt/  "$DEST/etc-libvirt/"
# 2) All libvirt data (images, NVRAM, snapshots, dnsmasq, etc.)
sudo rsync -aHAX --sparse --info=progress2 \
  /var/lib/libvirt/  "$DEST/var-libvirt/"
sync
echo "✅ Full backup complete: $DEST"
- Generate checksums for images
Helpful for integrity verification later.
sudo find "$DEST/var-libvirt/images" -type f \
  \( -name '*.qcow2' -o -name '*.img' -o -name '*.raw' -o -name '*.iso' \) -print0 \
  | sudo xargs -0 -r sha256sum | sudo tee "$DEST/IMAGES_SHA256SUMS.txt" >/dev/null
