A NixOS image (built on foundrix) that runs a GitLab Runner as a Hyper-V Gen2 VM. Credentials are passed in via a one-time setup dialog over SSH or the VM console — the var-disk-manager pattern: a daemon owns the state, every connected session mirrors it, the first one to touch the form drives it.
- Image: ephemeral tmpfs root, writable btrfs nix store, UEFI systemd-boot (Secure Boot off). Built by foundrix's repart machinery and converted to a dynamic VHDX.
- Disks: OS VHDX at SCSI LUN 0 (swappable for updates), data VHDX at
LUN 1 holding
/var— runner registration, CI caches, docker images. The data disk is found at runtime by LUN (Hyper-V has no stable by-id names) and partitioned on first boot. - Runners: registered at runtime with
gitlab-runner registerusing GitLab ≥17 runner authentication tokens (glrt-…); the dialog supports several runners, each either the shell executor (native Nix on the host — the writable store is the build cache) or the docker executor (isolated containers, nothing mounted from the host; an image that needs Nix brings its own). - State: everything imperative lives in
/var/lib/gitlab-runner/(config.toml,.provisionedsentinel, chosenhostname). Reboots and OS-disk swaps never re-prompt.
# Raw image + all foundrix outputs
nix flake show path:.
# The Hyper-V deliverable (VHDX + VM setup script)
nix build 'path:.#hyperv-vhdx'Before building a production image, put your admin SSH key into
configuration.nix (adminKeys) — without it the appliance is
unreachable over SSH.
On the Hyper-V host (elevated PowerShell):
.\setup-hyperv-vm.ps1 -VMName gitlab-runnerThis creates a Gen2 VM (Secure Boot off), attaches the OS disk and a 256 GB dynamic data disk, and connects it to an external switch.
VM files (config, OS disk, data disk) default to
C:\ProgramData\GitLabRunner\HyperV\VMs\<VMName>\. To put them on another
volume, or to place a single disk somewhere specific:
# everything on the D: VM storage volume
.\setup-hyperv-vm.ps1 -VMName gitlab-runner -VMStorageRoot D:\HyperV\VMs
# or per-disk
.\setup-hyperv-vm.ps1 -VMName gitlab-runner `
-OSDiskPath D:\vm\runner-os.vhdx `
-DataDiskPath E:\ci-cache\runner-data.vhdxOther knobs: -DataDiskSizeBytes (default 256 GB), -ProcessorCount
(4), -MemoryStartupBytes (8 GB), -SwitchName.
To update the OS later while keeping registration and caches — pass the same path options you used originally:
.\setup-hyperv-vm.ps1 -VMName gitlab-runner -Update -FinalImagePath .\new\gitlab-runner-os.vhdxStart the VM and open its console — or ssh root@<ip>. Both land in the
same fullscreen dialog:
GitLab URL: https://gitlab.example.com
── runner 1 ──
token: glrt-… (create under Group/Project → CI/CD → Runners)
executor: ◂ shell (native Nix on host) ▸
[ + add another runner ]
concurrency: 4
hostname: ci-runner-01
[ Register and start runner ]
On success the runner service starts immediately and the session becomes a normal shell; every later login is a plain shell too. Registration failures (bad token, unreachable URL) are shown in place — fix and resubmit.
To redo setup later: runner-reprovision (unregisters the runners, clears
the state, brings the dialog back).
The store is writable and nixos-rebuild is present, so an appliance is
updated in place — no image swap, no reimage:
nixos-rebuild switch --flake .#gitlab-runner --target-host root@<appliance>Registration, caches and the data disk are untouched.
One command starts the appliance as a throwaway VM whose console is the terminal it was started from. No admin SSH key is needed for this:
nix run 'path:.#qemu-run'The kernel and systemd output scroll by, then the setup dialog appears in
the terminal. Register a runner as on Hyper-V; the dialog quits into a
root shell (no password, TERM=xterm-256color) and the runner takes jobs
until the VM is shut down.
- Clean exit:
poweroffin the guest shell. On the way down the VM unregisters the session's runner from GitLab, while the network is still up, and the call is printed on the console. - Key bindings:
Ctrl-a ctoggles the QEMU monitor,Ctrl-a xkills the VM on the spot. A hard kill does not unregister the runner; remove it in GitLab by hand.Ctrl-Cgoes to the guest like any other key. - Nothing persists. The OS disk is a copy-on-write overlay over the image in the Nix store and the data disk is created fresh; both are removed when the launcher exits (also on error and on SIGINT/SIGTERM). The next launch shows the setup dialog again.
- State directory:
$(dirname "$PWD")/.cache/gitlab-runner-qemu, one level above the checkout (not/tmp, which may be RAM; not inside the checkout, which apath:flake copies into the store). Override withRUNNER_VM_DIR. The per-run disks live in a subdirectory of it. - Journal: the guest journal is streamed to
journal.login the state directory, overwritten on every launch and kept after exit. It is the only record of a session once the data disk is gone. - Size:
RUNNER_VM_CPUS(default4),RUNNER_VM_MEMORY(QEMU size string, default8G),RUNNER_VM_OS_SIZE(qemu-img size string, default200G, sparse; the store partition grows into it on first boot, so Nix builds have room) andRUNNER_VM_DATA_SIZE(default64G, sparse). The data disk must be at least9G:/varhas an 8G minimum and a smaller disk fails the first boot into emergency mode. - Networking: QEMU user-mode NAT, outbound only. The host is
10.0.2.2from inside the guest. KVM is used when/dev/kvmis writable, otherwise software emulation; the launcher prints which.
Design notes and acceptance criteria are in docs/qemu-run.md.
| Path | Purpose |
|---|---|
configuration.nix |
device-agnostic system config (admin keys go here) |
devices/hyperv/ |
Hyper-V device: guest support, kernel, networking |
devices/qemu/ |
local QEMU device: serial console, autologin, journal on hvc0, unregister on shutdown |
modules/appliance-disks.nix |
LUN 0 / LUN 1 storage model, image modules, runtime repart |
modules/appliance-filesystems.nix |
mounts: tmpfs root, ESP, /nix, /var, /home |
modules/gitlab-runner.nix |
runner service, hostname re-apply, runner-reprovision |
modules/runner-provision.nix |
setup daemon + console/SSH dialog wiring |
packages/runner-provisioner/ |
the Rust daemon + ratatui TUI |
packages/qemu-run/ |
the local VM launcher |
hyperv/setup-hyperv-vm.ps1 |
VM creation/update on the Hyper-V host |
docs/qemu-run.md |
design of the local QEMU runner |