5 03 Host Configurations
Nimmo edited this page 2026-05-30 22:40:08 +01:00

Chapter 3: Host Configurations

This chapter examines each host in detail: what it is, what it imports, and the design choices behind it.

The Hosts

Host Machine Channel Purpose
electra Framework 16 (AMD AI 300) unstable-small Primary machine — desktop, gaming, AI
lena Lenovo Ideapad 5 2-in-1 (Gen 9) unstable Family laptop
vega Home server (headless) unstable Home inference server, backups, monitoring

All Hosts Share One Foundation

All hosts import modules/common/base.nix, which pulls in:

# modules/common/base.nix imports:
./nix-settings.nix                          # Flakes, caches, parallelism, allowUnfree
./locale.nix                                # en_GB, Europe/London, UK keyboard
./default-config.nix                        # nixosConfig.* portability options
./sops-host.nix                             # sops-nix host key setup
../maintenance/garbage-collection.nix       # Daily GC, 3-day age limit
../maintenance/btrfs-maintenance.nix        # Monthly btrfs scrub
../services/beszel-agent.nix                # System monitoring agent
../services/backrest.nix                    # Automated backups (Backrest/restic)
inputs.disko.nixosModules.disko             # Declarative disk partitioning support

Plus bootloader (systemd-boot with @saved default to remember last selection), pcscd for YubiKey support, and NetworkManager with captive portal detection disabled.

Desktop hosts (electra, lena) also import modules/desktop/environment.nix:

# modules/desktop/environment.nix imports:
../common/system.nix            # Base system packages
./fonts.nix
../hardware/printing.nix
../hardware/removable-storage.nix
# Plus: programs.firefox.enable, AMD GPU utilities

Server hosts (vega) instead import modules/server/base.nix:

# modules/server/base.nix imports:
../common/system.nix            # Base system packages
# Plus: OpenSSH (no passwords, no root), mosh, network tools

Electra (Framework 16)

Electra has three boot tiers. See Chapter 4 for a full explanation of the specialisation mechanism.

Base (battery) tier

The base configuration is the battery-optimised minimal tier, used when booting without a specific specialisation selected.

# hosts/electra/default.nix (base tier)
imports = [
  ../../modules/common/base.nix
  ../../modules/desktop/environment.nix

  ./hardware-configuration.nix
  ../../modules/hardware/laptop.nix           # upower, powertop, endurance mode option

  ../../modules/boot/silent-boot.nix
  ../../modules/desktop/plasma.nix
  ../../modules/hardware/bluetooth.nix
  ../../modules/hardware/expansion-card-mount.nix
  ../../modules/power/auto-power-profile.nix
  ../../modules/services/btrbk-home.nix       # Hourly /home snapshots
  ../../modules/services/nixos-auto-update.nix
  ../../modules/profiles/ollama.nix           # Ollama on AMD iGPU (port 11434)
  ../../modules/networking/wifi-networks.nix
  ../../modules/networking/wireguard-vpn.nix
  ../../modules/profiles/ai-desktop.nix       # AI tools + opencode + desktop launchers
  ../../modules/profiles/maker.nix

  ../../modules/desktop/apps.nix
  ../../users/nimmo.nix

  ./hardware.nix                              # AMD iGPU configuration
  inputs.nixos-hardware.nixosModules.framework-16-amd-ai-300-series
  ../../modules/virtualization/docker-amd.nix
];

networking.hostName = "electra";
boot.kernelPackages = pkgs.linuxPackages_7_0;
boot.initrd.systemd.enable = true;          # Required for TPM2 unlock

# Endurance mode: automatic target is power-saver unless manually overridden
laptop.enduranceMode.enable = true;
power.autoPowerProfile.strategy = "endurance";
environment.etc."nixos-specialisation".text = "battery";
system.nixos.label = "battery";

# ROCm compatibility workaround for AMD RDNA3 780M iGPU
environment.variables.HSA_OVERRIDE_GFX_VERSION = "11.0.0";

# Nouveau blocked in case NVIDIA expansion bay is physically present
boot.blacklistedKernelModules = [ "nouveau" ];

# Auto-update: pull latest flake.lock from git and rebuild
services.nixos-auto-update = {
  enable = true;
  pullOnly = true;
};

The nixos-hardware module for Framework 16 AMD AI 300 series handles: AMD CPU microcode, amd-pstate, hardware.graphics, amdgpu initrd, framework-laptop-kmod, fprintd, IIO sensors, QMK, fstrim, power-profiles-daemon, fwupd, and kernel parameters.

igpu specialisation

The igpu tier is the full-featured daily-use configuration. It uses the dynamic power strategy, and adds gaming and Goose:

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;  # Linux 7.0 for AMD GPU improvements

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

