Lab 0A — Journey single-server (KVM + containers)

Updated

July 30, 2026

Lab 0A — Journey single-server (KVM + containers)

Goal: Turn one physical machine into a lab server that can run the entire Nix & NixOS journey: disposable NixOS VMs under KVM/libvirt (default) or Incus (optional CLI-first path), light containers, enough disk for /nix and disk images, and a network layout that makes deploy-rs/Colmena feel real.

Audience: anyone on a single spare box (including HP Elitebook 2570p · 16 GB—see Lab 0C).

Note

Several operational ideas below are aligned with Bas Nijholt’s write-up of leaving Proxmox for NixOS + Incus (2025–2026): git as source of truth, CLI-first hypervisor ops, simulate host configs in VMs before reinstalling metal, and capturing hardware quirks as Nix—not shell history. Proxmox itself remains fine software; the point is reproducible host state, not “GUI bad.”

What this lab optimizes for

Curriculum need Lab feature
Days 1–10 Nix on host or VM nix available; flakes on
Days 11–22 break/rebuild NixOS VM snapshots + generations
Days 33–44 services One primary guest with RAM for DB/proxy
Days 57–70 deploy / multi-host Second small VM on same lab network
Containers days Podman and/or Incus system containers
Capstone wipe/rebuild Guest disks you can delete without guilt
Host recoverability Host flake + generations; optional “simulate host in VM” first

Architecture

                    ┌─────────────────────────────────────┐
                    │  libvirt default or lab bridge        │
                    │  192.168.122.0/24 (example NAT)       │
                    └─────────────────────────────────────┘
                           │              │
              ┌────────────┘              └────────────┐
              ▼                                        ▼
     ┌─────────────────┐                    ┌─────────────────┐
     │ nixlab          │  ssh               │ deploy-a        │
     │ 4–6G RAM        │ ─────────────────► │ 2G RAM          │
     │ journey primary │                    │ deploy target   │
     └─────────────────┘                    └─────────────────┘
              ▲
              │ edit flakes from host
     ┌─────────────────┐
     │ host (metal)    │
     │ KVM · git · nix │
     └─────────────────┘

NAT is enough for the journey. Bridge to LAN only if you need LAN access from VMs and understand your network.

Part 1 — Host OS install (metal)

Option H2: Debian/Ubuntu host

sudo apt update
sudo apt install -y qemu-kvm libvirt-daemon-system libvirt-clients \
  bridge-utils virt-manager
sudo usermod -aG libvirt,kvm "$USER"
# log out/in
virsh list --all

Install Nix multi-user on the host for Lab 0B (editing flakes without only working inside guests):

# official installer — follow nixos.org/download
nix --version

Part 2 — Storage plan (critical on 16 GB RAM laptops)

RAM is fine; disk is usually the failure mode.

Path Role Guidance
/nix Host store (if NixOS/Nix on host) 40–80+ GB free if possible
/var/lib/libvirt/images VM disks 40–100 GB free
~/src Flake git repos SSD strongly preferred

If the Elitebook still has a spinning HDD

  • Prefer a used SATA SSD (even 256–512 GB) before investing more evenings.
  • Or use external USB3 SSD for libvirt images + large store (acceptable for lab).

Thin provision

Use qcow2 with modest virtual size (e.g. 32–40 GB per guest) so you do not preallocate everything.

Part 3 — KVM: create journey VMs

Resource template (16 GB host)

VM vCPU RAM Disk Purpose
nixlab 2 4096–6144 MiB 32–40 G Main curriculum
deploy-a 1–2 2048 MiB 20–25 G Remote deploy target
builder (optional) 2 2048 20 G Offload builds (Stage V+)

Leave ≥4 GB for the host if you run a graphical desktop on metal.

Example: virt-install (adjust ISO path)

# Download NixOS 26.05 minimal ISO to ~/iso/
ISO=~/iso/nixos-minimal-26.05-x86_64-linux.iso

