The guide · explanation
Host configuration and WSL 2
Almost everything vmlab needs is in the lab file. What is left is per-host: where it may allocate subnets from, which VNC viewer to launch, how big a chunk to push to a registry, where the config-weave binaries are, how large a file the workspace syncer carries, which UEFI firmware a VM boots. Those live in one optional host configuration file. This chapter explains that file, the directories vmlab uses, what changes when the host is WSL 2, and how vmlab picks between KVM and TCG. Every field is in Host configuration file and every path in Files and directories.
The host config file
vmlab reads ~/.config/vmlab/config.wcl, honouring XDG_CONFIG_HOME. The file is optional and every field in it is an override, so an absent file means all defaults. It must start with import <vmlab-host.wcl> and contain one host { … } block. A malformed value is reported at its line with the same wording a lab file gets, and every mistake in the file is reported in one pass. The errors surface where a lab is loaded, so a broken file fails vmlab up with its lines named; the daemon processes themselves fall back to defaults for a file they cannot read.
import <vmlab-host.wcl>
host {
subnet_pool = "10.99.0.0/16"
dns_suffix = "lab.local"
disk_low_percent = 5
viewer = "remote-viewer vnc://{}"
oci_chunk_size = 128MiB
workspace_max_file = 2GiB
}
| Field | What it sets | Default |
|---|---|---|
| subnet_pool | The CIDR segments with no subnet are auto-allocated from, one /24 each. See Networking. | 10.213.0.0/16 |
| dns_suffix | The suffix under which every machine's name is registered in segment DNS. | vmlab.internal |
| dns_upstream | The resolver ip[:port] segment DNS forwards to. | the host's resolver |
| disk_low_percent | The free-space percentage below which the host.disk_low watchdog fires. See Events and handlers. | 10 |
| psk | The pre-shared key that authenticates cross-host segment trunks. | none |
| trunk_port | The TCP port the supervisor listens on for inbound cross-host trunks. | 13947 |
| viewer | The VNC viewer command; {} is replaced by the target. | auto-detected |
| fastpath | The network fast path: auto, off, sockmap or afxdp. See vmlab fastpath. | auto |
| oci_chunk_size | The layer chunk size a template push uses. See Distributing templates over registries. | 512MiB |
| config_weave_bin_dir | The directory holding the config-weave guest binaries. See Playbooks. | ~/.local/share/config-weave/bin |
| workspace_max_file | The workspace syncer's per-file size guard; a larger file is refused by name. | 256MiB |
| firmware_dir | A UEFI firmware override, laid out <dir>/<arch>/. See the UEFI firmware section below. | vmlab's bundled firmware |
config_weave_bin_dir is a location knob with one code path behind it rather than a switch: it is the first rung of a three-rung lookup, ahead of the VMLAB_CONFIG_WEAVE_DIR environment variable and the XDG default. workspace_max_file is host config rather than a @dev argument because the cap is about your link to your guest, not about the lab everyone shares, and the refusal message names it so "raise the cap" points somewhere.
The viewer
vmlab console and gui = true launch a viewer chosen the same way. An explicit viewer in the host config wins and is dialled at the VNC unix socket directly. Otherwise vmlab takes the first of remote-viewer, gvncviewer and vncviewer found on PATH; all three are driven over a localhost TCP bridge to the socket, held open by a detached helper that exits when the viewer window closes, so neither command ties up the terminal. With no viewer at all, or with --tcp, vmlab console bridges the socket to a localhost port and prints the address for you to point any VNC client at. Closing a viewer only disconnects; the VM keeps running, always headless behind VNC. See vmlab console.
UEFI firmware
vmlab ships its own UEFI firmware. The guest assets carry Debian's edk2 builds, pinned by version and sha256, under firmware/<arch>/:
| Arch | Plain UEFI | Secure boot |
|---|---|---|
| x86_64 | OVMF_CODE_4M.fd with OVMF_VARS_4M.fd | OVMF_CODE_4M.secboot.fd with OVMF_VARS_4M.ms.fd, Microsoft's and Debian's keys enrolled |
| aarch64 | AAVMF_CODE.fd with AAVMF_VARS.fd | AAVMF_CODE.secboot.fd with AAVMF_VARS.ms.fd, keys enrolled |
So a host needs no ovmf or qemu-efi-aarch64 package, and every host boots the same firmware, which is what lets a Windows 11 template built on one host boot under secure boot on another. firmware/VERSION names the Debian packages the set came from. For each UEFI VM, vmlab looks in three places, in order, for plain UEFI and secure boot alike:
- firmware_dir from the host config, as <dir>/<arch>/ with the file names above. A directory there is authoritative for that architecture; a missing pair is an error.
- The bundled set, under each guest asset directory in turn: $VMLAB_GUEST_ASSET_DIR, /usr/share/vmlab/guest, ~/.local/share/vmlab/guest.
- The host's distribution firmware at its well-known paths. riscv64 always lands here, since vmlab bundles no riscv64 firmware.
A secure-boot build is only ever taken with the variable store that has keys enrolled for it: a blank store boots in setup mode, where nothing is verified, so vmlab refuses rather than boot one. Each VM copies its store from the template once, at its first start, into .vmlab/vms/<vm>/OVMF_VARS.fd, and keeps it, enrolled keys and boot entries included. A VM created under a firmware of a different flash layout, such as a host's old 2 MiB OVMF, keeps booting a build of that layout. Templates record firmware and secure_boot, never a firmware path, so nothing about the building host travels with them.
Directories
vmlab follows the XDG layout and honours each variable that overrides it:
| Path | Holds |
|---|---|
| ~/.config/vmlab/ | config.wcl and the profiles/ directory of user profiles. |
| ~/.local/share/vmlab/ | The template store under templates/ and the container image cache under oci/. |
| ~/.local/share/config-weave/bin/ | Where config-weave's own install puts its guest binaries. |
| ~/.local/state/vmlab/ | Daemon state, per-lab logs and event history. |
| $XDG_RUNTIME_DIR/vmlab/ | Control sockets: the supervisor's, each lab's, and per-VM QMP, agent and VNC sockets. |
| <lab>/.vmlab/ | The lab's own working data: disk clones, built media, TPM state, persisted state and sync ledgers. Gitignore it. |
The runtime directory is a full-privilege interface, since a client that can connect to a lab socket runs scripts in the lab and reads and writes guest files. vmlab creates it 0700, tightens an existing one, and refuses one owned by someone else. VMLAB_WORK_DIR relocates every lab's .vmlab/ under one base, namespaced by lab name and a hash of its root, which keeps the write-heavy working data off a slow filesystem while the lab file stays put.
WSL 2
vmlab was designed to be clean on WSL 2, and the choices that make it so are the same ones that make it need no privileges elsewhere: the network fabric is userspace with no tap, bridge or macvlan, and the only kernel grant KVM needs is /dev/kvm. Four things are specific to WSL 2.
- Nested virtualisation must be enabled in .wslconfig for /dev/kvm to exist inside the distribution. Without it every VM runs under TCG.
- XDG_RUNTIME_DIR may be missing. Some WSL setups do not set it; vmlab falls back to /tmp/vmlab-<uid>, created private and refused if owned by anyone else.
- The viewer lives on the Windows side. Run vmlab console --tcp to get a localhost address and point a Windows VNC client at it; WSL's localhost forwarding carries it across. Host access to guest services works the same way, through port forwards and localhost forwarding.
- The disk-space watchdog matters more. WSL 2's ext4 VHDX grows and does not shrink, and linked clones grow with use, so disk_low_percent and the host.disk_low event are worth handling.
KVM or TCG
For each machine vmlab picks the accelerator once: KVM when /dev/kvm can be opened and the guest architecture is the host's, TCG otherwise. TCG is full emulation, slow but functional, and vmlab warns loudly when it falls back, naming the machine and the architecture. Two cases hit it on purpose: a foreign-architecture guest such as an aarch64 or riscv64 template on an x86_64 host, which can only ever be emulated, and a host with no /dev/kvm. Give an emulated guest a couple of minutes to boot. Nested virtualisation inside a guest is separate: a KVM guest on x86 sees VMX or SVM only when its VM sets nested = true, and only when the host's own kvm_intel or kvm_amd module has nested virtualisation on. vmlab up warns with the fix when it is off.
A missing /dev/kvm is the first thing to check
If every VM is slow and vmlab logs shows the TCG fallback warning for x86_64 guests, the host lacks KVM: on WSL 2 enable nested virtualisation in .wslconfig, on a bare host check that your user can open /dev/kvm. See Troubleshooting.