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.

Overview sketch of a customer VM setup

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 bridges
  • libvirt-daemon-system: libvirt’s backbone – as a system service
  • qemu-system-x86: binaries for x86 emulation
  • virt-manager: A GUI for managing libvirt VMs – similar to VirtualBox’s GUI
  • qemu-utils: qemu-img for qcow2 manipulation
  • qemu-system-modules-spice and gir1.2-spiceclientgtk-3.0: for the virtual display GUI
  • ovmf: for UEFI boot in the VM
  • cloud-image-utils: for cloud-init configuration image creation
  • virtiofsd: 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)

Screenshot of the Display Spice menu in virt-manager

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”

Screenshot of the Video Virtio menu in virt-manager

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-init user-data and meta-data for the NoCloud datasource
instance-id: session-test1
local-hostname: dev-test1
users:
  - 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-init configuration ISO using the Ubuntu cloud-init convenience tool cloud-localds
    cd /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

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’s network-config)
  • Provide SSH access as alternative to graphical access (use public keys and limit users with AllowUsers in sshd_config)
  • Expose selected TCP ports to the host system for testing purposes (by binding to your new virtual network interface)
  • Harden your libvirt configuration

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-viewer and the SPICE desktop.
  • Another advantage: virsh is a better CLI than VBoxManage. It’s much closer to the resource-based subcommand style of modern cloud tools like docker or kubectl, …
  • The described update cycle really works like a charm.

Post header background image by Pixel-mixer (Marcel Langthim) from Pixabay.


Contact us