- tl;dr
- What’s the use case?
- Why would you like to replace your VirtualBox setup?
- Why not just OS users or containers?
- How to create the setup?
- Base VM update process
- Possible extensions
- Appendix 1: Fix QEMU RAM file location
- Appendix 2: Fix resolution issues
- This is no AI Slop
- Known limitations
- Update 2026-05-11: Learnings after some weeks
tl;dr
In 2026, you can replace VirtualBox for developer VMs – with standard tooling from almost any Linux distro:
By using transient VMs with an overlay disk setup you can reuse the same immutable base OS disk for many VMs. That approach
- saves precious disk space,
- saves SDD write cycles,
- saves update effort,
- avoids drift.
In this article, I’ll give a walkthrough for an example setup on Ubuntu 24.04.
What’s the use case?
At metamorphant, we strictly enforce separation of customer dev environments. Development happens in an isolated environment. Most customers provide virtual desktops or dedicated managed devices. For all other customers with local development on my own device, I used VirtualBox as virtualization hypervisor and ran one or more VMs per customer.
Why would you like to replace your VirtualBox setup?
VirtualBox works reasonably well. Usually, “don’t fix it, if it ain’t broken” is a good guideline. So why replace it?
Reason 1: 3rd party package
VirtualBox is a third-party package. You need to add the additional upstream virtualbox apt repo.
libvirt/KVM/QEMU ship with major Linux distributions out-of-the-box.
Reason 2: Redundancy
In my VirtualBox setup, each VM had a dedicated VDI disk. The same operating system was installed to each dedicated disk – over and again. The same updates had to run once for each VM.
That was a lot of redundancy.
Reason 3: Drift
In my old setup, base OS and userspace data was mixed on the same disk. Unavoidably with a long-living disk, you get configuration drift over time. Drift is the nemesis of automation. Also, drift means you have to check the current state manually, whenever you want to reproduce a VM or delete a VM (without losing data).
In data center and cloud operations we have adopted “cattle, not pets” a long time ago. Why not use it for local dev VMs, too?
Why not just OS users or containers?
You might ask why I want to use proper virtualization instead of more lightweight solutions. The most lightweight solution would probably be separate OS users. The second-lightest solution would be containers or different cgroups+namespace based solutions.
While every security decision is a bet, I assume that VMs provide me with the highest level of isolation and the lowest risk of a guest VM compute payload escaping their boundaries – in essence, the same approach taken by QubesOS.
How to create the setup?
Without further ado, let’s set it up. I assume you are running Ubuntu 24.04.
Install packages on the VM host
On your VM host:
sudo apt update
sudo apt install bridge-utils libvirt-daemon-system qemu-system-x86 virt-manager virt-viewer qemu-utils qemu-system-modules-spice gir1.2-spiceclientgtk-3.0 ovmf cloud-image-utils virtiofsd
bridge-utils: tools for configuring Linux ethernet bridgeslibvirt-daemon-system: libvirt’s backbone – as a system serviceqemu-system-x86: binaries for x86 emulationvirt-manager: A GUI for managing libvirt VMs – similar to VirtualBox’s GUIqemu-utils:qemu-imgfor qcow2 manipulationqemu-system-modules-spiceandgir1.2-spiceclientgtk-3.0: for the virtual display GUIovmf: for UEFI boot in the VMcloud-image-utils: forcloud-initconfiguration image creationvirtiofsd: for virtio-fs shared folders
Initially I also installed qemu-system-gui, but it is not required on the host. It contains guest-only utils.
Add user to groups
In order to allow your user to create VMs, you need to become member of the groups libvirt and kvm.
sudo usermod -aG libvirt $USER
sudo usermod -aG kvm $USER
In Linux, groups are attached to processes and aren’t reloaded from /etc/group (or other sources) in real-time. You’ll have to relogin. Check your groups:
id
Restart libvirtd service
Make sure that libvirtd is enabled:
sudo systemctl enable --now libvirtd
### Check status
sudo systemctl status libvirtd
Don’t be confused, if the service is inactive (dead). On Ubuntu, systemd starts the service lazily when the first client accesses the libvirt control socket – in systemd lingo: it’s “triggered”.
Install base VM
Download the Ubuntu Desktop installation ISO and put it into your local libvirt image dir (e.g. /data/libvirt_images). Create a VM base-vm-ubuntu-24.04 in virt-manager (I chose to perform that one-time step by hand).
- Choose “Local install media (ISO image or CDROM)” -> “Forward”
- Choose “Browse” -> “Browse Local” (bottom button row) -> “Forward”
- Choose memory & CPU (I chose 8192MiB RAM + 4 CPUs)
- Choose “Select or create custom storage” (I assume you don’t want your base VM to live in the default location
/var/lib/libvirt)- Create a separate volume pool – to keep VM images on a different filesystem than the default , e.g. to prevent running out of disk space
- Choose a name (I chose
base-vm-ubuntu-24.04) - “Finish”
- Boot
- Install Ubuntu as usual
- Customize your base VM at your own discretion
apt install ... apt purge ...
For deriving customer VMs later, we will need cloud-init. In my case, the package was already installed.
Optional: Enable 3D Acceleration
Open your base VM in virt-manager.
Step 1: Reconfigure SPICE – this controls how libvirt exposes the virtual display to the host
- Choose “Display Spice”
- Select “Listen type: none” to force the connection over memory sharing instead of through a connection to the SPICE service
- Tick “OpenGL”
- Choose the correct device (on my laptop, I had to pick the internal Intel UHD instead of the additional NVIDIA)

