Vraag de meeste mensen hoe je thuis een “echte” hypervisor draait en je krijgt hetzelfde rijtje: VMware, Hyper-V, of op zijn minst Proxmox. Iets met een web UI, een clustering-tab, een marketingpagina vol enterprise features. Dat beeld zit er zo diep in dat virtuele machines draaien zonder zo’n product voelt als bochten afsnijden. We hebben stilletjes geaccepteerd dat serieuze virtualisatie met een leverancier komt.

Draai het nu om. Wat in al die producten het echte werk doet, is een Linux kernel module die al meer dan tien jaar production-grade is. KVM met libvirt geeft je live migratie, memory ballooning, CPU pinning, GPU passthrough, SR-IOV, nested virtualization. De features waar de glanzende hypervisors mee adverteren zijn kernel features. De web UI is een schil eromheen.

Ik draai NixOS als mijn hypervisor. Geen Proxmox, geen web UI, alleen declaratieve Nix configs en virsh. Laat me je tonen waar die grens echt ligt, want het gat tussen “rauwe KVM” en “enterprise hypervisor” is smaller dan de marketing suggereert, en het stuk dat overblijft verdient eerlijkheid.

De basis: KVM en QEMU

Voordat we verdergaan, even de stack:

  • KVM (Kernel-based Virtual Machine): Een Linux kernel module die je kernel verandert in een type-1 hypervisor. Hardware-versneld, near-native performance.
  • QEMU: De userspace emulator die virtuele hardware (disks, netwerkkaarten, etc.) levert aan VMs.
  • libvirt: Een management laag die een consistente API biedt over verschillende virtualisatietechnologieën. Bevat de virsh CLI.

Dit is dezelfde stack van de VMs op je laptop tot massieve cloud infrastructuur. AWS’s Nitro hypervisor is gebouwd op KVM. Google Cloud draait KVM. Het grootste deel van de cloud waar je voor betaalt is deze technologie met een facturatiesysteem eraan vast.

NixOS configuratie

Zo stel ik NixOS in als hypervisor:

{ config, pkgs, ... }:

{
  # Enable virtualization
  virtualisation.libvirtd = {
    enable = true;
    qemu = {
      package = pkgs.qemu_kvm;
      runAsRoot = true;
      swtpm.enable = true;  # TPM emulatie
      ovmf = {
        enable = true;  # UEFI support
        packages = [ pkgs.OVMFFull.fd ];
      };
    };
  };

  # Voeg jezelf toe aan de libvirt groep
  users.users.tom = {
    extraGroups = [ "libvirtd" ];
  };

  # Handige tools
  environment.systemPackages = with pkgs; [
    virt-manager    # GUI als je wilt
    virt-viewer     # Voor console access
    libguestfs      # VM image manipulatie
    win-virtio      # Windows VirtIO drivers
  ];

  # Bridge networking (optioneel, voor bridged VMs)
  networking.bridges.br0.interfaces = [ "enp1s0" ];
  networking.interfaces.br0.useDHCP = true;
}

Pas deze config toe en je hebt een volledig functionele hypervisor. Geen installer, geen setup wizard, geen account aanmaken bij de eerste boot. De config is de installatie. Rol hem terug en de hypervisor is weg op dezelfde manier als hij kwam.

VMs maken met virsh

Laten we een VM maken. Eerst een disk alloceren:

# Maak een 50GB qcow2 disk
qemu-img create -f qcow2 /var/lib/libvirt/images/ubuntu-server.qcow2 50G

Definieer dan de VM met XML (of gebruik virt-install):

virt-install \
  --name ubuntu-server \
  --memory 4096 \
  --vcpus 2 \
  --disk /var/lib/libvirt/images/ubuntu-server.qcow2 \
  --cdrom /var/lib/libvirt/images/ubuntu-24.04-server.iso \
  --os-variant ubuntu24.04 \
  --network bridge=br0 \
  --graphics vnc

