3 04 Specialisations
Nimmo edited this page 2026-05-30 22:40:08 +01:00

Chapter 4: NixOS Specialisations

This chapter explains the specialisation mechanism and how electra uses it to implement a three-tier boot system from a single configuration.

What Is a Specialisation?

A NixOS specialisation is a named variant of the base system configuration. You declare it inside specialisation.<name>.configuration:

# In hosts/electra/default.nix

# Everything outside the specialisation blocks is the "base" configuration
networking.hostName = "electra";
boot.kernelPackages = pkgs.linuxPackages_7_0;
power.autoPowerProfile.strategy = "endurance";
# ... all the base config ...

specialisation.igpu.configuration = {
  # Overrides and additions for the "igpu" variant
  boot.kernelPackages = lib.mkForce pkgs.linuxPackages_7_0;  # Linux 7.0 for better AMD GPU support
  power.autoPowerProfile.strategy = lib.mkForce "dynamic";
  imports = sharedSpecialisationImports ++ [
    ../../modules/profiles/maker.nix
  ];
};

specialisation.dgpu.configuration = {
  # Overrides and additions for the "dgpu" variant
  boot.kernelPackages = lib.mkForce pkgs.linuxPackages_7_0;
  power.autoPowerProfile.strategy = lib.mkForce "dynamic";
  imports = sharedSpecialisationImports ++ [
    inputs.nixos-hardware.nixosModules.framework-16-amd-ai-300-series-nvidia
    ./hardware-dgpu.nix
    ../../modules/virtualization/docker-nvidia.nix
    ../../modules/virtualization/docker-amd.nix
  ];
  profiles.ollama.nvidia.enable = true;
};

A single nixos-rebuild switch --flake .#electra builds all three: the base and both specialisations. They appear as separate entries in the systemd-boot boot menu.

What specialisations inherit

A specialisation inherits everything from the base configuration. The content of specialisation.<name>.configuration is overlaid on top of the base — it only needs to declare what differs.

This means:

  • If the base installs Plasma, all specialisations also have Plasma
  • If the base sets a hostname, all specialisations use the same hostname
  • If the base imports a profile, all specialisations get that profile's packages
  • Only values explicitly overridden (with lib.mkForce) or added (via imports) differ

When to use lib.mkForce

If the base already sets an option and the specialisation needs a different value, you must use lib.mkForce to override it. Without it, setting the same option twice causes a conflict error.

# Base sets:
boot.kernelPackages = pkgs.linuxPackages_7_0;

# igpu specialisation reinforces:
boot.kernelPackages = lib.mkForce pkgs.linuxPackages_7_0;
#                     ^^^^^^^^^^^ used because the specialisation sets the option explicitly

# dgpu specialisation reinforces:
boot.kernelPackages = lib.mkForce pkgs.linuxPackages_7_0;

For list options (like environment.systemPackages), no force is needed — the specialisation's list is concatenated with the base list. lib.mkForce is only needed for singleton options.

Electra's Three Tiers

Electra has three boot tiers that map to three different hardware and use-case scenarios.

Why three tiers?

The Framework 16 expansion bay supports swappable modules including an NVIDIA GPU. Swapping requires a full power-off. Each physical configuration benefits from a tailored software profile:

Tier Hardware state Use case
Base (battery) Any (no expansion bay GPU in use) Travel, battery life, minimal footprint
igpu AMD 780M iGPU only Daily use, gaming, full desktop features
dgpu AMD 780M + NVIDIA dGPU GPU-intensive work, NVIDIA Ollama, CUDA tasks

The boot menu remembers your last selection (via @saved in the bootloader config), so rebooting after an update reactivates the same tier automatically.

Base (battery) tier

The base is the battery-optimised minimal configuration. It is active whenever no specialisation has been selected, and is the fallback if something goes wrong.

What the base provides:

  • KDE Plasma 6 desktop
  • AMD 780M iGPU via hardware.nix
  • Docker with AMD iGPU access (docker-amd.nix)
  • Endurance mode power settings (laptop.enduranceMode.enable = true; automatic strategy targets power-saver)
  • Ollama on AMD iGPU + Open WebUI (ai-desktop profile)
  • AI CLI tools (Claude Code, Codex CLI, opencode)
  • Linux 7.0 kernel series
  • Hourly btrfs snapshots of /home
  • All base packages and services

What the base intentionally omits:

  • Dynamic power strategy (uses the endurance target instead)
  • Gaming profile
  • Goose desktop app
  • NVIDIA drivers

Marker:

environment.etc."nixos-specialisation".text = "battery";
system.nixos.label = "battery";

igpu specialisation

The igpu tier is the full-featured daily-use configuration when running on iGPU only.

What igpu adds to the base:

specialisation.igpu.configuration = {
  environment.etc."nixos-specialisation".text = lib.mkForce "igpu";
  system.nixos.label = lib.mkForce "igpu";
  boot.kernelPackages = lib.mkForce pkgs.linuxPackages_7_0;

  power.autoPowerProfile.strategy = lib.mkForce "dynamic";
  imports = sharedSpecialisationImports ++ [
    ../../modules/profiles/maker.nix
  ];
};

