virsh is the command-line client for libvirt, the management layer used by KVM/QEMU hosts. Anything you can do in a graphical tool such as virt-manager, you can do with virsh, which makes it the right tool for headless servers and scripts. In this tutorial you will use virsh on Ubuntu 24.04 to control the lifecycle of a virtual machine, change its resources, add storage, take snapshots, and clone, back up and remove it.
Prerequisites
To follow this tutorial you need:
- An Ubuntu 24.04 server with KVM, QEMU and libvirt installed, and your user in the
libvirtgroup (see our guide on installing KVM and QEMU on Ubuntu 24.04). - At least one VM. The examples use a VM called
vm1created from an Ubuntu cloud image with a qcow2 disk at/var/lib/libvirt/images/vm1.qcow2. Replacevm1with your VM's name. - A non-root user with
sudoprivileges.
Step 1 - Connecting to the right libvirt instance
libvirt has two instances: qemu:///system, where system-wide VMs, networks and storage pools live, and qemu:///session, a per-user instance. When run as a regular user, virsh connects to the session instance by default, which is why a VM you created can seem to "disappear". Check which one you are using:
virsh uri
qemu:///system
If you see qemu:///session, make the system instance your default:
echo 'export LIBVIRT_DEFAULT_URI=qemu:///system' >> ~/.bashrc
source ~/.bashrc
You can also connect to a remote host over SSH with virsh -c qemu+ssh://your_user@your_server_ip/system list. Every virsh subcommand has built-in help, for example virsh help snapshot-create-as.
Step 2 - Listing and inspecting VMs
libvirt calls a virtual machine a domain. List all of them, including the ones that are shut off:
virsh list --all
Id Name State
-----------------------
1 vm1 running
- web2 shut off
Show a VM's resources and state:
virsh dominfo vm1
Id: 1
Name: vm1
UUID: 3f1c9a2e-7b64-4d0a-9e15-2c8b7d6a5f01
OS Type: hvm
State: running
CPU(s): 2
CPU time: 41.3s
Max memory: 2097152 KiB
Used memory: 2097152 KiB
Persistent: yes
Autostart: disable
Managed save: no
Security model: apparmor
Security DOI: 0
Security label: libvirt-3f1c9a2e-7b64-4d0a-9e15-2c8b7d6a5f01 (enforcing)
Three more commands you will use constantly:
virsh domblklist vm1
virsh domiflist vm1
virsh domifaddr vm1
They list the VM's disks, its network interfaces, and the IP address it received from libvirt's DHCP server (add --source agent to domifaddr if the QEMU guest agent runs inside the VM).
Step 3 - Controlling the VM lifecycle
Start a VM that is shut off:
virsh start vm1
Shut it down gracefully. This sends an ACPI power button event, and the guest OS shuts down on its own:
virsh shutdown vm1
If the guest does not respond (for example it is hung), force it off. destroy is the equivalent of pulling the power cable: it does not delete anything, but unsaved data inside the guest is lost:
virsh destroy vm1
Other lifecycle commands:
| Command | Effect |
|---|---|
virsh reboot vm1 | Graceful reboot through the guest OS |
virsh reset vm1 | Hard reset, like pressing the reset button |
virsh suspend vm1 | Pause the vCPUs; the VM stays in RAM |
virsh resume vm1 | Continue a paused VM |
virsh managedsave vm1 | Save RAM to disk and stop; the next start restores it |
To start a VM automatically when the host boots, enable autostart, then check the Autostart line in virsh dominfo vm1:
virsh autostart vm1
Disable it again with virsh autostart --disable vm1.
Step 4 - Using the serial console
VMs created with --graphics none, such as cloud images, expose a serial console. It is how you fix a VM whose network or SSH is broken:
virsh console vm1
Connected to domain 'vm1'
Escape character is ^] (Ctrl + ])
vm1 login:
Press Enter if no prompt appears, and leave the console with Ctrl+].
Step 5 - Changing CPU and memory
libvirt keeps two views of each VM: the live state of a running VM and the persistent config used at the next boot. Options such as --config (next boot) and --live (now) choose which one a change applies to. The simplest and safest way to resize is to change the config and restart.
Shut the VM down and wait until virsh list --all shows it as shut off:
virsh shutdown vm1
Raise the memory to 4 GiB. The maximum must be set first, because the current allocation cannot exceed it:
virsh setmaxmem vm1 4G --config
virsh setmem vm1 4G --config
Raise the vCPUs to 4, again maximum first:
virsh setvcpus vm1 4 --config --maximum
virsh setvcpus vm1 4 --config
Start the VM and verify:
virsh start vm1
virsh dominfo vm1 | grep -E 'CPU\(s\)|memory'
CPU(s): 4
Max memory: 4194304 KiB
Used memory: 4194304 KiB
For changes that have no dedicated command, edit the domain XML directly. virsh edit opens it in your editor and validates it when you save:
virsh edit vm1
Changes made with virsh edit apply at the next full shutdown and start, not on a reboot from inside the guest.
Step 6 - Adding a second disk
Create a 10 GB qcow2 volume for data:
sudo qemu-img create -f qcow2 /var/lib/libvirt/images/vm1-data.qcow2 10G
Attach it to the VM as vdb on the virtio bus. --persistent applies the change to the running VM and to its saved config, so the disk survives a restart:
virsh attach-disk vm1 /var/lib/libvirt/images/vm1-data.qcow2 vdb \
--driver qemu --subdriver qcow2 --targetbus virtio --persistent
Confirm it is attached:
virsh domblklist vm1
Target Source
---------------------------------------------------
vda /var/lib/libvirt/images/vm1.qcow2
vdb /var/lib/libvirt/images/vm1-data.qcow2
Inside the guest, the disk appears as /dev/vdb, ready to partition and format. To remove it later, unmount it in the guest first, then run virsh detach-disk vm1 vdb --persistent.
Step 7 - Working with storage pools and networks
A storage pool is a place libvirt manages volumes in, usually a directory. List pools:
virsh pool-list --all
If there is no default pool, define one on libvirt's standard image directory, start it and enable it at boot:
virsh pool-define-as default dir --target /var/lib/libvirt/images
virsh pool-start default
virsh pool-autostart default
List the volumes it contains:
virsh vol-list default
Virtual networks work the same way. List them and see which addresses the DHCP server handed out:
virsh net-list --all
virsh net-dhcp-leases default
Expiry Time MAC address Protocol IP address Hostname Client ID or DUID
---------------------------------------------------------------------------------------------------------
2026-09-25 12:41:07 52:54:00:6b:3c:58 ipv4 192.168.122.45/24 vm1 ff:...
Step 8 - Taking and restoring snapshots
Snapshots let you return to a known state, for example before a risky upgrade. With qcow2 disks, virsh stores internal snapshots inside the disk file. Create one:
virsh snapshot-create-as vm1 --name before-upgrade --description "Before apt full-upgrade"
If the VM is running, this also saves its memory state, so the VM pauses for a few seconds. List the snapshots:
virsh snapshot-list vm1
Name Creation Time State
------------------------------------------------------
before-upgrade 2026-09-25 11:05:32 +0000 running
Roll back to it:
virsh snapshot-revert vm1 before-upgrade
Delete it when you no longer need it, because snapshots keep growing the disk file:
virsh snapshot-delete vm1 before-upgrade
NoteInternal snapshots are not supported for VMs that boot with UEFI firmware (pflash NVRAM). For those, use external disk snapshots or the backup method in Step 10. A snapshot is also not a backup: it lives in the same file on the same disk.
Step 9 - Cloning a VM
virt-clone, from the virtinst package, copies a VM's configuration and disks and generates a new name, UUID and MAC address. The source VM must be shut off:
virsh shutdown vm1
virt-clone --original vm1 --name vm2 --auto-clone
Allocating 'vm2.qcow2' | 1.2 GB 00:00:02
Clone 'vm2' created successfully.
The guest inside the clone still has the same hostname, SSH host keys and /etc/machine-id as the original. For a lasting copy, reset them in the clone (for example with sudo cloud-init clean --machine-id followed by a reboot on cloud-image guests), or create new VMs from the base image instead of cloning.
Step 10 - Backing up and removing a VM
A simple, consistent backup is the domain XML plus a copy of the disk taken while the VM is shut off. Create a backup directory and save the definition:
sudo mkdir -p /var/backups/vms
virsh dumpxml vm1 | sudo tee /var/backups/vms/vm1.xml > /dev/null
With the VM shut off, export the disk. qemu-img convert produces a standalone qcow2 file, merging in the backing cloud image if the VM uses one, and compresses it:
virsh shutdown vm1
sudo qemu-img convert -c -O qcow2 /var/lib/libvirt/images/vm1.qcow2 /var/backups/vms/vm1.qcow2
virsh start vm1
To restore, copy the disk back to the path referenced in the XML and run virsh define /var/backups/vms/vm1.xml.
To delete a VM you no longer need, make sure it is shut off (use virsh destroy vm2 if it is running), then remove its definition together with all its disks:
virsh undefine vm2 --remove-all-storage
undefine without --remove-all-storage removes only the definition and leaves the disk files in place. For UEFI VMs, add --nvram.
Troubleshooting
error: failed to get domain 'vm1': you are probably connected toqemu:///session. Runvirsh uriand see Step 1.virsh shutdownhas no effect: the guest ignores ACPI events. Install and startqemu-guest-agentin the guest and usevirsh shutdown vm1 --mode agent, or usevirsh destroyas a last resort.- The VM fails to start: read the QEMU log for that VM with
sudo tail -n 50 /var/log/libvirt/qemu/vm1.log, and the daemon log withjournalctl -u libvirtd -n 50. Requested operation is not valid: domain is already runningduring a resize: some changes need--configand a full stop and start of the VM, as shown in Step 5.
Conclusion
You can now manage the whole life of a KVM virtual machine from the command line: start and stop it, resize it, add disks, snapshot, clone, back up and delete it with virsh. Because every operation is a single command, these steps are easy to put into scripts or an Ansible playbook. Next, explore virsh domstats vm1 for CPU, memory and disk counters, set up a bridge network so VMs get public addresses, and look into virsh backup-begin for backups of running VMs.
