--- title: "How to Live Migrate KVM Virtual Machines with virsh" description: "Move running KVM guests between two Ubuntu 24.04 hosts with virsh migrate and shared NFS storage, check CPU compatibility, and tune downtime and bandwidth." category: "Virtualization VPS" language: en source: https://cubepath.com/docs/virtualization-vps/live-vm-migration updated: 2026-09-25 --- # How to Live Migrate KVM Virtual Machines with virsh 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 them `host1` (source) and `host2` (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_user` on both hosts, with `sudo` privileges and membership in the `libvirt` group. - An NFS server reachable from both hosts, called `storage` here. 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 `vm1` whose 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`: ```bash sudo nano /etc/hosts ``` ```text 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: ```bash 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`: ```bash 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: ```bash virsh -c qemu+ssh://your_user@host2/system list --all ``` ```output 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: ```bash 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: ```bash 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: ```bash sudo apt install nfs-kernel-server sudo mkdir -p /srv/vmstore ``` Edit `/etc/exports`: ```bash sudo nano /etc/exports ``` ```text /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: ```bash sudo exportfs -ra sudo exportfs -v ``` On **both** hosts, install the NFS client: ```bash sudo apt install nfs-common ``` Then, on **both** hosts, define a libvirt storage pool that mounts the export. Create the pool definition: ```bash nano ~/vmstore-pool.xml ``` ```xml vmstore /var/lib/libvirt/images/vmstore ``` ```bash 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: ```bash findmnt /var/lib/libvirt/images/vmstore ``` ```output 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: ```bash 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 `` block, change the `` path to `/var/lib/libvirt/images/vmstore/vm1.qcow2`, and set `cache='none'` on the `` line: ```xml ``` 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: ```bash 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: ```bash sudo virsh dumpxml vm1 | grep -A3 ' ~/caps-$(hostname).xml ``` Copy both files to one host and compute the baseline: ```bash cat ~/caps-host1.xml ~/caps-host2.xml > ~/caps-all.xml sudo virsh cpu-baseline ~/caps-all.xml ``` The output is a `` 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: ```bash virsh -c qemu:///system migrate --live --persistent --undefinesource --verbose \ vm1 qemu+ssh://your_user@host2/system ``` What the flags do: - `--live` keeps the guest running during the copy. - `--persistent` defines the VM on `host2` so it survives a restart there. - `--undefinesource` removes the definition from `host1` so the VM is not accidentally started twice. - `--verbose` prints progress. ```output Migration: [100 %] ``` Confirm the guest now runs on the destination and is gone from the source: ```bash virsh -c qemu+ssh://your_user@host2/system list virsh -c qemu:///system list --all ``` ```output 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: ```bash virsh -c qemu:///system domjobinfo vm1 ``` ```output 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: ```bash 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: ```bash virsh -c qemu:///system migrate-postcopy vm1 ``` > Warning: During 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: ```bash 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.