---
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.