Specialisations
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 nvidiaRuntime 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 switchIf 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 --versionAfter switch, confirm tags:
cat /run/current-system/nixos-version
# specialised systems often carry system.nixos.tags in the banner / labels you setActivation 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
- Add a
specialisation.demo.configurationthat only setsenvironment.variables.DEMO = "1";andsystem.nixos.tags. nixos-rebuild switch, then activate with--specialisation demoandecho $DEMOin a new login.- Change a kernel package in the specialisation on a VM, reboot into the entry, confirm
uname -r. - Document on the rollback card: how to get back to the parent (
switchwithout--specialisation+ reboot if needed).