Design Spec: NixOS VMs on Proxmox
Implementation Status
| Component / Feature | Status | Details |
|---|---|---|
proxmox-vm Role |
Fully Implemented | QEMU guest profile, VirtIO drivers, guest agent, systemd-boot. |
| Host Structure Pattern | Fully Implemented | Logical/physical split with per-host Disko layout. |
| Baseline Disk Layout | Fully Implemented | GPT, 512M ESP, 100% ext4 root partition on /dev/sda. |
| ISO Artifact Delivery | Fully Implemented | Installer ISO staged on the node via Terraform. |
| Terraform VM Pattern | Fully Implemented | Reference VM resource shape in vms-pve1.tf. |
| Host Integration Tests | Fully Implemented | Automatically generated by the flake for host configs. |
Deployed VM Hosts (hl02) |
Fully Implemented | hl02 on pve1 follows this framework end-to-end. |
1. Goal
Provide a reusable framework for fully declarative NixOS virtual machines on the
Proxmox nodes: a shared proxmox-vm role for guest hardware settings, a
standard host directory structure with Disko partitioning, automatic
provisioning via Terraform and nixos-anywhere, and auto-generated integration
tests.
The framework covers NixOS VMs only. Non-NixOS VMs (such as the Debian hl01
VM, which is provisioned via cloud-init) are out of scope.
2. Rationale
This spec is the VM counterpart of the NixOS LXC Containers on Proxmox framework. VMs carry a full kernel and bootloader, so — unlike LXC hosts — they need hardware guest settings, a partition layout (Disko), and an installer path. Those concerns are the same for every NixOS VM in the lab, so they are defined once here; each host only declares its logical parameters and its virtual hardware sizing.
It builds upon the global Home Lab Bootstrapping Spec for installation and the Declarative Integration Testing Spec for CI/CD validation.
3. proxmox-vm Role (config/nix/roles/proxmox-vm/default.nix)
The role generalizes everything a NixOS guest needs to run well under Proxmox/QEMU, so host directories do not carry a hardware configuration file:
- Imports the built-in NixOS
qemu-guestprofile. - Loads the standard VirtIO initrd modules (
virtio_pci,virtio_scsi,sd_mod, ...) so the installer and the installed system can see the virtual disks. - Enables the QEMU Guest Agent (
services.qemuGuest), which Proxmox and Terraform rely on for IP reporting and clean shutdowns. - Enables
boot.growPartitionso the root partition expands when the virtual disk is enlarged. - Optimizes the console for the Proxmox serial console (
console=ttyS0). - Configures
systemd-bootfor UEFI (OVMF) VMs, capping retained generations (configurationLimit) so the small ESP cannot fill up.
All settings use lib.mkDefault where a host may plausibly need to override
them (e.g. a legacy-BIOS VM switching to GRUB).
4. Host Structure
Following the standard logical/physical split (see Testing Spec - Split Design), each VM host is described by a small set of files under its host directory:
config/nix/hosts/<host>/
├── default.nix # Physical entrypoint: configuration.nix + disko.nix
├── configuration.nix # Logical config (roles + host parameters)
├── disko.nix # Filesystem and partition layout
└── test-override.nix # Optional test-only overrides
default.nixis what the flake discovers and builds asnixosConfigurations.<host>; its presence also registers the host for auto-generated integration tests. For a VM host it importsconfiguration.nixanddisko.nix.configuration.nixis the logical configuration: it importsroles/common,roles/proxmox-vm, androles/comin(pull-based GitOps), and defines the per-host logical parameters:networking.hostName = "<host>"networking.hostId = "<hostid>"networking.useDHCP = truesystem.stateVersion = "<release>"
disko.nixaccepts{ inputs, ... }(provided globally viaspecialArgs), imports the core Disko module (inputs.disko.nixosModules.disko), and declares the host's physical disk layout (see the baseline layout).test-override.nix(optional) supplies host-specific test assertions or configuration overrides; it is automatically merged by the flake, so a dedicatedtest.nixentrypoint is not required.
No hardware.nix file is needed: standard VM hardware configuration (VirtIO
drivers, systemd-boot) is generalized in the
proxmox-vm role.
5. Baseline Disk Layout (Disko)
The baseline disko.nix declares a GPT partition table on the primary SCSI
virtual disk (/dev/sda):
- EFI Partition (ESP): 512MB, formatted as
vfat, mounted at/bootwith standard secure mount options (umask=0077). - Root Partition: 100% of the remaining space, formatted as
ext4, mounted at/.
This layout is the default for new VM hosts. A host with different storage needs
overrides it in its own disko.nix; the layout is per-host by design.
6. ISO Artifact Delivery
To bridge the local Nix build pipeline with the hypervisor environment, the compiled installer ISO must be staged on the Proxmox target node prior to VM orchestration.
- Mechanism: Handled declaratively via the
proxmox_virtual_environment_fileresource within the Terraform workspace. - Source Target: The local Nix flake build output directory
(
./result/iso/\*.iso). - Destination Target: Stored in the cluster's default structural storage
(
local:iso/). - Staging: Build the
proxmox-imagesaggregate package before running Terraform, so the ISO coexists behindresultwith the other image artifacts Terraform reads. - Node Coverage: The ISO is uploaded to every node (
pve1andpve2) on each apply, mirroring the LXC template upload pattern. Any node is therefore ready to receive its first NixOS VM without additional staging work.
7. Infrastructure Provisioning (Terraform)
VM hardware is declared per host in
config/terraform/220-proxmox-workloads/vms-pve<N>.tf using the bpg/proxmox
provider. Each node's file is created when the node receives its first VM
(currently only vms-pve1.tf exists); a resource uses that node's provider
alias (e.g. proxmox.pve2) and references that node's installer ISO resource.
Every NixOS VM resource follows the same reference shape; only the sizing values
and identifiers change per host (see the
deployed hosts table).
- Hardware: Host-type CPU cores and dedicated RAM per the host's sizing,
with a
virtio-scsi-pciSCSI controller. - EFI Disk & Storage: OVMF BIOS with an EFI disk of type
4m, stored on thelocal-zfspool. - Disk Interface (
scsi0): Presents the virtual disk as/dev/sdainside the VM, matching thedisko.nixexpectations. - Storage Optimization: Enables
discard = "on"(TRIM) to reclaim unused blocks dynamically on the underlying ZFS pool. - Installer CDROM (
ide2): Mounts the custom installer ISO (uploaded automatically as described in Bootstrapping Spec - Custom ISO). - MAC Pinning: Pins a static MAC address to the virtio network interface, enabling stable DHCP reservations.
- Automated Boot Order: Configures
boot_order = ["scsi0", "ide2"].- First Boot: The VM disk
/dev/sdais empty, so EFI falls back to booting from the installer ISO onide2. - Subsequent Boots: Boots directly from the production system on
/dev/sda(scsi0), bypassing the installer.
- First Boot: The VM disk
8. Integration Testing
Integration tests are automatically generated and registered for all hosts
containing a configuration.nix file. The flake dynamically discovers each
config/nix/hosts/<host>/configuration.nix and creates the host-<host>-test
check using the global test generator config/nix/tests/make-test.nix.
Because VM host configurations import roles/common (enabling SSH) and
roles/proxmox-vm (enabling the QEMU Guest Agent), the auto-generated test
automatically verifies:
- Successful boot (
multi-user.targetreached). - SSH port 22 availability.
- QEMU Guest Agent service status.
Host-specific assertions go in the optional test-override.nix (see
Host Structure). Tests run in CI (see
Testing Spec - Dynamic Discovery).
9. Deployed VM Hosts
| Host | Node | VM ID | Cores | RAM | Disk | MAC Address | hostId |
State Version |
|---|---|---|---|---|---|---|---|---|
hl02 |
pve1 |
101 |
2 | 4 GB | 20 GB | BC:24:11:D4:F6:65 |
92bbb1e6 |
25.11 |
Bringing up a new NixOS VM host consists of:
- Create
config/nix/hosts/<host>/per the host structure, with freshhostName,hostId, and the currentstateVersion. - Add the VM resource to the node's
vms-pve<N>.tffollowing the Terraform reference pattern, with a fresh VM ID and pinned MAC address. - Add the host to the
home_lab_nix_vmsgroup in the Ansible inventory (config/ansible/inventory/hosts.yml), which is the operational record of which hosts run NixOS. - Map the pinned MAC address to the designated IP for the host in the router's DHCP reservations.
- Stage the image artifacts and apply Terraform (see ISO Artifact Delivery).
-
Install the system:
nix develop .#operations -c nixos-anywhere --flake .#<host> root@<host>.<fqdn>
After installation, the comin role keeps the host converged with this
repository through the pull-based
GitOps model.
10. Assumptions and Constraints
10.1 Disk Device Configuration (/dev/sda)
The physical Disko configuration explicitly targets /dev/sda. This assumes the
Proxmox VM is provisioned using a VirtIO SCSI (virtio-scsi-pci)
controller. If the VM is provisioned using a VirtIO Block controller, the
disk will present as /dev/vda and partitioning will fail.
- Constraint: The VM must use a VirtIO SCSI disk interface (
scsi0) in Terraform to maintain support for TRIM/Discard on thin-provisioned ZFS pools.
10.2 Networking (DHCP)
VM hosts use DHCP for simplicity in this phase, with stable addressing provided by MAC-pinned router DHCP reservations. Transitioning to static IPs can be done in a future specification.
11. Future Work
Future work items are tracked centrally in the specs index.