virt-install \
  --name nixlab \
  --memory 4096 \
  --vcpus 2 \
  --disk path=/var/lib/libvirt/images/nixlab.qcow2,size=40,format=qcow2 \
  --cdrom "$ISO" \
  --os-variant nixos-unknown \
  --network network=default \
  --graphics spice \
  --noautoconsole

Use virt-manager GUI if you prefer.

Snapshots = courage

Before risky Stage II/IV experiments:

virsh snapshot-create-as nixlab pre-stage-II "clean before break"
# restore:
# virsh snapshot-revert nixlab pre-stage-II

Also learn NixOS generations inside the guest—both matter.

Networking for “remote” deploy

On the host:

virsh net-dhcp-leases default
# note nixlab and deploy-a IPs
ssh YOU@192.168.122.XX

Put stable hostnames in host /etc/hosts or libvirt network XML if you get tired of changing IPs.

Part 4 — Containers (Podman)

Use containers for OCI days, not as a substitute for learning NixOS modules.

On host (NixOS):

virtualisation.podman.enable = true;
virtualisation.containers.enable = true;

Rootless Podman preferred for daily experiments.

podman run --rm -it docker.io/library/alpine:latest uname -a

When the curriculum says “declarative OCI on NixOS,” prefer defining that inside nixlab, so the journey guest remains the system under study.

Many operators eventually run Compose-style “pets” inside a VM while the host stays pure NixOS. That split (declarative host + stateful guest workloads) is valid later; early stages should still teach NixOS modules before outsourcing everything to containers.

Part 4b — Optional hypervisor: Incus (CLI-first VMs + system containers)

Incus (community fork of LXD) is a strong fit when you want one CLI for VMs and system containers, and when you care about agentic/automation workflows that hate opaque UIs. It is optional for this curriculum; libvirt remains the default on 16 GB laptops.

When to consider Incus on the lab host

Choose Incus if… Prefer libvirt if…
You want incus launch / profiles as daily API You already know virt-manager and want fastest start
You may import Proxmox LXC/VM images later You only need 1–2 simple NixOS guests
You like image profiles + clean CLI You want minimal host packages

Do not run full Incus + full libvirt stacks + desktop + heavy VMs on the Elitebook without a reason—pick a primary.

NixOS host sketch (Incus)

{
  virtualisation.incus.enable = true;
  # networking: follow current nixpkgs Incus docs for bridge (incusbr0) vs custom
  users.users.YOU.extraGroups = [ "incus-admin" ]; # group name may vary by package
  networking.firewall.trustedInterfaces = [ "incusbr0" ]; # only if you use default bridge
}

Verify against nixpkgs 26.05 option names (man configuration.nix / search.nixos.org)—Incus module knobs evolve.

incus admin init   # first-time interactive; then encode stable choices into docs/flake notes
incus list
# VM example (image names change — use `incus image list images:`):
# incus launch images:nixos/unstable nixlab --vm -c limits.memory=4GiB

Simulate the host before you reinstall metal

Powerful idea from the Proxmox→NixOS migration write-up: run an Incus (or libvirt) VM that imports almost the same modules as the physical host, with a small overrides file (disk device names, interface names, disable real-metal-only services).

modules/host-common.nix     ← ssh, libvirt/incus, users, nix settings
hosts/elitebook.nix         ← real hardware-configuration + product
hosts/elitebook-sim.nix     ← imports common + overrides (virtio NICs, qcow root)

Workflow:

  1. Get elitebook-sim healthy in a VM.
  2. Only then install/reinstall the physical Elitebook from the same flake.
  3. Keep incus-overrides.nix-style diffs tiny and commented.

This removes “nuking the only lab machine” fear—especially valuable once the host itself is NixOS.

If you already run Proxmox

You do not need to migrate mid-curriculum. If you later consolidate:

Workload Typical move
Proxmox LXC vzdump → import into Incus (community scripts exist; treat as advanced)
Proxmox VM disk qemu-img convert → qcow2 → Incus or libvirt import
NixOS journey guests Prefer fresh NixOS VMs for learning; migrate pets only

