artemis: Small microVM host

{
  imports = [
    ./charizard.nix
    ./charmander.nix
    ./charmeleon.nix
    ./mrmime.nix
    ./nidoking.nix
  ];

  networking.hostName = "artemis";
  networking.hostId = "88d7144a";
  nixpkgs.hostPlatform = "x86_64-linux";
  system.stateVersion = "25.05";

  <<config>>

  _class = "nixos";
}

artemis is a small form factor Dell OptiPlex 3060 with 16GiB of RAM. It runs a small number of VMs, mostly ones that are redundant with VMs running elsewhere. The one exception is mrmime, which runs my Home Assistant setup.

boot.initrd.availableKernelModules = [
  "xhci_pci"
  "ahci"
  "nvme"
  "usbhid"
  "sd_mod"
];
boot.kernelModules = [ "kvm-intel" ];
hardware.enableRedistributableFirmware = true;

This comes from nixos-generate-config based on detected hardware in the machine.

fileSystems."/nix" = {
  device = "rpool/crypt/nix";
  fsType = "zfs";
};

mjm.zfs.enable = true;
mjm.zfs.arcMax = 4 * 1024;
mjm.zfs.encryption.rootPool = "rpool";

artemis uses ZFS. For important filesystems that need to be mounted in stage 1, I set mountpoint=legacy and mount them with fstab. I set a lower maximum for the ARC cache because demeter only has 16G of RAM. demeter is using ZFS native encryption with support from my ZFS module.

fileSystems."/persist" = {
  device = "rpool/crypt/persist";
  fsType = "zfs";
  neededForBoot = true;
};

mjm.state.persistDir = "/persist";
system.etc.overlay.mutable = false;

artemis is using an ephemeral root filesystem, so / is a tmpfs and things that must be persisted are bind-mounted from /persist. The use of an ephemeral root also makes it easier to adopt the immutable etc overlay.

boot.zfs.extraPools = [ "slow" ];

artemis also has a spinning disk in it, which is used for local backups of VM data.

fileSystems."/boot" = {
  device = "/dev/disk/by-partlabel/boot";
  fsType = "vfat";
  options = [
    "fmask=0077"
    "dmask=0077"
  ];
};

mjm.secureboot.enable = true;
boot.loader.efi.canTouchEfiVariables = true;
boot.lanzaboote.measuredBoot.enable = false;

artemis uses my secureboot module to set up Lanzaboote for SecureBoot. It can't use measured boot, because the TPM in this machine doesn't support the necessary operation.

Lanzaboote is built on systemd-boot, so it needs the EFI system partition mounted at /boot.

mjm.server.enable = true;

This machine is running as a server, so it needs the common server infrastructure for my lab.

mjm.spire.agent.tpm.publicKeyHash =
  "8e218669f5dad42ea590329f451a8b91db0eb8ea62fef908eff18fdb3617913b";

Any of my machines that has a TPM uses it to attest to the SPIRE server and get its node identity. The public key hash here is specified only to create a registration entry that aliases this public key identity to the hostname of the machine.

mjm.microvm-host.enable = true;
mjm.microvm-host.zfsPrefix = "rpool/crypt";
mjm.networkd.secondaryLinkName = "lan1";
mjm.networkd.macvlan.enable = true;

artemis's sole purpose is to host microVMs, with their data living in the rpool/crypt/microvms dataset. All microVM hosts use MACVLAN to provide network devices to the VMs, and they all do so with a dedicated secondary NIC named lan1 (as opposed to lan0, which is reserved for the host).

mjm.remote-builder.enable = true;

My microVM hosts also do double duty as remote builders for CI, specifically for running NixOS VM tests.

mjm.nut = {
  enable = true;
  connectedUPSName = "or500";
};

All physical hosts in my homelab run NUT to be able to be shutdown properly in the case of power loss. Since I have two UPSes in the rack, each machine is configured with the one it is connected to, so that will be the one that it monitors.

microVMs

=> charizard
=> charmander
=> charmeleon
=> mrmime
=> nidoking

Proxy Information
Original URL
gemini://midna.dev/homelab/hosts/artemis/
Status Code
Success (20)
Meta
text/gemini;lang=en-US
Capsule Response Time
16.156432 milliseconds
Gemini-to-HTML Time
0.325201 milliseconds

This content has been proxied by September (UNKNO).