TL;DR KVM en QEMU zijn de motor in Proxmox en het grootste deel van de cloud die je huurt. Op NixOS, met libvirt erbovenop, is die motor één blok config, en live migratie, memory ballooning, GPU passthrough, SR-IOV en CPU pinning zijn elk één commando of één stukje XML verderop. Wat Proxmox toevoegt is het dashboard en de delegatie, en hieronder ben ik eerlijk over wat dat waard is.
Vraag eens rond hoe je thuis een “echte” hypervisor draait en je krijgt hetzelfde rijtje. VMware, Hyper-V, of op z’n minst Proxmox. Iets met een web-UI, een clustering-tab en een marketingpagina vol enterprise features. Dat rijtje zit er zo diep in dat VMs draaien zonder zo’n product voelt als bochten afsnijden. Ik neem het niemand kwalijk. De marketing is goed.
Wat de marketing weglaat is dit. Het deel dat in al die producten het werk doet, is een Linux-kernelmodule die al meer dan tien jaar production-grade is. KVM met libvirt doet live migratie, memory ballooning, CPU pinning, GPU passthrough, SR-IOV en nested virtualization. De features op de glimmende pagina zijn kernelfeatures, en de web-UI zit ervoor.
Ik draai NixOS als hypervisor. Geen Proxmox, geen web-UI, een declaratieve Nix-config en virsh. Deze post loopt feature voor feature langs wat dat oplevert, en daarna het stuk waar ik zou liegen als ik zei dat de CLI altijd genoeg is.
KVM, QEMU en libvirt
Drie namen komen in elk commando hieronder terug, dus even voorstellen. KVM is de kernelmodule. Die maakt van de Linux-kernel een type-1 hypervisor met hardwareversnelling, dus een guest draait op bijna native snelheid. QEMU is het userspace-programma dat de guest z’n virtuele hardware geeft, de disks en de netwerkkaarten. libvirt is de beheerlaag over die twee, met een stabiele API over verschillende virtualisatiebackends en de virsh-CLI erbovenop.
Het is dezelfde stack, van de VMs op je laptop tot aan de cloud. AWS’s Nitro-hypervisor is gebouwd op KVM. Google Cloud draait KVM. Het grootste deel van de cloud waar je voor betaalt is deze stack met een facturatiesysteem eraan vast.
De NixOS-config
Dit is m’n hypervisorconfig, helemaal:
{ 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 hem toe en de machine is een hypervisor. Geen installer, geen setup-wizard en geen account aanmaken bij de eerste boot, want de config ís de installatie. Haal het blok weg, rebuild, en de hypervisor is weg zoals hij kwam.
VMs maken met virsh
Laten we er een maken. Eerst een disk:
# Maak een 50GB qcow2 disk
qemu-img create -f qcow2 /var/lib/libvirt/images/ubuntu-server.qcow2 50G
Dan de VM definiëren. De libvirt-XML kun je met de hand schrijven, of je laat virt-install dat doen:
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
Dat is een draaiende VM. Het dagelijkse werk is vijf commando’s:
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 weten dat je er een commercieel product voor nodig hebt. Een draaiende VM zonder downtime naar een andere host verplaatsen klinkt als iets met een licentiesleutel eraan.
Het is een virsh-subcommando. Live migratie kopieert het geheugen van de VM naar de doelhost terwijl de guest doordraait, en doet dan de laatste overstap in milliseconden. De guest merkt er niks van.
Drie dingen moeten kloppen:
- Gedeelde storage (NFS, Ceph , GlusterFS) of hetzelfde diskpad op beide hosts
- Dezelfde CPU-architectuur, of een compatibel CPU-model
- 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, en de VM draait op de andere host. Heb je geen gedeelde storage, dan neemt --copy-storage-all de disk mee met het geheugen.
Wil je de doelhost automatisch laten kiezen, dan is dat een lus om hetzelfde commando:
#!/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
Dat is een load balancer in een paar regels bash. De HA-tab van Proxmox stuurt dezelfde QEMU-migratie aan.
Memory ballooning
Met ballooning pas je het geheugen van een VM aan terwijl hij draait. De guest heeft een balloon-driver die pagina’s teruggeeft aan de host of om meer vraagt, zonder reboot.
Zet het aan in de VM-definitie:
<memballoon model='virtio'>
<address type='pci' domain='0x0000' bus='0x00' slot='0x08' function='0x0'/>
</memballoon>
Dan aanpassen 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 maakt overcommitten veilig. Deel 64GB uit over VMs op een host van 32GB en laat de balloon-drivers uitvechten wie wat nodig heeft.
GPU passthrough
Dé feature waar homelabmensen door geobsedeerd raken. Geef een fysieke GPU aan een VM en de guest krijgt bijna native graphics, en dat wil een gaming-VM, een machine-learning-bak of een hardwaretranscoder nu eenmaal.
Eerst IOMMU, in de 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"
];
}
Zoek de PCI-IDs van je GPU op:
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]
Geef dan beide functies, video en audio, aan de 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 rechtstreeks met de GPU. Installeer de drivers van de fabrikant in de guest en je hebt volledige hardwareversnelling, zonder host ertussen.
Netwerkkaart-passthrough en SR-IOV
Hetzelfde principe werkt voor netwerkkaarten. Een 10GbE- of 25GbE-kaart kan rechtstreeks naar een VM:
# Vind de NIC
lspci -nn | grep -i ethernet
# Geef door zoals de GPU
SR-IOV (Single Root I/O Virtualization) is de interessantere optie. Het splitst één fysieke NIC in meerdere virtual functions, en elk daarvan geef je 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 heeft z’n eigen PCI-adres. De VM krijgt netwerk op hardwaresnelheid, zonder virtio ertussen.
CPU pinning
Voor latency-gevoelige workloads wil je de vCPUs van de VM vastgespijkerd op specifieke host-cores, zodat de scheduler ze niet meer 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>
Dat pint de 4 vCPUs van de VM op host-cores 4-7 en houdt de emulator-threads op cores 0-1. Zet isolcpus in de kernelparameters en de latency komt dicht bij bare metal.
VMs in dezelfde config als de host
Hier loopt NixOS weg van een gewone Linux-bak met libvirt erop. De VMs zelf kunnen in de config:
{
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 met NixVirt, dat is netter:
{
virtualisation.libvirt.connections."qemu:///system" = {
domains = [{
definition = ./vms/ubuntu-server.xml;
active = true;
}];
};
}
De VMs staan nu in hetzelfde bestand als de host. Versiebeheerd, reproduceerbaar, en weg zodra je ze uit het bestand haalt. Er slingert nergens een verouderde definitie rond die niemand zich herinnert, want het bestand is de enige plek waar een definitie kan staan.
Waar de CLI pijn doet
Tot hier leest dit alsof de simpelere stack gewoon wint. Ik draai het al jaren en ik ga niet doen alsof dat het hele plaatje is. Elke feature is er. Het dagelijkse werk eromheen is ruwer dan een goede UI, en dat zeg ik liever zelf dan dat je erachter komt.
Vier dingen worden vervelend. virt-install heeft een vlag voor alles, dus ik zit bij elke nieuwe VM in de man page. Ik heb wrapperscripts, die werken, en ze zitten vol aannames die alleen ik ken.
Monitoring betekent scripts die virsh bevragen om status en resourcegebruik, of een Grafana-dashboard met een libvirt-exporter, en dat is een weekendje op zich. Storagepools, volumes en snapshots kunnen allemaal vanuit virsh, maar de commando’s zijn lang en ik typ ze verkeerd. En welke VM op welke bridge zit met welk IP, dat zit in m’n scripts of in m’n hoofd. Een scherm is er niet voor.
Drie dingen doen scripts slecht. Een snel overzicht, de blik van “hoe staat alles ervoor”, is virsh list plus virsh domstats plus een parser, en dat legt het af tegen een dashboard. Consoletoegang is lokaal prima met virt-viewer, en een webconsole is fijner als ik remote en half wakker ben. Delegatie, iemand twee specifieke VMs laten beheren zonder de hele host uit handen te geven, is omslachtig met kale libvirt.
Waarom Proxmox bestaat
Wat me bij Proxmox brengt. Dezelfde KVM, QEMU en libvirt eronder, verpakt in een web-UI die alles uit de vorige sectie doet zonder één script.
Vind ik dat je het nodig hebt? Voor een homelab met één beheerder niet. NixOS plus virsh plus een paar scripts is voor mij al jaren prima. Waar de UI z’n plek verdient is onboarding, waar een nieuw iemand de infrastructuur van een scherm afleest in plaats van uit m’n scripts. Incidenten, waar klikken het wint van virsh-syntax herinneren onder druk. Zichtbaarheid, elke VM met z’n status en z’n netwerk in één overzicht.
Proxmox levert ook clustering, Ceph-integratie en backupbeheer mee, en dat vanuit scripts nabouwen is echt werk.
Wat Proxmox verkoopt is de operationele laag. De hypervisor eronder heb je al.
Terug naar het rijtje
Het rijtje had gelijk over het dashboard en ongelijk over de hypervisor. VMware, Hyper-V en Proxmox draaien dezelfde kernelmodule als m’n NixOS-bak, en NixOS legt daar declaratieve config en versiebeheer bovenop, wat geen van drie op die as heeft. Wat die drie verkopen is het dashboard, de delegatie en de ergonomie tijdens een incident.
Welke je wilt hangt ervan af welke van die twee je liever zelf bouwt. Wil je virtualisatie begrijpen tot aan de kernelmodule, begin dan met NixOS, KVM en virsh. Niks zit verstopt, en elke “enterprise” feature is een commando dat je zelf hebt getypt. Zoals ik betoogde in Sovereign Infrastructure, is een black box die je niet kunt inspecteren een risico op zich, en in deze stack zit geen black box.
Draai je productie met een team, dan koopt de Proxmox-UI een operationele laag voor je, en dat is een prima aankoop. Voor m’n homelab is de machine begrijpen de halve reden dat hij er staat, dus de config bovenaan deze post is m’n hypervisor. Declaratief en versiebeheerd, zonder leverancier tussen mij en de kernel. Het is nog steeds de hele installatie.