The igpu tier inherits all base capabilities (Plasma, AI tools, Ollama, Docker AMD, maker tools) 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 and battery thresholds)
  • Full gaming stack
  • Goose desktop app

dgpu specialisation

The dgpu tier adds NVIDIA drivers, a second Ollama instance (on the dGPU), and NVIDIA Docker support:

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;

  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  # NVIDIA dGPU access
    ../../modules/virtualization/docker-amd.nix     # AMD 780M iGPU access (also present)
  ];

  profiles.ollama.nvidia.enable = true;
  power.autoPowerProfile.strategy = lib.mkForce "dynamic";
  environment.systemPackages = with pkgs; [ nvtopPackages.full ];

  # framework-tool with nvidia feature enabled
  nixpkgs.overlays = [
    (final: prev: {
      framework-tool = (inputs.framework-system.packages.${prev.stdenv.hostPlatform.system}.default).overrideAttrs (old: {
        buildFeatures = (old.buildFeatures or []) ++ [ "nvidia" ];
      });
    })
  ];
};

Both the AMD 780M iGPU and the NVIDIA dGPU are physically present in dgpu mode, so HSA_OVERRIDE_GFX_VERSION (set in the base) applies to the AMD GPU in both modes. The sharedSpecialisationImports pattern in the actual code avoids repeating the common igpu/dgpu imports.

Kernel selection summary

Tier Kernel Why
Base (battery) linuxPackages_7_0 Current Electra kernel series with endurance power strategy
igpu linuxPackages_7_0 Better AMD GPU driver support
dgpu linuxPackages_7_0 NVIDIA/AMD hybrid using the same current Electra kernel series

Hardware files

hardware.nix — imported by the base, configures AMD iGPU specifics (GTT memory extension, ROCm packages, PipeWire, zram swap, fingerprint).

hardware-dgpu.nix — imported only by the dgpu specialisation, adds the NVIDIA proprietary driver, Prime offload configuration, and NVIDIA kernel modules.

Host-specific Ollama providers

Electra declares host-specific Ollama endpoints for opencode via the profiles.aiDesktop.opencodeExtraProviders option, injecting local iGPU (port 11434) and dGPU (port 11435) providers into the opencode config. The inactive instance simply won't respond in the current boot tier.

Lena (Lenovo 2-in-1)

Lena uses the default unstable nixpkgs input. Its system.stateVersion remains 25.11, which records the version used at first install and should not be treated as the active package channel.

# hosts/lena/default.nix
imports = [
  ../../modules/common/base.nix
  ../../modules/desktop/environment.nix

  ./hardware-configuration.nix
  ./disko.nix
  ../../modules/hardware/laptop.nix
  inputs.nixos-hardware.nixosModules.lenovo-ideapad-16ahp9

  ../../modules/boot/silent-boot.nix
  ../../modules/desktop/plasma.nix
  ../../modules/power/auto-power-profile.nix
  ../../modules/hardware/bluetooth.nix
  ../../modules/networking/wifi-networks.nix
  ../../modules/networking/wireguard-vpn.nix
  ../../modules/profiles/ai-desktop.nix
  ../../modules/profiles/maker.nix
  ../../modules/services/nixos-auto-update.nix

  ../../modules/desktop/apps.nix
  ../../users/nimmo.nix
  ../../users/claire.nix               # Second user (not on any other host)
];

networking.hostName = "lena";
hardware.sensor.iio.enable = true;  # Tablet mode: screen rotation, etc.
services.fprintd.enable = true;     # Fingerprint reader
services.smartd.enable = true;      # S.M.A.R.T. disk monitoring

nixosConfig.wireguard = {
  enable = true;
  ipv4Address = "10.0.0.2/24";      # WireGuard IP (different from electra)
};

services.displayManager.sddm = {
  enable = true;
  autoNumlock = true;
};

services.nixos-auto-update = {
  enable = true;
  pullOnly = true;
};

Notable differences from electra:

  • Default unstable channel
  • disko.nix for declarative disk partitioning
  • lenovo-ideapad-16ahp9 nixos-hardware module for Lenovo-specific optimizations
  • Tablet mode support (hardware.sensor.iio.enable)
  • Two users: nimmo and claire
  • SDDM display manager (electra uses plasma-login-manager)
  • No specialisations, no Docker, no Ollama (no discrete GPU)
  • ai-desktop profile (same as electra — CLI tools + opencode, but no local Ollama providers configured)
  • WireGuard VPN client enabled (same wireguard-vpn.nix module as electra, different IP: 10.0.0.2/24)
  • Auto-update in pull-only mode

Vega (Home Server)

Vega is a headless NixOS server on the unstable channel. It runs AI agent tools, Docker, automated backups, and system monitoring.

