Live migration moves a running virtual machine from one KVM host to another while the guest keeps running. QEMU copies the guest's memory to the destination in the background, repeatedly re-sends pages the guest changes, and then pauses the VM for a fraction of a second to transfer the last dirty pages and CPU state. In this tutorial you will prepare two Ubuntu 24.04 hosts, give them shared NFS storage, migrate a running guest with virsh migrate, and learn the options that control downtime and bandwidth.
Prerequisites
To follow this guide you need:
- Two physical or bare metal servers running Ubuntu 24.04 LTS with KVM and libvirt installed (
qemu-kvm,libvirt-daemon-system,libvirt-clients). This guide calls themhost1(source) andhost2(destination). - The same Ubuntu release and the same QEMU and libvirt package versions on both hosts. Migrating to an older QEMU is not supported.
- A non-root user called
your_useron both hosts, withsudoprivileges and membership in thelibvirtgroup. - An NFS server reachable from both hosts, called
storagehere. It can be a third server or a NAS. - A private network between the hosts, ideally 10 Gbit/s. Migration traffic is not encrypted by default.
- A running guest called
vm1whose disk you will place on the shared storage.
Step 1 - Preparing name resolution and SSH access
virsh connects to the destination over SSH, and the source QEMU then opens a direct TCP connection to the destination using the destination's hostname. Both hosts must resolve each other's names to their private addresses. On both hosts, edit /etc/hosts:
sudo nano /etc/hosts
10.0.0.11 host1
10.0.0.12 host2
10.0.0.20 storage
Replace the addresses with your private IPs. Check that hostname on each host returns exactly host1 or host2, since libvirt announces that name to the peer.
Next, add your_user to the libvirt group on both hosts if you have not already, then log out and back in:
sudo usermod -aG libvirt your_user
On host1, create an SSH key for your_user (skip if you already have one) and copy it to host2:
ssh-keygen -t ed25519
ssh-copy-id your_user@host2
Verify that you can open a libvirt connection to host2 from host1 without a password prompt:
virsh -c qemu+ssh://your_user@host2/system list --all
Id Name State
--------------------
An empty list with no error means the remote connection works. In the rest of this guide, virsh commands on the source run as your_user with -c qemu:///system, so the SSH connection uses that user's key.
Step 2 - Opening the migration ports
The destination QEMU listens for migration data on a port in the range 49152-49215 by default. If UFW is enabled, allow SSH and that range from the other host on both servers:
sudo ufw allow from 10.0.0.11 to any port 22 proto tcp
sudo ufw allow from 10.0.0.12 to any port 22 proto tcp
sudo ufw allow from 10.0.0.0/24 to any port 49152:49215 proto tcp
Adjust the addresses to your private subnet. Check the result:
sudo ufw status
Step 3 - Setting up shared NFS storage
With shared storage, only memory and CPU state cross the network, which keeps migrations fast. Both hosts must see the disk image at the same path.
On the storage server, install the NFS server and create the export directory:
sudo apt install nfs-kernel-server
sudo mkdir -p /srv/vmstore
Edit /etc/exports:
sudo nano /etc/exports
/srv/vmstore 10.0.0.11(rw,sync,no_subtree_check,no_root_squash) 10.0.0.12(rw,sync,no_subtree_check,no_root_squash)
no_root_squash is needed because libvirt, running as root, changes the ownership of disk images when it starts a guest. Restrict the export to your hypervisors only. Apply it:
sudo exportfs -ra
sudo exportfs -v
On both hosts, install the NFS client:
sudo apt install nfs-common
Then, on both hosts, define a libvirt storage pool that mounts the export. Create the pool definition:
nano ~/vmstore-pool.xml
<pool type='netfs'>
<name>vmstore</name>
<source>
<host name='storage'/>
<dir path='/srv/vmstore'/>
<format type='nfs'/>
</source>
<target>
<path>/var/lib/libvirt/images/vmstore</path>
</target>
</pool>
sudo virsh pool-define ~/vmstore-pool.xml
sudo virsh pool-build vmstore
sudo virsh pool-start vmstore
sudo virsh pool-autostart vmstore
Confirm the mount:
findmnt /var/lib/libvirt/images/vmstore
TARGET SOURCE FSTYPE OPTIONS
/var/lib/libvirt/images/vmstore storage:/srv/vmstore nfs4 rw,relatime,...
Move vm1's disk to the pool on host1. Shut the guest down, copy the image, and point the VM at the new path:
sudo virsh shutdown vm1
sudo cp --sparse=always /var/lib/libvirt/images/vm1.qcow2 /var/lib/libvirt/images/vmstore/
sudo virsh edit vm1
In the <disk> block, change the <source file=.../> path to /var/lib/libvirt/images/vmstore/vm1.qcow2, and set cache='none' on the <driver> line:
<driver name='qemu' type='qcow2' cache='none' discard='unmap'/>
<source file='/var/lib/libvirt/images/vmstore/vm1.qcow2'/>
Libvirt refuses to live migrate a guest whose shared disk uses a host page cache, because the destination could read stale data. Start the guest again:
sudo virsh start vm1
Step 4 - Checking CPU compatibility
The guest sees a virtual CPU model. After migration, the destination must be able to offer exactly the same CPU features, or the migration fails before it starts.
Check the guest's CPU mode:
sudo virsh dumpxml vm1 | grep -A3 '<cpu'
host-passthroughexposes the physical CPU as-is. It only migrates safely between hosts with identical CPU models and microcode.host-model(thevirt-installdefault) copies the source host's CPU model at start time. It migrates to hosts with the same or a newer CPU generation.- A named model such as
Skylake-Server-IBRSorEPYC-Milanexposes a fixed feature set and gives the most predictable results across mixed hardware.
For hosts with different CPUs, compute a model both can run. On each host, save its capabilities:
sudo virsh capabilities > ~/caps-$(hostname).xml
Copy both files to one host and compute the baseline:
cat ~/caps-host1.xml ~/caps-host2.xml > ~/caps-all.xml
sudo virsh cpu-baseline ~/caps-all.xml
The output is a <cpu> block you can paste into the guest with virsh edit. Changing the CPU model requires a full guest shutdown and start.
Step 5 - Migrating the running VM
On host1, start a continuous ping to the guest from another machine so you can see how much connectivity is lost. Then run the migration:
virsh -c qemu:///system migrate --live --persistent --undefinesource --verbose \
vm1 qemu+ssh://your_user@host2/system
What the flags do:
--livekeeps the guest running during the copy.--persistentdefines the VM onhost2so it survives a restart there.--undefinesourceremoves the definition fromhost1so the VM is not accidentally started twice.--verboseprints progress.
Migration: [100 %]
Confirm the guest now runs on the destination and is gone from the source:
virsh -c qemu+ssh://your_user@host2/system list
virsh -c qemu:///system list --all
Id Name State
----------------------
3 vm1 running
The ping should show at most one or two lost packets. To move the guest back, run the same command on host2 pointing at host1 (copy an SSH key in that direction too).
Step 6 - Monitoring and tuning a migration
While a migration runs, open a second terminal on host1 and watch the job:
virsh -c qemu:///system domjobinfo vm1
Job type: Unbounded
Operation: Outgoing migration
Time elapsed: 8214 ms
Data processed: 3.102 GiB
Data remaining: 402.551 MiB
Memory remaining: 402.551 MiB
Dirty rate: 21344 pages/s
...
A guest that writes to memory faster than the network can transfer it never converges. These options help:
| Option | Effect |
|---|---|
--bandwidth 1000 | Caps migration traffic at 1000 MiB/s, to protect other traffic on the link |
--auto-converge | Throttles the guest's vCPUs progressively until the copy catches up |
--postcopy | Allows switching to post-copy: the VM starts on the destination and pulls missing pages on demand |
--compressed | Compresses repeated pages; useful on slow links, costs CPU |
--timeout 300 --timeout-suspend | Pauses the guest after 300 seconds to force completion |
You can also raise the maximum pause allowed at switchover while the job is running. The value is in milliseconds:
virsh -c qemu:///system migrate-setmaxdowntime vm1 500
For post-copy, start the migration with --postcopy and then, from another terminal, switch modes once the first memory pass is done:
virsh -c qemu:///system migrate-postcopy vm1
WarningDuring post-copy, the guest's memory is split between both hosts. If the network or either host fails before the migration completes, the guest is lost. Use it only on reliable links.
Step 7 - Migrating without shared storage
If the hosts do not share storage, libvirt can copy the disk along with the memory. The destination needs a storage pool at the same path as the source disk (for example the default /var/lib/libvirt/images pool), where libvirt pre-creates the volume:
virsh -c qemu:///system migrate --live --persistent --undefinesource --verbose \
--copy-storage-all vm1 qemu+ssh://your_user@host2/system
This transfers the full disk image, so it takes much longer and puts heavy I/O on both hosts. Once finished, delete the old disk from host1 after confirming the guest runs correctly on host2.
Troubleshooting
unable to connect to server at 'host2:49152': No route to host. The migration port range is blocked. Recheck the UFW rules from Step 2 on the destination.
Unable to resolve address 'host2' or the migration connects to a public IP. The source resolves the destination's hostname through DNS. Fix /etc/hosts on both hosts, or force the data address explicitly with --migrateuri tcp://10.0.0.12.
Unsafe migration: Migration may lead to data corruption if disks use cache other than none. Set cache='none' on every shared disk (Step 3) and restart the guest.
the CPU is incompatible with host CPU: Host CPU does not provide required features. The guest's CPU model is newer than the destination supports. Use a baseline model from Step 4.
Migration stays at 99 % and never finishes. The guest dirties memory faster than it can be sent. Add --auto-converge, increase the allowed downtime, or use a faster link.
Permission denied on the disk at the destination. The NFS export is squashing root. Confirm no_root_squash in /etc/exports and rerun sudo exportfs -ra.
Conclusion
You configured two KVM hosts with name resolution, SSH access, firewall rules and a shared NFS storage pool, and you live migrated a running guest between them with a near-invisible pause. Next, consider encrypting migration traffic with libvirt's TLS transport (--tls), tuning guest CPU placement with CPU pinning and NUMA, or moving to a cluster manager such as Proxmox VE that automates migration and high availability.
