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