# hosts/vega/default.nix
imports = [
  ../../modules/common/base.nix
  ../../modules/server/base.nix       # SSH, mosh, network tools

  ./hardware-configuration.nix
  ./disko.nix

  ../../modules/profiles/ai-agents.nix  # Claude Code, Codex CLI, MCP plugins
  ../../modules/virtualization/docker.nix
  ../../modules/services/nixos-auto-update.nix

  ../../users/nimmo.nix
];

networking.hostName = "vega";

# Persistent SSD storage (survives OS wipe, managed outside disko)
fileSystems."/mnt/storage" = {
  device = "/dev/disk/by-uuid/...";
  fsType = "btrfs";
  options = [ "compress=zstd:3" "noatime" "subvol=@storage" ];
};

fileSystems."/mnt/partial" = { ... };  # nodatacow for large volatile files

# NFS/CIFS support for tdarr NAS volumes
boot.supportedFilesystems = [ "nfs" "cifs" ];

services.nixos-auto-update = {
  enable = true;
  pullOnly = true;
};

Notable features:

  • modules/server/base.nix — OpenSSH (no passwords, no root), mosh, network tools
  • modules/profiles/ai-agents.nix — Claude Code + Codex CLI + nixd LSP + Claude Code MCP plugin (nixos), without desktop launchers
  • Docker (basic, no GPU access needed)
  • Two storage mounts on a separate persistent SSD (/mnt/storage, /mnt/partial)
  • NFS/CIFS support for accessing NAS volumes
  • Auto-update in pull-only mode
  • isServer = true — home-manager skips all desktop-only packages
  • No display manager, no Plasma, no GUI tools

The Specialisation Detection Mechanism

Electra writes the active tier name to /etc/nixos-specialisation at build time:

# Base:  environment.etc."nixos-specialisation".text = "battery";
# igpu:  environment.etc."nixos-specialisation".text = lib.mkForce "igpu";
# dgpu:  environment.etc."nixos-specialisation".text = lib.mkForce "dgpu";

This file is read by nixos-rebuild-auto (a shell function in home/nimmo.nix) and by the nixos-auto-update service so they can reactivate the correct specialisation after a switch:

nixos-rebuild-auto() {
  local CURRENT_SPEC=$(cat /etc/nixos-specialisation 2>/dev/null || echo "none")
  sudo nixos-rebuild switch --flake /home/nimmo/nixos-config#$(hostname)
  # nixos-rebuild switch always activates the base tier
  # Re-apply the specialisation if one was active
  if [ "$CURRENT_SPEC" != "none" ] && [ "$CURRENT_SPEC" != "battery" ] && \
     [ -d "/run/current-system/specialisation/$CURRENT_SPEC" ]; then
    sudo /run/current-system/specialisation/$CURRENT_SPEC/bin/switch-to-configuration switch
  fi
}

lib.mkForce is needed on the specialisation values because environment.etc merges entries by key at equal priority — without mkForce, the base value and the specialisation value would conflict.

Design Pattern: Where Does Configuration Go?

Question Where it goes
Should every host have this? modules/common/base.nix (or a module it imports)
Should every desktop host have this? modules/desktop/environment.nix (or a module it imports)
Should every server host have this? modules/server/base.nix
Should electra have this (all three tiers)? hosts/electra/default.nix (outside specialisation blocks)
Should igpu and dgpu tiers have this (not base)? Inside specialisation.igpu.configuration and specialisation.dgpu.configuration
Should only the dgpu tier have this? Inside specialisation.dgpu.configuration only
Is it a self-contained feature that hosts opt into? modules/<category>/
Is it specific to one machine's hardware? hosts/<hostname>/hardware*.nix
Should every user on every host have this software? modules/common/system.nix
Should nimmo have this on every host? home/nimmo.nix (via home-manager)
Should nimmo have this on desktop hosts only? home/nimmo.nix behind if !isServer then [...] else []

Adding a New Host

1. Generate the hardware configuration

On the new machine:

nixos-generate-config --show-hardware-config > hardware-configuration.nix

2. Create the host directory and default.nix

# hosts/newhostname/default.nix
{ config, pkgs, inputs, ... }:

{
  imports = [
    ../../modules/common/base.nix
    # ../../modules/desktop/environment.nix   # Add for desktop machines
    # ../../modules/server/base.nix           # Add for server/headless machines
    ./hardware-configuration.nix
    ../../modules/boot/silent-boot.nix
    # ... add modules as needed ...
    ../../users/nimmo.nix
  ];

  networking.hostName = "newhostname";
  system.stateVersion = "25.11";  # Set to your NixOS version at first install; never change
}

3. Add it to flake.nix

newhostname = makeNixosSystem {
  pkgs = nixpkgs;
  hmFlake = home-manager;
  configName = "newhostname";
  isServer = false;  # or true for headless servers
};

4. Stage, check, deploy

git add hosts/newhostname/
nix flake check
sudo nixos-rebuild switch --flake .#newhostname