Nu heb je een draaiende VM. Beheer het met:

virsh list --all           # Lijst alle VMs
virsh start ubuntu-server  # Start VM
virsh shutdown ubuntu-server  # Graceful shutdown
virsh destroy ubuntu-server   # Force stop
virsh console ubuntu-server   # Seriële console

Live migratie

Dit is de feature waarvan mensen het zekerst zijn dat je er een commercieel product voor nodig hebt. Een draaiende VM tussen hosts verplaatsen zonder echte downtime klinkt als iets waar je een licentie voor betaalt.

Live migratie kopieert het geheugen van de VM naar de target host terwijl die blijft draaien, en doet dan de finale switchover in milliseconden. De guest merkt er niets van.

Vereisten:

  • Gedeelde storage (NFS, Ceph, GlusterFS) of zelfde disk pad op beide hosts
  • Zelfde CPU architectuur (of gebruik CPU model compatibiliteit)
  • libvirt draaiend op beide hosts
# Vanaf de bron host
virsh migrate --live ubuntu-server qemu+ssh://target-host/system

# Met opties voor betere performance
virsh migrate --live --persistent --undefinesource \
  --copy-storage-all \
  ubuntu-server qemu+ssh://target-host/system

Eén commando. De VM draait nu op de andere host. De --copy-storage-all flag kopieert zelfs de disk als je geen gedeelde storage hebt.

Voor gescripte omgevingen kun je dit programmatisch doen:

#!/bin/bash
# Simpele load balancer - migreer VM naar minst belaste host

HOSTS=("host1" "host2" "host3")
VM="ubuntu-server"

# Vind host met laagste load
TARGET=$(for h in "${HOSTS[@]}"; do
  echo "$(ssh $h 'cat /proc/loadavg | cut -d" " -f1') $h"
done | sort -n | head -1 | cut -d" " -f2)

virsh migrate --live $VM qemu+ssh://$TARGET/system

Een paar regels bash en je hebt de load balancer die achter een betaald HA dashboard zit.

Memory ballooning

Memory ballooning laat je VM-geheugen tijdens runtime aanpassen zonder reboot. De balloon driver van de guest geeft geheugen terug aan de host of vraagt om meer.

Zet het aan in je VM definitie:

<memballoon model='virtio'>
  <address type='pci' domain='0x0000' bus='0x00' slot='0x08' function='0x0'/>
</memballoon>

Pas dan aan tijdens runtime:

# Zet huidig geheugen op 2GB (VM heeft 4GB max)
virsh setmem ubuntu-server 2G --live

# Geef weer 4GB terug
virsh setmem ubuntu-server 4G --live

Dit is wat memory overcommitting veilig maakt. Wijs 64GB toe over VMs op een 32GB host en laat de balloon driver het daadwerkelijke gebruik tussen ze balanceren.

GPU passthrough

Dit is de ene homelab waar mensen geobsedeerd door raken: een fysieke GPU direct doorgeven aan een VM voor near-native graphics. Gaming VMs, machine learning workloads, hardware transcoding, alles.

Configureer eerst IOMMU in je NixOS config:

{
  boot.kernelParams = [
    "intel_iommu=on"  # of amd_iommu=on voor AMD
    "iommu=pt"
  ];

  # Isoleer de GPU van de host
  boot.extraModprobeConfig = ''
    options vfio-pci ids=10de:2204,10de:1aef
  '';

  boot.initrd.kernelModules = [
    "vfio_pci"
    "vfio"
    "vfio_iommu_type1"
  ];
}

Vind de PCI IDs van je GPU:

lspci -nn | grep -i nvidia
# 01:00.0 VGA compatible controller [0300]: NVIDIA Corporation ... [10de:2204]
# 01:00.1 Audio device [0403]: NVIDIA Corporation ... [10de:1aef]

Voeg dan de GPU toe aan je VM:

<hostdev mode='subsystem' type='pci' managed='yes'>
  <source>
    <address domain='0x0000' bus='0x01' slot='0x00' function='0x0'/>
  </source>
</hostdev>
<hostdev mode='subsystem' type='pci' managed='yes'>
  <source>
    <address domain='0x0000' bus='0x01' slot='0x00' function='0x1'/>
  </source>
</hostdev>

De VM praat nu direct met de GPU. Installeer de native drivers in de guest en je krijgt volledige hardware acceleratie, zonder host ertussen.

Netwerkkaart passthrough en SR-IOV

Hetzelfde principe werkt voor netwerkkaarten. Voor 10GbE of 25GbE kaarten wil je misschien directe passthrough:

# Vind de NIC
lspci -nn | grep -i ethernet

# Geef door zoals de GPU

Waar het interessant wordt is SR-IOV (Single Root I/O Virtualization). Het splitst één fysieke NIC in meerdere virtual functions, elk toewijsbaar aan een andere VM:

# Enable 4 virtual functions op de NIC
echo 4 > /sys/class/net/enp5s0f0/device/sriov_numvfs

# Lijst ze
lspci | grep "Virtual Function"

Elke VF krijgt zijn eigen PCI adres en kan worden doorgegeven aan een VM. Hardware-versnelde networking zonder de overhead van virtio.

CPU pinning

Voor latency-gevoelige workloads wil je VMs vastpinnen op specifieke host cores zodat de scheduler ze niet langer heen en weer schuift:

<vcpu placement='static'>4</vcpu>
<cputune>
  <vcpupin vcpu='0' cpuset='4'/>
  <vcpupin vcpu='1' cpuset='5'/>
  <vcpupin vcpu='2' cpuset='6'/>
  <vcpupin vcpu='3' cpuset='7'/>
  <emulatorpin cpuset='0-1'/>
</cputune>

Dit pint de 4 vCPUs van de VM aan host cores 4-7, met emulator threads op cores 0-1. Combineer dat met isolcpus in je kernel parameters en de latency komt dicht bij bare metal.

Declaratief VM beheer op NixOS

Hier loopt NixOS uit op een generieke Linux bak met libvirt. Je kunt VMs declaratief definiëren:

{
  virtualisation.libvirtd.enable = true;

  # Definieer VMs als Nix expressies
  systemd.services."libvirt-guest-ubuntu" = {
    after = [ "libvirtd.service" ];
    requires = [ "libvirtd.service" ];
    wantedBy = [ "multi-user.target" ];
    serviceConfig = {
      Type = "oneshot";
      RemainAfterExit = "yes";
    };
    script = ''
      ${pkgs.libvirt}/bin/virsh define /etc/libvirt/qemu/ubuntu-server.xml || true
      ${pkgs.libvirt}/bin/virsh start ubuntu-server || true
    '';
    preStop = ''
      ${pkgs.libvirt}/bin/virsh shutdown ubuntu-server || true
      sleep 10
      ${pkgs.libvirt}/bin/virsh destroy ubuntu-server || true
    '';
  };
}

Of gebruik NixVirt voor een schonere aanpak:

{
  virtualisation.libvirt.connections."qemu:///system" = {
    domains = [{
      definition = ./vms/ubuntu-server.xml;
      active = true;
    }];
  };
}

Je VMs zijn nu onderdeel van dezelfde config die de host definieert. Version controlled, reproduceerbaar, en weer weg op het moment dat je ze uit het bestand haalt. Geen Proxmox cluster dat een verouderde VM definitie bewaart die niemand zich herinnert te hebben aangemaakt.

Waar de CLI echt pijn doet

Tot nu toe leest dit alsof de simpelere stack gewoon wint. Ik draai het al jaren en ik ga niet doen alsof dat het hele plaatje is. De features zijn er allemaal. Het dagelijkse werk eromheen is ruwer dan een gepolijste UI, en doen alsof dat niet zo is zou oneerlijk zijn.

