Hardware Configuration and Boot in NixOS

hardware-configuration.nix

When you run the NixOS installer, it generates two files: configuration.nix and hardware-configuration.nix. The hardware file is auto-generated from what nixos-generate-config finds on your system: detected filesystems, UUIDs, kernel modules required to boot, and any hardware-specific settings.

You don't hand-edit hardware-configuration.nix directly. Let the installer generate it, commit it to your config repo, and regenerate it if you make significant hardware changes. Read through it anyway, because the kernel module list and device references directly affect whether the system boots:

# hardware-configuration.nix (auto-generated, trimmed for clarity)
{ config, lib, pkgs, modulesPath, ... }:
{
  imports = [ (modulesPath + "/installer/scan/not-detected.nix") ];

  boot.initrd.availableKernelModules = [
    "xhci_pci" "ahci" "nvme" "usbhid" "usb_storage" "sd_mod"
  ];
  boot.initrd.kernelModules = [];
  boot.kernelModules = [ "kvm-intel" ];
  boot.extraModulePackages = [];

  fileSystems."/" = {
    device = "/dev/disk/by-uuid/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";
    fsType = "ext4";
  };

  fileSystems."/boot" = {
    device = "/dev/disk/by-uuid/XXXX-XXXX";
    fsType = "vfat";
  };

  swapDevices = [];
}

Always use by-uuid or by-label device references, never /dev/sdX. Drive letter assignments aren't stable across reboots when you have multiple disks.

Boot Loader: systemd-boot vs GRUB

For UEFI systems (anything made after 2012), use systemd-boot. NixOS manages the generation entries automatically: each nixos-rebuild switch adds a boot entry and older ones are pruned based on configurationLimit. GRUB adds complexity you don't need unless you're on legacy BIOS or doing LUKS full-disk encryption.

boot.loader = {
  systemd-boot.enable = true;
  systemd-boot.configurationLimit = 10;
  efi.canTouchEfiVariables = true;
};

Use GRUB if you're on a legacy BIOS system or need more complex boot setups (multi-disk, full disk encryption with LUKS, etc.):

boot.loader = {
  grub = {
    enable = true;
    device = "/dev/sda";
    useOSProber = false;
  };
};

Set useOSProber = false unless you're dual-booting. On a dedicated server, os-prober just adds latency and noise.

Kernel Modules and Parameters

NixOS uses the kernel from nixpkgs, which tracks upstream LTS kernels. You can pin to a specific version:

boot.kernelPackages = pkgs.linuxPackages_6_6;

Or use the latest if you need newer driver support:

boot.kernelPackages = pkgs.linuxPackages_latest;

Kernel parameters go in boot.kernelParams:

boot.kernelParams = [
  "intel_iommu=on"   # for GPU/device passthrough
  "iommu=pt"
  "zfs.zfs_arc_max=4294967296"  # cap ZFS ARC at 4GB
];

ZFS Pool Setup

ZFS is where NixOS really shines compared to other distros. NixOS has native ZFS support: no out-of-tree kernel modules, no DKMS headaches, no version skew problems between the module and your kernel.

First, add ZFS support and import your pool:

boot.supportedFilesystems = [ "zfs" ];
boot.zfs.forceImportRoot = false;
networking.hostId = "deadbeef";  # required by ZFS; generate with: head -c4 /dev/urandom | od -A none -t x4 | tr -d ' '

The hostId is a ZFS requirement. It prevents a pool from being imported on the wrong host in a multi-host setup. Generate a random one during install and keep it in your config.

Declare your ZFS datasets as filesystems in configuration.nix:

fileSystems."/data" = {
  device = "tank/data";
  fsType = "zfs";
};

fileSystems."/data/media" = {
  device = "tank/media";
  fsType = "zfs";
};

ZFS dataset properties (compression, recordsize, atime, etc.) aren't managed by NixOS directly. Set those with zfs set after pool creation or in a systemd.services activation script. This is one area where you have to step outside the declarative model slightly, but the pool topology itself is stable once created.

Enable scrubs on a schedule:

services.zfs.autoScrub.enable = true;
services.zfs.autoScrub.interval = "weekly";

And automatic snapshots if you use zfs-auto-snapshot:

services.zfs.autoSnapshot = {
  enable = true;
  frequent = 4;
  hourly = 24;
  daily = 7;
  weekly = 4;
  monthly = 12;
};

This gives you a pretty solid snapshot policy out of the box with no cron wrangling.

Next up: nginx, TLS, and DDNS.