Keep migration notes in your flake repo (inventory of CT/VM IDs, disk backends). Optional reading: migration notes linked from the same article’s references.

Part 5 — Nested virtualization?

Elitebook Ivy Bridge: VT-x yes (enable in BIOS). Nested KVM (VM inside VM) is optional and often slow on this generation.

Need nested? When
No Default path: guests are the NixOS systems under test
Maybe You insist on running libvirt inside nixlab

Recommendation: do not nest until you have a specific reason.

Part 6 — Services the host should run (boring is good)

Service Why
sshd Access from your main laptop if Elitebook is headless-ish
libvirtd and/or incus Hypervisor
chrony/systemd-timesyncd TLS and logs make sense
Optional cockpit / virt-manager GUI if local—still commit CLI equivalents

Do not turn the host into the Stage IV app zoo. Guests carry curriculum apps.
(Exception later: a deliberate host service that is in the flake—e.g. a binary cache—never “I apt installed it at 2am.”)

Part 7 — Daily workflow for the journey

1. Open host terminal / editor on ~/src/nix-config
2. Edit flake modules for nixlab / deploy-a  (and host/ only when intentional)
3. Prefer: nix build / nixos-rebuild build-vm / --target-host
4. Apply inside guest; avoid unexplained one-off guest shell config
5. Snapshot before scary changes
6. Commit flake.lock + config  ← if it isn’t in git, it didn’t happen

Alternative: nixos-rebuild --target-host / deploy-rs from host into guests (Stage VI).

Debugging without drift

When something breaks, the order is:

  1. Reproduce
  2. Fix in Nix (or Incus profile / cloud-init you track)
  3. switch / redeploy
  4. Commit with a comment that cites why (forum link, hardware quirk)

Avoid: fix live → forget → six months of mystery. That is the same class of failure as “checkbox in a web UI three years ago.”

Part 8 — Declarative hardware quirks (host flake)

Older Intel NICs and laptops often need one-line kernel/driver workarounds. Example pattern (adapt; do not copy blindly):

# hosts/elitebook/networking.nix — illustrative
{
  # Document: link to bug/forum in a comment above the service.
  systemd.services.disable-nic-offload = {
    description = "Disable problematic NIC offloads on this hardware";
    wantedBy = [ "multi-user.target" ];
    after = [ "network-pre.target" ];
    serviceConfig.Type = "oneshot";
    serviceConfig.RemainAfterExit = true;
    script = ''
      # Example only — identify YOUR interface with `ip link`
      # ethtool -K enp0s25 tso off gso off gro off || true
    '';
    path = [ pkgs.ethtool ];
  };
}

On the Elitebook, when WiFi/Ethernet misbehaves: capture the fix here, not only in a successful interactive shell. Same for microcode, boot.kernelParams, and firmware.

Part 9 — Failure modes checklist

Symptom Fix
VMs won’t start BIOS VT-x; user in libvirtd/kvm / incus-admin groups
Host OOM killer Lower guest RAM; shut deploy-a when idle
Disk full GC carefully; delete old snapshots; move images to external SSD
Nested rebuild hell You nested by accident—flatten topology
WiFi-only flaky SSH Prefer Ethernet dock/USB ethernet for lab sessions
“Works after I typed stuff” unreproducible Re-apply from flake; delete untracked live changes
Incus + libvirt both fighting bridges Pick one primary network stack

Checkpoint — Lab 0A complete

  • Host installed and updated; host flake exists in git
  • virsh list --all or incus list works (primary stack chosen)
  • nixlab VM created (ISO install can finish on Day 11 if needed)
  • Snapshot (libvirt) or publish/restore strategy (Incus) tested once
  • Optional: host-sim VM validates host modules before metal reinstall
  • Podman (or Incus container) smoke test optional
  • Written note: IPs/hostnames of guests
  • Know wipe boundary: guests + lab disks; host only via generations + git
  • At least one hardware or network quirk documented in Nix if you hit it

Next

Lab 0B — NixOS dev machine (tooling on this same host)
Lab 0C — Elitebook 2570p (your hardware numbers)