Table of Contents
- Chapter 1: Configuration Structure
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
│ │ ├── hardware.nix # AMD iGPU configuration (shared by base and igpu)
│ │ ├── hardware-dgpu.nix # NVIDIA/AMD Prime offload (dgpu specialisation only)
│ │ └── 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
│
├── 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.)
│ │ ├── environment.nix # Desktop foundation: system.nix + fonts + printing + removable-storage
│ │ ├── fonts.nix # Font packages and fontconfig defaults
│ │ └── goose.nix # Goose desktop app (deb packaging with patchelf workaround)
│ ├── 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 (3-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
│ ├── profiles/
│ │ ├── ai-agents.nix # Claude Code, Codex CLI, nixd LSP, Claude MCP plugin, credentials
│ │ ├── ai-desktop.nix # Imports ai-agents + opencode, desktop launchers, Ollama providers
│ │ ├── gaming.nix # Steam, GameMode, MangoHud, Discord
│ │ ├── 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)
│ │ ├── 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/
│ ├── secrets.yaml # 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; };
};
}
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 is a headless server (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/common/base.nix (foundation for all hosts)
│ │ ├── 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
│ │ └── [disko module from flake input]
│ │
│ ├── modules/desktop/environment.nix (desktop-only foundation)
│ │ ├── modules/common/system.nix (base system packages)
│ │ ├── modules/desktop/fonts.nix
│ │ ├── modules/hardware/printing.nix
│ │ └── modules/hardware/removable-storage.nix
│ │
│ ├── hosts/electra/hardware-configuration.nix
│ ├── modules/hardware/laptop.nix
│ ├── 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
│ ├── modules/services/nixos-auto-update.nix
│ ├── modules/profiles/ollama.nix
│ ├── modules/networking/wifi-networks.nix
│ ├── modules/networking/wireguard-vpn.nix
│ ├── modules/profiles/ai-desktop.nix
│ ├── modules/profiles/maker.nix
│ ├── modules/desktop/apps.nix
│ ├── users/nimmo.nix
│ ├── hosts/electra/hardware.nix
│ ├── [nixos-hardware: framework-16-amd-ai-300-series]
│ ├── modules/virtualization/docker-amd.nix
│ │
│ ├── specialisation.igpu.configuration:
│ │ ├── modules/profiles/gaming.nix
│ │ ├── modules/desktop/goose.nix
│ │ ├── modules/profiles/maker.nix
│ │ └── power.autoPowerProfile.strategy = "dynamic"
│ │
│ └── specialisation.dgpu.configuration:
│ ├── modules/profiles/gaming.nix
│ ├── modules/desktop/goose.nix
│ ├── [nixos-hardware: framework-16-amd-ai-300-series-nvidia]
│ ├── hosts/electra/hardware-dgpu.nix
│ ├── modules/virtualization/docker-nvidia.nix
│ ├── modules/virtualization/docker-amd.nix
│ ├── profiles.ollama.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-agents.nix
│ ├── modules/virtualization/docker.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. The AI agents profile provides Claude Code, Codex CLI, and the nixd LSP without desktop launchers.
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, 3-day age limitbtrfs-maintenance.nix— monthly btrfs scrubbeszel-agent.nix— system monitoring agent (all hosts)backrest.nix— automated backups (all hosts)- Bootloader (systemd-boot with
@saveddefault to remember last selection) - 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) 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, goose 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/plasma.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 and vega use unstable for the latest packages. Lena uses stable for predictability — a shared family laptop should not change unexpectedly. 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.