The guide · explanation

Host configuration and WSL 2

8 min read · 2026-09-02 · vmlab 0.9

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.

~/.config/vmlab/config.wclwcl
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
}
FieldWhat it setsDefault
subnet_poolThe CIDR segments with no subnet are auto-allocated from, one /24 each. See Networking.10.213.0.0/16
dns_suffixThe suffix under which every machine's name is registered in segment DNS.vmlab.internal
dns_upstreamThe resolver ip[:port] segment DNS forwards to.the host's resolver
disk_low_percentThe free-space percentage below which the host.disk_low watchdog fires. See Events and handlers.10
pskThe pre-shared key that authenticates cross-host segment trunks.none
trunk_portThe TCP port the supervisor listens on for inbound cross-host trunks.13947
viewerThe VNC viewer command; {} is replaced by the target.auto-detected
fastpathThe network fast path: auto, off, sockmap or afxdp. See vmlab fastpath.auto
oci_chunk_sizeThe layer chunk size a template push uses. See Distributing templates over registries.512MiB
config_weave_bin_dirThe directory holding the config-weave guest binaries. See Playbooks.~/.local/share/config-weave/bin
workspace_max_fileThe workspace syncer's per-file size guard; a larger file is refused by name.256MiB
firmware_dirA 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>/:

ArchPlain UEFISecure boot
x86_64OVMF_CODE_4M.fd with OVMF_VARS_4M.fdOVMF_CODE_4M.secboot.fd with OVMF_VARS_4M.ms.fd, Microsoft's and Debian's keys enrolled
aarch64AAVMF_CODE.fd with AAVMF_VARS.fdAAVMF_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:

  1. 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.
  2. The bundled set, under each guest asset directory in turn: $VMLAB_GUEST_ASSET_DIR, /usr/share/vmlab/guest, ~/.local/share/vmlab/guest.
  3. 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:

PathHolds
~/.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.

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.