Table of contents
- Chapter 1: Configuration Structure
- Directory Layout
- The Import Chain
- How flake.nix builds each host
- Electra's import chain
- Lena's import chain
- Vega's import chain
- Lyra's import chain
- The Four Layers
- Layer 1: Common (all hosts)
- Layer 2: Desktop vs. Server foundation
- Layer 3: Opt-in feature modules
- Layer 4: Host-specific config, users, and home-manager
- How Options Merge Across Layers
- Design Decisions Worth Understanding
Chapter 1: Configuration Structure
This chapter maps out exactly how your repository is organized and how all the pieces connect.
Directory Layout
nixos-config/
├── flake.nix # Entry point: inputs, makeNixosSystem helper, host definitions
├── flake.lock # Pinned input versions (never edit by hand)
│
├── home/
│ ├── nimmo.nix # Home-manager config: user packages, shell, git, dotfiles
│ └── plasma.nix # Declarative KDE Plasma settings (via plasma-manager)
│
├── hosts/
│ ├── electra/ # Framework 16 laptop (3-tier boot system)
│ │ ├── default.nix # Base (battery) + igpu + dgpu specialisations
│ │ ├── hardware-configuration.nix # Auto-generated hardware detection
│ │ ├── framework-16.nix # Framework-specific hardware configuration
│ │ ├── disko.nix # Declarative encrypted disk layout
│ │ └── framework-16.nix # Framework 16-specific notes
│ │
│ ├── lena/ # Lenovo Ideapad 5 2-in-1
│ │ ├── default.nix # Tablet mode, fingerprint, two users
│ │ ├── hardware-configuration.nix # Auto-generated hardware detection
│ │ └── disko.nix # Declarative disk partitioning
│ │
│ ├── vega/ # Home server
│ │ ├── default.nix # Server config, storage mounts, AI agents
│ │ ├── hardware-configuration.nix # Auto-generated hardware detection
│ │ └── disko.nix # Declarative disk partitioning
│ │
│ └── lyra/ # GPU compute and media server
│ ├── default.nix # NVIDIA, Ollama, Docker stacks
│ ├── hardware-configuration.nix # Auto-generated hardware detection
│ └── disko.nix # Declarative disk partitioning
│
├── modules/ # Reusable feature modules
│ ├── boot/
│ │ └── silent-boot.nix # Plymouth splash, quiet boot
│ ├── common/
│ │ ├── base.nix # Foundation for ALL hosts: bootloader, networking, sops, GC, etc.
│ │ ├── default-config.nix # Portability options (user, email, paths)
│ │ ├── locale.nix # Timezone, language, keyboard
│ │ ├── nix-settings.nix # Nix daemon configuration, binary caches
│ │ ├── sops-host.nix # sops-nix host key configuration
│ │ └── system.nix # System packages for all hosts (git, htop, jq, sops, etc.)
│ ├── desktop/
│ │ ├── apps.nix # Desktop applications (Kate, LibreOffice, VLC, etc.)
│ │ ├── audio.nix # PipeWire audio configuration
│ │ ├── environment.nix # Desktop foundation: system.nix + fonts + printing + removable-storage
│ │ ├── flatpak-apps.nix # Flatpak application list
│ │ ├── fonts.nix # Font packages and fontconfig defaults
│ │ └── kde-plasma.nix # KDE Plasma 6 desktop environment
│ ├── hardware/
│ │ ├── bluetooth.nix # Bluetooth with experimental features
│ │ ├── expansion-card-mount.nix # 1TB expansion card automount (electra only)
│ │ ├── laptop.nix # Shared laptop hardware (upower, powertop, endurance mode option)
│ │ ├── printing.nix # CUPS printing support
│ │ └── removable-storage.nix # udisks2, NTFS support, polkit mount rules
│ ├── maintenance/
│ │ ├── btrfs-maintenance.nix # Monthly btrfs scrub (all hosts)
│ │ └── garbage-collection.nix # Daily GC (7-day age limit)
│ ├── networking/
│ │ ├── wifi-networks.nix # Known Wi-Fi networks (desktop hosts)
│ │ └── wireguard-vpn.nix # WireGuard VPN client (desktop hosts)
│ ├── power/
│ │ ├── auto-power-profile.nix # Battery-triggered switching + manual override guard
│ │ └── battery-efficiency.nix # Reversible EPP and energy-preference tuning (laptop base/battery)
│ ├── profiles/
│ │ ├── ai-agents.nix # Codex CLI and managed Nix/Python/Beszel/Forgejo MCP clients
│ │ ├── ai-desktop.nix # Imports ai-agents + opencode, desktop launchers, Ollama providers
│ │ ├── gaming.nix # Steam, GameMode, MangoHud, Discord
│ │ ├── gui.nix # Common GUI foundation: base, desktop, kde-plasma, apps, users
│ │ ├── jobseeking.nix # Job-search tools and resources
│ │ ├── laptop.nix # Laptop profile: gui + bluetooth + laptop hardware + wifi
│ │ ├── maker.nix # 3D printing, electronics, CAD tools
│ │ └── ollama.nix # Ollama/Open WebUI profile for AMD/NVIDIA instances
│ ├── server/
│ │ └── base.nix # Server foundation: SSH, mosh, network tools
│ ├── services/
│ │ ├── backrest.nix # Automated backups via Backrest/restic (NAS target)
│ │ ├── beszel-agent.nix # System monitoring agent (reports to Beszel dashboard)
│ │ ├── btrbk-home.nix # Hourly btrfs snapshots of /home (electra)
│ │ ├── forgejo-mcp.nix # Vega-hosted Forgejo issues/wiki MCP service
│ │ ├── nixos-auto-update.nix # Automatic updates and pull-only rebuilds
│ │ └── ollama-common.nix # Shared Ollama systemd helper
│ └── virtualization/
│ ├── docker.nix # Docker (basic)
│ ├── docker-amd.nix # Docker with AMD iGPU (ROCm) access
│ └── docker-nvidia.nix # Docker with NVIDIA dGPU access
│
├── secrets/
│ ├── <service-or-host>.yaml # Scoped encrypted secrets (sops-nix, age-encrypted)
│ └── README.md # Secrets management documentation
│
├── scripts/
│ └── check-flake.sh # Container-based flake validation (podman)
│
└── users/
├── nimmo.nix # Admin user account definition (groups only)
└── claire.nix # Standard user account (lena only)
The Import Chain
The key to understanding this configuration is tracing how modules get imported, starting from flake.nix.
How flake.nix builds each host
Rather than repeating nixpkgs.lib.nixosSystem calls for each host, flake.nix defines a helper function makeNixosSystem that encapsulates the shared setup:
let
makeNixosSystem = { pkgs, hmFlake, configName, isServer }:
pkgs.lib.nixosSystem {
specialArgs = { inherit inputs; };
modules = [
./hosts/${configName} # Host entry point
sops-nix.nixosModules.sops # Encrypted secrets
hmFlake.nixosModules.home-manager
({ config, ... }: {
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
home-manager.backupFileExtension = "backup";
home-manager.sharedModules = [ inputs.plasma-manager.homeModules.plasma-manager ];
home-manager.users.${config.nixosConfig.primaryUser} = import ./home/${config.nixosConfig.primaryUser}.nix;
home-manager.extraSpecialArgs = {
inherit inputs;
isServer = isServer;
flakeRepo = config.nixosConfig.flakeRepo;
primaryUser = config.nixosConfig.primaryUser;
userEmail = config.nixosConfig.userEmail;
};
})
];
};
in
{
nixosConfigurations = {
electra = makeNixosSystem { pkgs = nixpkgs-unstable-small; configName = "electra"; isServer = false; };
lena = makeNixosSystem { configName = "lena"; isServer = false; };
vega = makeNixosSystem { configName = "vega"; isServer = true; };
lyra = makeNixosSystem { configName = "lyra"; isServer = true; };
};
}
The isServer flag is forwarded to home-manager as extraSpecialArgs. Home-manager modules can read it to skip installing desktop-only software on headless machines. Electra and lena are desktop machines (isServer = false); vega and lyra are headless servers (isServer = true).
Electra's import chain
Electra has three boot tiers built from one configuration file. The base tier is "battery" mode; the two specialisations (igpu and dgpu) extend the base:
flake.nix → makeNixosSystem (electra, isServer=false)
├── hosts/electra/default.nix (host entry point — base/battery tier)
│ ├── modules/profiles/laptop.nix (laptop profile aggregator)
│ │ ├── modules/profiles/gui.nix (GUI foundation)
│ │ │ ├── modules/common/base.nix
│ │ │ │ ├── modules/common/nix-settings.nix
│ │ │ │ ├── modules/common/locale.nix
│ │ │ │ ├── modules/common/default-config.nix
│ │ │ │ ├── modules/common/sops-host.nix
│ │ │ │ ├── modules/maintenance/garbage-collection.nix
│ │ │ │ ├── modules/maintenance/btrfs-maintenance.nix
│ │ │ │ ├── modules/services/beszel-agent.nix
│ │ │ │ └── modules/services/backrest.nix
│ │ │ ├── modules/desktop/environment.nix
│ │ │ │ ├── modules/common/system.nix
│ │ │ │ ├── modules/desktop/fonts.nix
│ │ │ │ ├── modules/hardware/printing.nix
│ │ │ │ └── modules/hardware/removable-storage.nix
│ │ │ ├── modules/boot/silent-boot.nix
│ │ │ ├── modules/desktop/kde-plasma.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
│ │ │ ├── modules/desktop/flatpak-apps.nix
│ │ │ └── users/nimmo.nix
│ │ ├── modules/hardware/bluetooth.nix
│ │ ├── modules/hardware/laptop.nix
│ │ └── modules/networking/wifi-networks.nix
│ │
│ ├── hosts/electra/hardware-configuration.nix
│ ├── modules/hardware/expansion-card-mount.nix
│ ├── modules/power/auto-power-profile.nix
│ ├── modules/power/battery-efficiency.nix
│ ├── modules/services/btrbk-home.nix
│ ├── modules/profiles/ollama.nix
│ ├── modules/profiles/jobseeking.nix
│ ├── hosts/electra/framework-16.nix
│ ├── hosts/electra/disko.nix
│ ├── [nixos-hardware: framework-16-amd-ai-300-series]
│ ├── modules/virtualization/docker-amd.nix
│ │
│ ├── specialisation.igpu.configuration:
│ │ ├── modules/profiles/gaming.nix
│ │ ├── modules/profiles/maker.nix
│ │ └── power.autoPowerProfile.strategy = "dynamic"
│ │
│ └── specialisation.dgpu.configuration:
│ ├── modules/profiles/gaming.nix
│ ├── [nixos-hardware: framework-16-amd-ai-300-series-nvidia]
│ ├── modules/hardware/nvidia-gpu.nix
│ ├── modules/virtualization/docker-nvidia.nix
│ ├── modules/virtualization/docker-amd.nix
│ ├── nixosConfig.gpu.nvidia.enable = true
│ └── power.autoPowerProfile.strategy = "dynamic"
│
└── [home-manager module from flake.nix]
└── home/nimmo.nix (user packages, shell, git, dotfiles)
All three tiers (base, igpu, dgpu) are built in a single nixos-rebuild switch --flake .#electra. They appear as separate boot menu entries.
Lena's import chain
flake.nix → makeNixosSystem (lena, isServer=false)
├── hosts/lena/default.nix
│ ├── modules/common/base.nix (same foundation)
│ ├── modules/desktop/environment.nix
│ ├── hosts/lena/hardware-configuration.nix
│ ├── hosts/lena/disko.nix
│ ├── modules/hardware/laptop.nix
│ ├── [nixos-hardware: 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
│
└── [home-manager module from flake.nix]
└── home/nimmo.nix
Differences from electra: disko declarative partitioning, Lenovo nixos-hardware module, no specialisations, tablet mode (iio sensors), two user accounts, no Docker, no Ollama.
Vega's import chain
flake.nix → makeNixosSystem (vega, isServer=true, pkgs=nixpkgs)
├── hosts/vega/default.nix
│ ├── modules/common/base.nix (same foundation)
│ ├── modules/server/base.nix (SSH, mosh, network tools)
│ ├── hosts/vega/hardware-configuration.nix
│ ├── hosts/vega/disko.nix
│ ├── modules/profiles/ai-desktop.nix
│ ├── modules/virtualization/docker.nix
│ ├── modules/services/atticd.nix
│ ├── modules/services/nixos-auto-update.nix
│ └── users/nimmo.nix
│
└── [home-manager module from flake.nix]
└── home/nimmo.nix (isServer=true — skips desktop packages)
Vega is headless: no desktop environment, no Plasma, no GUI tools. It imports modules/server/base.nix instead of modules/desktop/environment.nix. Its managed AI tooling includes Codex, OpenCode, Paseo, Hermes, and the configured MCP servers without a local desktop session.
Lyra's import chain
flake.nix → makeNixosSystem (lyra, isServer=true, pkgs=nixpkgs)
├── hosts/lyra/default.nix
│ ├── modules/common/base.nix (same foundation)
│ ├── modules/server/base.nix (SSH, mosh, network tools)
│ ├── hosts/lyra/hardware-configuration.nix
│ ├── hosts/lyra/disko.nix
│ ├── modules/virtualization/docker-nvidia.nix
│ ├── modules/profiles/ollama.nix
│ ├── modules/services/nixos-auto-update.nix
│ └── users/nimmo.nix
│
└── [home-manager module from flake.nix]
└── home/nimmo.nix (isServer=true — skips desktop packages)
Lyra is headless like vega, but is the GPU compute and media host. It runs the NVIDIA proprietary driver for an RTX 4070 eGPU (Thunderbolt-connected), Docker with NVIDIA CDI, Ollama on the dGPU, and a collection of Docker-based media and AI stacks in /srv/stacks.
The Four Layers
Think of the configuration as four concentric layers, from most shared to most specific:
Layer 1: Common (all hosts)
Location: modules/common/base.nix
Everything imported via base.nix applies to every host. It pulls in:
nix-settings.nix— Nix daemon config, binary caches, allowUnfreelocale.nix— en_GB, Europe/London, UK keyboarddefault-config.nix—nixosConfig.*portability options (user, email, paths)sops-host.nix— sops-nix host key setupgarbage-collection.nix— daily GC, 7-day age limitbtrfs-maintenance.nix— monthly btrfs scrubbeszel-agent.nix— system monitoring agent (all hosts)backrest.nix— automated backups (all hosts)- Bootloader (systemd-boot; NixOS activation manages the default boot entry)
- NetworkManager with captive portal detection disabled
If you want something on every machine, put it in modules/common/base.nix (or a module it imports).
Layer 2: Desktop vs. Server foundation
Location: modules/desktop/environment.nix or modules/server/base.nix
Desktop hosts (electra, lena) import modules/desktop/environment.nix, which adds:
modules/common/system.nix— base system packages (git, htop, jq, sops tools, etc.)modules/desktop/fonts.nixmodules/hardware/printing.nixmodules/hardware/removable-storage.nix- Firefox (via
programs.firefox.enable) - AMD GPU utilities
Server hosts (vega and lyra) import modules/server/base.nix instead, which adds:
modules/common/system.nix- OpenSSH (password auth disabled, no root login)
- mosh, network tools (dnsutils, netcat, cifs-utils)
Layer 3: Opt-in feature modules
Location: modules/
Self-contained features that hosts choose to import. Each module handles one concern. A host opts into a feature by adding it to its imports list:
boot: silent-boot desktop: plasma, apps, fonts hardware: bluetooth, printing, removable-storage, expansion-card-mount, laptop maintenance: garbage-collection, btrfs-maintenance networking: wifi-networks, wireguard-vpn power: auto-power-profile profiles: ai-agents, ai-desktop, gaming, maker, ollama server: base services: backrest, beszel-agent, btrbk-home, nixos-auto-update, ollama-common virtualization: docker, docker-amd, docker-nvidia
Layer 4: Host-specific config, users, and home-manager
Location: hosts/<hostname>/, users/, and home/
Each host's default.nix is where per-machine decisions are made: which modules to import, hardware configuration, and (for electra) the specialisation definitions.
- User accounts (
users/nimmo.nix,users/claire.nix) — account definitions (groups, shell only) - User environment (
home/nimmo.nix) — home-manager: packages, git config, shell, dotfiles, desktop entries
Home-manager is integrated via flake.nix (not via imports in host modules) and uses home.packages rather than users.users.nimmo.packages. The isServer flag allows home/nimmo.nix to skip desktop-only packages on server hosts. KDE Plasma settings are managed declaratively via home/nimmo.nix using the plasma-manager flake input.
How Options Merge Across Layers
When multiple modules set the same list option, the lists are concatenated. This is how packages from different files all end up installed.
For example, environment.systemPackages is set in:
modules/common/system.nix— btop, htop, git, jq, sops, etc.modules/desktop/apps.nix— Kate, LibreOffice, VLC, etc.modules/desktop/environment.nix— AMD GPU utilities
NixOS merges all these lists. The final system has all of these packages.
Design Decisions Worth Understanding
Why does electra use three tiers instead of separate configurations?
All three boot configurations run on the same physical Framework 16 and are nearly identical. The NixOS specialisation mechanism expresses this directly: one base + two specialisations, built together in a single nixos-rebuild. They are always in sync by construction.
The base (battery) tier exists because the GPU expansion bay must be powered off to swap — a reboot is always required. Battery mode makes the reboot-without-GPU case efficient.
Why is desktop/environment.nix separate from common/base.nix?
Server hosts inherit all of modules/common/base.nix but should not get fonts, printing, removable storage, or AMD GPU tools. Splitting those concerns into desktop/environment.nix lets desktop hosts import both while server hosts import only base.nix.
Why two nixpkgs channels?
Electra uses nixpkgs-unstable-small, a smaller nixpkgs branch that tracks nixpkgs-unstable but includes only packages with passing CI. This gives electra faster, more reliable binary cache hits. Lena, vega, and lyra use the default nixpkgs input, which is nixpkgs-unstable. The makeNixosSystem helper makes the channel selection explicit per host.
Why is common/base.nix a flat file instead of a directory?
The former hosts/common/ directory has been consolidated into modules/common/base.nix. This makes the import path explicit: every host that imports base.nix gets exactly what that file declares. The sub-modules (nix-settings.nix, locale.nix, etc.) are sibling files in modules/common/ rather than a subdirectory.