Step 2: Enable 3D acceleration for virtio
- Choose “Video Virtio”
- Select “Type: virtio” (this controls which virtual graphics card your guest will see)
- Tick “3D acceleration”

Create a customer VM template
- Create a customer VM directory
test1(in reality, choose a better naming scheme, e.g. the customer or project name)mkdir -p /data/libvirt_images/test1 - Create an XML definition for the customer VM
virsh dumpxml base-vm-ubuntu-24.04 > /data/libvirt_images/test1/test1-dev.xml - Create the
cloud-inituser-data and meta-data for the NoCloud datasource
instance-id: session-test1
local-hostname: dev-test1users:
- name: developer
groups: sudo
shell: /bin/bash
sudo: ['ALL=(ALL) NOPASSWD:ALL']
mounts:
- [ "test1_home", "/home/developer", "virtiofs", "defaults", "0", "0" ]
runcmd:
- chown -R developer:developer /home/developer- Create the
cloud-initconfiguration ISO using the Ubuntucloud-initconvenience toolcloud-localdscd /data/libvirt_images/test1 cloud-localds cloud-init-seed-test1.iso user-data meta-data - Create a customer-specific share directory for saving persistent data (all transient data will be gone after shutting down a transient VM)
mkdir /data/libvirt_shared/test1 - Adjust the customer VM XML definition
- adjust name + remove uuid from the general domain XML metadata
- allow memory sharing for virtio-fs – I chose memfd, default is file-backed.
- mark the base VM disk as transient
- add the cloud-init ISO as DVD drive
- add the share directory as virtio filesystem (make sure the names match between cloud-init user-data and XML)
Here is a diff to guide you:
diff -u test1-dev.xml <(virsh dumpxml base-vm-ubuntu-24.04) | colordiff
--- /dev/fd/63 2026-04-10 21:04:43.061761247 +0200
+++ test1-dev.xml 2026-04-09 17:06:48.542821890 +0200
@@ -1,12 +1,15 @@
<domain type='kvm'>
- <name>base-vm-ubuntu-24.04</name>
- <uuid>03642c5a-c67f-4493-8109-7d36ae4b571f</uuid>
+ <name>dev-test1</name>
<metadata>
<libosinfo:libosinfo xmlns:libosinfo="http://libosinfo.org/xmlns/libvirt/domain/1.0">
<libosinfo:os id="http://ubuntu.com/ubuntu/24.04"/>
</libosinfo:libosinfo>
</metadata>
<memory unit='KiB'>8388608</memory>
+ <memoryBacking>
+ <source type='memfd' />
+ <access mode='shared' />
+ </memoryBacking>
<currentMemory unit='KiB'>8388608</currentMemory>
<vcpu placement='static'>4</vcpu>
<os>
@@ -38,14 +41,21 @@
<target dev='vda' bus='virtio'/>
<boot order='1'/>
<address type='pci' domain='0x0000' bus='0x04' slot='0x00' function='0x0'/>
+ <transient />
</disk>
<disk type='file' device='cdrom'>
<driver name='qemu' type='raw'/>
- <source file='/data/libvirt_images/ubuntu-24.04.4-desktop-amd64.iso'/>
+ <source file='/data/libvirt_images/test1/cloud-init-seed-test1.iso'/>
<target dev='sda' bus='sata'/>
<readonly/>
<address type='drive' controller='0' bus='0' target='0' unit='0'/>
</disk>
+
+ <filesystem type='mount' accessmode='passthrough'>
+ <driver type='virtiofs'/>
+ <source dir='/data/libvirt_shared/test1'/>
+ <target dir='test1_home'/>
+ </filesystem>
<controller type='usb' index='0' model='qemu-xhci' ports='15'>
<address type='pci' domain='0x0000' bus='0x02' slot='0x00' function='0x0'/>
</controller>
Start the transient customer VM
virsh create test1-dev.xml
The VM will run an overlay disk setup. All changes to the virtual disk will be gone on shutdown. You should store all permanent data in the customer share folder.
Access your VM’s graphical desktop
virt-viewer --attach dev-test1
Base VM update process
Now that you have overlay systems, you only need to update your base VM. Your derived transient customer VMs will run the new version instantly on next startup – no extra update needed.
virsh start base-vm-ubuntu-24.04
virt-viewer --attach base-vm-ubuntu-24.04
# perform update steps
virsh shutdown base-vm-ubuntu-24.04
My sequence of update steps:
# Perform updates
apt update
apt upgrade
snap refresh
# Seal the base VM (so it can take on new personalities during cloud-init)
apt clean
cloud-init clean --logs
rm -f /etc/machine-id
touch /etc/machine-id
poweroff
Possible extensions
This article just gets you started. You are now ready to customize your setup to your needs. Some ideas:
- Create a separate virtual network with fixed IPs (and
cloud-init’snetwork-config) - Provide SSH access as alternative to graphical access (use public keys and limit users with
AllowUsersinsshd_config) - Expose selected TCP ports to the host system for testing purposes (by binding to your new virtual network interface)
- Harden your
libvirtconfiguration
Appendix 1: Fix QEMU RAM file location
I prefer memfd as memory backing. But if you use file-backed memory, your system disk on /var/lib/libvirt/qemu will get under pressure. You can configurate a different filesystem. Just customize QEMU’s configuration:
...
# This directory is used for memoryBacking source if configured as file.
# NOTE: big files will be stored here
memory_backing_dir = "/data/libvirt_qemu_ram"
...systemctl restart libvirtd
Appendix 2: Fix resolution issues
I have a UHD display (3840 x 2160 ~ 16:9) with a HiDPI zoom level of 200% on my laptop. When resizing virt-viewer to fullscreen, I get an oversized 7680x4320 virtual screen.
In order to get the correct resolution, I had to uncheck “auto-resize VM” in virt-viewer and configure the guest resolution manually using the GNOME display settings in the guest (3840 x 2160 and a 200% scaling factor).
Unfortunately, pinning these settings at connection time using virt-viewer command line parameters didn’t work.
This is no AI Slop
Disclaimer: This article was written by a human with (a little) love. It’s neither written by an AI nor has an AI assisted in writing. Also, the recipe is not theoretical, but has been tested successfully on my laptop.
Hardware:
- Intel® Core™ i9 – 16 virtual cores
- 32G RAM
- 1TB NVMe SSD
Software Versions:
- Ubuntu 24.04.4 LTS
- bridge-utils 1.7.1
- cloud-image-utils 0.33
- libvirt-daemon-system 10.0.0
- ovmf 2024.02
- qemu-system-x86 8.2.2
- qemu-utils 8.2.2
- virt-manager 4.1.0
- virt-viewer 11.0
- virtiofsd 1.10.0
Google Gemini was used as a sparring partner to learn more about libvirt, though.
Known limitations
- Only one of the VMs can run at a time. You can overcome this limitation by creating VM-specific “linked copies”, i.e. overlay disk images.
Update 2026-05-11: Learnings after some weeks
I used the setup for some weeks now. Here some learnings:
- The performance is absolutely excellent! I didn’t run any hard benchmarks, but in VirtualBox I used to experience lags, excessive CPU usage at rest, etc. I never had this in KVM, so far. This applies to the VM itself as well as
virt-viewerand the SPICE desktop. - Another advantage:
virshis a better CLI thanVBoxManage. It’s much closer to the resource-based subcommand style of modern cloud tools likedockerorkubectl, … - The described update cycle really works like a charm.
Post header background image by Pixel-mixer (Marcel Langthim) from Pixabay.