Wat vervelend wordt:

  • VMs maken: virt-install heeft een flag voor alles, wat betekent dat je elke keer de man page leest. Ik bewaar wrapper scripts, en die werken, maar ze hangen aan elkaar van mijn eigen aannames.
  • Monitoring: Je eindigt met scripts schrijven om VM status, resource gebruik en health te checken. Of je zet een Grafana dashboard op met een libvirt exporter, wat een eigen projectje is.
  • Storage beheer: Pools, volumes, snapshots zijn allemaal mogelijk met virsh, maar de commando’s zijn lang en makkelijk verkeerd te typen.
  • Netwerk visualisatie: Welke VM zit op welke bridge? Welke heeft welk IP? Dat antwoord leeft in je scripts of in je hoofd, niet op een scherm.

En sommige dingen doen scripts slecht:

  • Een snel visueel overzicht: Als je gewoon wilt weten hoe alles ervoor staat, is virsh list plus virsh domstats plus een parser geen partij voor een dashboard waar je op kunt kijken.
  • Console toegang: virt-viewer werkt prima lokaal. Een web console is echt prettiger als je remote en half wakker bent.
  • Delegatie: Iemand twee specifieke VMs laten beheren zonder de hele host uit handen te geven is omslachtig met rauwe libvirt.

Waarom Proxmox bestaat

Wat me bij Proxmox brengt. Dezelfde engine eronder, KVM en QEMU en libvirt, verpakt in een web UI die al het bovenstaande toegankelijk maakt zonder ook maar één script.

Ik ga je niet vertellen dat je het nodig hebt. Voor een homelab waar jij de enige admin bent, is NixOS plus virsh plus een paar scripts echt prima. Ik heb het jaren zo gedraaid en het bleef werken.

De UI verdient zijn plek in specifieke situaties:

  • Onboarding: een nieuw iemand kan de infrastructuur van het scherm aflezen in plaats van uit je scripts
  • Emergency response: om 3 uur ’s nachts is klikken betrouwbaarder dan virsh syntax herinneren
  • Zichtbaarheid: elke VM, zijn status, resource gebruik en netwerk topologie in één blik
  • Geïntegreerde features: backup scheduling, HA clustering en firewall regels geconfigureerd op één plek

Proxmox bundelt ook clustering, Ceph integratie en backup beheer die echt werk zouden zijn om vanuit scripts opnieuw te bouwen. De waarde zit niet in een andere hypervisor. Het is de operationele laag die je anders met de hand in elkaar zet.

Het gat dichten

Hier komen de twee kanten samen. De features waren nooit de scheidslijn. KVM geeft je ze allemaal, en NixOS legt daar declaratieve configs en version control bovenop, wat op die specifieke as meer is dan Proxmox je geeft. Wat Proxmox verkoopt is de operationele laag: het dashboard, de delegatie, de ergonomie om 3 uur ’s nachts.

Kies op basis van welk gat je liever zelf dicht. Wil je virtualisatie begrijpen tot aan de kernel module, begin dan met NixOS, KVM en virsh. Niets zit verstopt, en elke “enterprise” feature is een commando dat je bewust hebt getypt. Zoals ik betoogde in Sovereign Infrastructure, is afhankelijk zijn van een black box die je niet kunt inspecteren een eigen soort risico, en in deze stack zit geen black box.

Draai je productie of werk je met een team, dan koopt de Proxmox UI je operationele rust, geen betere hypervisor, en dat is iets om met een gerust hart te kopen. Voor mijn homelab, waar de machine begrijpen het halve punt is, is NixOS als hypervisor de keuze die ik blijf maken. Declaratieve configs, volledige controle, geen leverancier tussen mij en de kernel. Gewoon Linux, dat doet wat het stilletjes al doet in elke cloud die je ooit gebruikt hebt.