Specialisations

Updated

October 31, 2026

Specialisations

Goal: Ship boot-menu alternate configurations from one flake host using NixOS specialisation.<name>.configuration, know how to activate them at runtime vs reboot, and when a specialisation beats a second container or VM.

NixOS 26.05 (“Yarara”) keeps specialisations as first-class boot entries built beside the parent system. Channel Nix is typically 2.34.x; many desks run Nix CLI 2.35+ via nix.package — specialisations do not depend on that split, but pin your notes to nixos-version / nix --version on the lab.

Mental model

nixosConfigurations.lab
        │
        ▼
 parent system closure  (default boot entry)
        │
        ├── specialisation.nvidia.configuration  →  extra closure + boot submenu entry
        ├── specialisation.rescue.configuration →  …
        └── …
                │
                ▼
 /run/current-system/specialisation/<name>/bin/switch-to-configuration
Piece Role
specialisation.<name>.configuration Nested NixOS module(s) for the variant
inheritParentConfig (default true) Merge on top of the parent; false = build from scratch
Boot loader entries Choose the specialisation at boot (kernel/initrd differences need reboot)
nixos-rebuild switch --specialisation <name> Activate that variant after a rebuild
/run/current-system/specialisation/<name>/… Switch without a full rebuild when the closure already exists
Nested specialisations Ignored — do not nest

Names must not contain /.

When to use vs containers / second hosts

Need Prefer
Same machine, same disks, alternate kernel / drivers / desktop / “laptop on battery vs docked GPU” Specialisation
Strong isolation, different OS, throwaway rootfs Container or VM
Long-lived second identity with different networking policy Separate nixosConfigurations host or VM
One-off package test nix shell / vm test — not a specialisation

Specialisations still share the machine’s hardware and usually much of the store; they are configuration variants, not security boundaries.

Worked examples

Case 1: GPU-off by default, GPU-on specialisation

Parent keeps heavy drivers off with mkDefault so the child can flip them:

# hosts/lab/specialisations-gpu.nix
{ lib, pkgs, ... }:

{
  # Parent desk: drivers off unless a specialisation overrides
  hardware.nvidia.modesetting.enable = lib.mkDefault false;

  specialisation.nvidia = {
    configuration = {
      system.nixos.tags = [ "nvidia" ];
      hardware.nvidia.modesetting.enable = true;
      # Add the rest of your NVIDIA module stack here as you already do on a GPU host
    };
  };
}

Import that file from the host module list. Rebuild:

sudo nixos-rebuild switch --flake .#lab
# Boot menu should offer a specialised entry tagged/named for nvidia

Runtime activate (userspace switch — does not reload the kernel):

sudo nixos-rebuild switch --flake .#lab --specialisation nvidia
# Or, if already built into the current system:
sudo /run/current-system/specialisation/nvidia/bin/switch-to-configuration switch

If the specialisation changes the kernel or early initrd modules, reboot and pick the boot entry; a userspace switch will not magically load the other kernel.

Case 2: Independent specialisation (inheritParentConfig = false)

# hosts/lab/specialisation-kiosk.nix
{ pkgs, ... }:

{
  specialisation.kiosk = {
    inheritParentConfig = false;
    configuration = {
      system.nixos.tags = [ "kiosk" ];
      imports = [ ./kiosk-baseline.nix ];
      users.users.kiosk = {
        isNormalUser = true;
        extraGroups = [ "video" ];
      };
      services.displayManager.autoLogin = {
        enable = true;
        user = "kiosk";
      };
      environment.systemPackages = with pkgs; [ chromium ];
    };
  };
}

Use this when the variant should not inherit desk services, VPN, or extra users from the parent. You own the full module closure — easy to under-specify boot/networking; test on a disposable VM first.

Case 3: Parent-only config (not pulled into children)

When specialisation != {}, you can attach options that apply to the default system only:

# modules/parent-only-opengl.nix
{ lib, config, pkgs, ... }:

{
  config = lib.mkIf (config.specialisation != { }) {
    # Example: keep a parent-only package set out of specialised closures
    environment.systemPackages = lib.mkBefore [ pkgs.pciutils ];
  };
}

If no specialisations exist, config.specialisation != {} is false — do not rely on this pattern to mean “always on the default host” in a config with zero specialisations.

Case 4: Inspect built specialisations

ls -la /run/current-system/specialisation/
readlink -f /run/current-system/specialisation/nvidia
nixos-version
nix --version

After switch, confirm tags:

cat /run/current-system/nixos-version
# specialised systems often carry system.nixos.tags in the banner / labels you set

Activation cheat sheet

Action Command
Build parent + all specialisations sudo nixos-rebuild switch --flake .#lab
Switch into one by name sudo nixos-rebuild switch --flake .#lab --specialisation <name>
Switch using already-built path sudo /run/current-system/specialisation/<name>/bin/switch-to-configuration switch
Test without adding a boot generation the same way you usually test Prefer switch-to-configuration test on that path when experimenting
Kernel / initrd divergent Reboot into the boot-menu entry

Plain nixos-rebuild switch without --specialisation returns you to the parent default — a common surprise after you had activated a child.

The trap

Trap A — treat specialisations like containers. They do not isolate failures or untrusted workloads.

Trap B — expect switch --specialisation to change the running kernel. Boot-menu reboot required for kernel/initrd.

Trap C — override parent booleans without mkDefault on the parent. The merge may not let the child win; set parent defaults with lib.mkDefault.

Trap D — nest specialisations. Nested entries are ignored; flatten the matrix or use separate hosts.

Trap E — huge Cartesian products. Each specialisation is another system closure to build and store; prefer two or three named modes, not a dozen.

Try this

  1. Add a specialisation.demo.configuration that only sets environment.variables.DEMO = "1"; and system.nixos.tags.
  2. nixos-rebuild switch, then activate with --specialisation demo and echo $DEMO in a new login.
  3. Change a kernel package in the specialisation on a VM, reboot into the entry, confirm uname -r.
  4. Document on the rollback card: how to get back to the parent (switch without --specialisation + reboot if needed).

Sources