The igpu tier inherits all base capabilities (Plasma, Ollama, AI tools, Docker AMD) and adds:

  • Dynamic power strategy on the same Linux 7.0 kernel series
  • Dynamic power profile (switches between power-saver/balanced/performance based on AC state)
  • Full gaming stack
  • Goose desktop app

dgpu specialisation

The dgpu tier is for when the NVIDIA expansion bay is installed. It adds NVIDIA drivers, a second Ollama instance dedicated to the dGPU, and NVIDIA Docker support.

What dgpu adds to the base:

specialisation.dgpu.configuration = {
  environment.etc."nixos-specialisation".text = lib.mkForce "dgpu";
  system.nixos.label = lib.mkForce "dgpu";
  boot.kernelPackages = lib.mkForce pkgs.linuxPackages_7_0;
  power.autoPowerProfile.strategy = lib.mkForce "dynamic";

  imports = sharedSpecialisationImports ++ [
    inputs.nixos-hardware.nixosModules.framework-16-amd-ai-300-series-nvidia
    ./hardware-dgpu.nix                        # NVIDIA drivers + Prime offload
    ../../modules/virtualization/docker-nvidia.nix  # Docker with NVIDIA CDI
    ../../modules/virtualization/docker-amd.nix     # Docker with AMD 780M (also in base)
  ];

  profiles.ollama.nvidia.enable = true;
  environment.systemPackages = with pkgs; [ nvtopPackages.full ];
};

The gaming and goose imports are shared between the igpu and dgpu tiers using a sharedSpecialisationImports let binding in the actual code. The auto-power module is imported by the base, with power.autoPowerProfile.strategy forced to dynamic in the igpu/dgpu tiers. The dgpu tier additionally imports docker-amd.nix to ensure both AMD and NVIDIA GPU access coexist in Docker.

Key interactions between tiers

HSA_OVERRIDE_GFX_VERSION: Set in the base, inherited by both specialisations. The AMD 780M iGPU is physically present in all three hardware states, so the ROCm compatibility override applies everywhere. In the dgpu tier, this means both GPUs have their correct ROCm settings.

boot.blacklistedKernelModules = [ "nouveau" ]: Set in the base, ensures Nouveau does not load even when the NVIDIA bay is physically present but not yet in active use. The proprietary NVIDIA driver in hardware-dgpu.nix takes over in the dgpu tier.

nouveau vs NVIDIA: When the NVIDIA expansion bay is installed and you boot into the base or igpu tier, nouveau is blocked so it cannot cause kernel module conflicts. The NVIDIA proprietary driver is only loaded in the dgpu tier.

How Specialisations Appear in the Boot Menu

After nixos-rebuild switch --flake .#electra, systemd-boot shows three entries:

NixOS (battery)      ← base tier
NixOS (igpu)         ← igpu specialisation
NixOS (dgpu)         ← dgpu specialisation

The labels come from system.nixos.label. The bootloader is configured with @saved as the default entry, which means it remembers and re-selects the last-chosen entry across reboots.

To switch tiers, reboot and select the desired entry. There is no way to switch tiers without rebooting — the difference between base, igpu, and dgpu includes kernel version and GPU drivers, which cannot change on a running system.

Activating a Specialisation After nixos-rebuild switch

nixos-rebuild switch always activates the base configuration, even if a specialisation was running before. If you want to stay in a specialisation after a manual rebuild, you must reactivate it:

sudo /run/current-system/specialisation/igpu/bin/switch-to-configuration switch

The nixos-rebuild-auto function in home/nimmo.nix automates this:

  1. Reads /etc/nixos-specialisation to know which tier was active
  2. Runs nixos-rebuild switch
  3. If the saved tier was igpu or dgpu, re-applies the specialisation

The nixos-auto-update service does the same thing when running automatic overnight updates, ensuring electra wakes up in the same tier it was in.

Building Specialisations

Since all three tiers are built in a single command, you can inspect any of them:

# Build without activating — result symlink appears in current directory
sudo nixos-rebuild build --flake .#electra

# Inspect the built specialisation outputs
ls result/specialisation/
# igpu/  dgpu/

The result/ symlink points to the base tier. result/specialisation/igpu/ and result/specialisation/dgpu/ are the specialisation closures.

In the Nix REPL:

nix repl
:lf .
nixosConfigurations.electra.config.specialisation
# Shows the igpu and dgpu configurations

Using Specialisations Elsewhere

The specialisation mechanism is not specific to GPU switching. Any configuration that requires a reboot to change and has several valid variants is a good candidate:

  • Different kernel variants
  • Desktop-vs-minimal modes on a single machine
  • Experimental configurations you want in the boot menu alongside a stable baseline

For electra specifically, the three-tier model makes permanent the earlier approach of two separate flake configurations that had to be kept in sync via a service. Specialisations enforce sync by construction.