No description
  • Nix 56.5%
  • Shell 27.3%
  • Just 16.2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Nova Nix Builder 2ea68b2ea9 chore: Auto-update flake inputs
Validated-base: c1bb2c3eef
Validated-lock-sha256: 6f215caf722bed21cc66c5d91bc7b691eb592ecdbce4235981b8d498d4ba2169
Cached-paths-sha256: 8931aecd40ae4d5d2f77b2a32ca87d36b571af15d29e78dbe65d85b8b1f31256
2026-07-30 04:12:22 +01:00
.agents/skills/update-wiki chore: migrate repository guidance to Codex 2026-06-30 23:36:28 +01:00
.engram Replace Graphiti with Engram autosync 2026-07-03 06:16:56 +01:00
.forgejo fix: remove Nix check from AI review 2026-07-25 23:34:06 +01:00
home fix: restore shell history arrow search 2026-07-29 00:22:08 +01:00
hosts refactor: split scoped secrets 2026-07-29 21:23:21 +01:00
justfiles hardening: validate Nebula secret files 2026-07-29 22:03:48 +01:00
lib fix: include Cosmos in update policy 2026-07-28 23:57:23 +01:00
modules refactor: finish scoped secret migration 2026-07-29 21:23:54 +01:00
nebula feat: add Cosmos PostgreSQL VM 2026-07-28 23:53:10 +01:00
packages/nixos-auto-update perf: trim cache uploads and replace Mouzi 2026-07-21 19:11:58 +01:00
potential chore: remove obsolete Open WebUI and sbomnix pins 2026-07-18 19:59:54 +01:00
scripts feat: adopt treefmt formatting and checks 2026-07-28 08:59:11 +01:00
secrets chore: remove retired Mem0 secret 2026-07-29 22:08:00 +01:00
tests fix: include Cosmos in update policy 2026-07-28 23:57:23 +01:00
users feat: add Cosmos PostgreSQL VM 2026-07-28 23:53:10 +01:00
.gitignore Setting mem0 LLM to gemma4:e2b on lyra to process new memories 2026-07-02 16:27:29 +01:00
.sops.yaml chore: remove retired Mem0 secret 2026-07-29 22:08:00 +01:00
AGENTS.md docs: codify change and deployment workflow 2026-07-17 16:21:39 +01:00
flake.lock chore: Auto-update flake inputs 2026-07-30 04:12:22 +01:00
flake.nix feat: add Cosmos PostgreSQL VM 2026-07-28 23:53:10 +01:00
Justfile feat(nebula): add certs for nova and phone; add nebula just recipes 2026-06-23 22:25:03 +01:00
paseo.json chore: document comment-based PR review workflow 2026-07-26 18:39:31 +01:00
README.md refactor: finish scoped secret migration 2026-07-29 21:23:54 +01:00
treefmt.nix feat: adopt treefmt formatting and checks 2026-07-28 08:59:11 +01:00

NixOS Configuration

Multi-host NixOS configuration with modular structure.

Commands

Use just to manage your NixOS configuration. Run just or just --list to see all available recipes.

Common recipes:

  • just check — Validate flake syntax
  • just deploy — Deploy current host configuration (auto-detects specialisation)
  • just deploy igpu — Deploy and activate a specific specialisation
  • just deploy --host vega — Delegate a deploy to a remote host over SSH
  • just switch-spec igpu — Switch specialisation without rebuilding
  • just plasma-capture — Capture current KDE Plasma settings to home/users/nimmo/plasma.nix
  • just diff-last-update — Show package changes from the last update
  • just generations — List system generations (before rollback)
  • just gc — Run garbage collection
  • just firmware-update — Update firmware via LVFS

Timer management:

  • just backup-snapshots — List Backrest/restic backups for this host
  • just backup-snapshots-all — List Backrest/restic backups for all hosts
  • just backup-unlock — Remove stale restic locks
  • just backup-logs — Show Backrest service logs
  • just backup-status — Show Backrest service status
  • just btrfs-snapshots — List local and second-NVMe btrfs snapshots
  • just btrfs-snapshot-usage — Show per-snapshot btrfs space usage for both snapshot locations
  • just btrfs-restore SNAPSHOT PATH — Restore one path from btrfs into /tmp
  • just backup-ls — Browse files in a restic snapshot
  • just backup-restore-path — Restore one path from restic into /tmp
  • just backup-restore — Restore latest snapshot (disaster recovery)
  • just nixos-gc-last-run — Show last GC run details
  • just nixos-gc-run-now — Trigger GC immediately
  • just auto-update-last-run — Show last auto-update run details
  • just auto-update-run-now — Trigger auto-update immediately
  • just auto-update-hold "reason" — Block automatic updates during deployment testing
  • just auto-update-release — Release a deployment testing hold
  • just auto-update-hold-status — Show the active deployment hold
  • just auto-update-recover-stopped — Restart still-inactive services from the last auto-update stop list
  • just auto-update-recover-stopped-preview — Preview which services would be restarted

Directory Structure

nixos-config/
├── flake.nix                      # Flake definition (inputs & host configs)
├── flake.lock                     # Locked dependency versions
├── .sops.yaml                     # SOPS age key configuration for secrets
│
├── home/
│   ├── common/                    # Modules shared by multiple Home Manager users
│   │   └── README.md              # Shared-module ownership rules
│   ├── users/
│   │   └── nimmo/
│   │       ├── default.nix         # Nimmo Home Manager entry point
│   │       ├── git.nix             # Git identity and defaults
│   │       ├── shell.nix           # Bash, Bash-it, direnv, and shell helpers
│   │       ├── browser.nix         # Firefox profile and privacy settings
│   │       ├── desktop.nix         # Desktop tools, MIME, and XDG settings
│   │       ├── download-organizer.nix
│   │       └── plasma.nix          # Declarative KDE Plasma settings
│   └── nimmo.nix                  # Compatibility entry point
│
├── hosts/
│   ├── electra/                   # Framework 16 laptop
│   │   ├── default.nix            # Unified config: base=battery, specialisations=igpu,dgpu
│   │   ├── hardware.nix           # Host-specific hardware imports (base)
│   │   ├── hardware-dgpu.nix      # NVIDIA/AMD Prime offload (dgpu specialisation)
│   │   └── framework-16.nix       # AMD GPU, fingerprint, PipeWire, zram, Framework tools
│   │
│   ├── lena/                      # Lenovo Ideapad 5 2-in-1 (Gen 9)
│   │   ├── default.nix            # Tablet mode, fingerprint, touchscreen
│   │   ├── disko.nix              # Btrfs partitioning layout
│   │   └── hardware-configuration.nix  # Auto-generated hardware config
│   │
│   ├── vega/                      # Headless services/cache server
│   │   ├── default.nix            # Docker services, Attic, Paseo daemon
│   │   ├── disko.nix              # Btrfs partitioning layout
│   │   └── hardware-configuration.nix  # Auto-generated hardware config
│   │
│   ├── lyra/                      # GPU/media services server
│   │   ├── default.nix            # NVIDIA Docker, Ollama, media service ports
│   │   ├── disko.nix              # Btrfs partitioning layout
│   │   └── hardware-configuration.nix  # Host hardware config
│   │
│
├── modules/                       # Reusable feature modules
│   ├── common/
│   │   ├── base.nix               # Shared host baseline
│   │   ├── default-config.nix     # Portability options (user, email, paths)
│   │   ├── locale.nix             # UK locale, timezone, keyboard
│   │   ├── nix-settings.nix       # Nix daemon settings and binary caches
│   │   ├── system.nix             # Universal CLI/admin packages
│   │   └── sops-host.nix          # sops-nix key source (host SSH key)
│   ├── boot/
│   │   └── silent-boot.nix        # Plymouth splash, quiet kernel parameters
│   ├── desktop/
│   │   ├── environment.nix        # Desktop environment support modules/packages
│   │   ├── kde-plasma.nix         # KDE Plasma 6 desktop environment
│   │   ├── apps.nix               # Desktop applications (Kate, LibreOffice, VLC, etc.)
│   │   ├── fonts.nix              # Font packages and fontconfig defaults (desktop only)
│   ├── hardware/
│   │   ├── bluetooth.nix          # Bluetooth with experimental features
│   │   ├── laptop.nix             # Laptop utilities and optional endurance mode
│   │   ├── printing.nix           # CUPS with Brother printer drivers, Avahi discovery
│   │   ├── removable-storage.nix  # udisks2, NTFS support, polkit mount rules
│   │   └── expansion-card-mount.nix  # 1TB expansion card automount (electra only)
│   ├── maintenance/
│   │   ├── garbage-collection.nix # Daily cleanup (3-day age limit)
│   │   └── btrfs-maintenance.nix  # Monthly btrfs scrub (all hosts)
│   ├── networking/
│   │   └── wifi-networks.nix      # WiFi network definitions (home + other known networks)
│   ├── power/
│   │   └── auto-power-profile.nix # Automatic power profile switching
│   ├── profiles/
│   │   ├── gui.nix                # Composed graphical KDE Plasma environment
│   │   ├── laptop.nix             # GUI profile + laptop hardware/Wi-Fi/Bluetooth
│   │   ├── backup.nix             # Backrest backup capability for current host roles
│   │   ├── gaming.nix             # Steam, Gamemode, Discord, MangoHUD
│   │   ├── ai-agents.nix          # Codex CLI, nixd, pylsp, NixOS MCP
│   │   ├── ai-desktop.nix         # AI desktop/agent experience composition point
│   │   ├── ai-runtime.nix         # OpenCode service/client and Paseo runtime
│   │   ├── ollama.nix             # Composable AMD/NVIDIA Ollama loadouts
│   │   └── maker.nix              # Cura, OpenSCAD (3D printing/CAD)
│   ├── server/
│   │   └── base.nix               # Server baseline: SSH and admin/network tools
│   ├── services/
│   │   ├── nixos-auto-update.nix  # Scheduled auto-update with rollback
│   │   ├── btrbk-home.nix         # Hourly btrfs snapshots of /home (electra)
│   │   ├── ollama-common.nix      # Shared ollama user/group definition
│   │   ├── backrest.nix           # Backrest/restic backups (sops-nix secrets)
│   │   └── forgejo-mcp.nix        # Vega-hosted Forgejo MCP service for issues/wiki
│   └── virtualization/
│       ├── docker.nix             # Base Docker with Btrfs
│       ├── docker-amd.nix         # Docker with AMD GPU (ROCm) support
│       └── docker-nvidia.nix      # Docker with NVIDIA Container Toolkit
│
├── packages/
│   └── nixos-auto-update/
│       └── default.nix             # Updater package and hold controller
│
├── users/
│   ├── nimmo.nix                  # NixOS admin user account definition
│   └── claire.nix                 # NixOS standard user (lena only, no sudo)
│
├── secrets/
│   ├── secrets/                   # Scoped encrypted service/host secrets
│   └── README.md                  # Secrets management documentation
│

How It All Fits Together

Understanding the import chain is key to knowing where to make changes. Each host pulls in only what it needs:

flake.nix
  └── hosts/<name>/default.nix             (host composition root)
        ├── hosts/<name>/*.nix              (hardware facts and host values)
        ├── modules/common/base.nix         (shared baseline, where applicable)
        ├── modules/profiles/*.nix          (composed workload/environment bundles)
        ├── modules/<domain>/*.nix          (individual capabilities and policies)
        ├── users/*.nix                     (NixOS account definitions)
        └── specialisation.<mode>.configuration
                                             (hardware-presence-specific additions)

This means: if you want to change something for all hosts, first check whether it is genuinely universal before placing it in modules/common/. If you want to change a single capability, edit the relevant modules/<domain>/ module. If you want to compose a usable workload or environment, use a profile. If you want to change something for one host, edit that host's default.nix; hardware facts belong beside it. Hardware-presence-specific changes belong under the relevant Electra specialisation.

Architecture Rules and Boundary Decisions

Use these rules when deciding where new configuration belongs:

  • Modules provide one coherent capability or policy. They may expose options and import lower-level capabilities, but should not require a particular host's hardware facts.
  • Profiles compose related capabilities into a usable environment or workload. A profile is a composition point, not a reason to move unrelated implementation details into one file.
  • Hosts are composition roots. They select modules and profiles, provide hardware facts and host-specific values, and own specialisation-only configuration.
  • NixOS user modules under users/ define system accounts, groups, and privileges. Home Manager modules under home/ define per-user user-environment configuration; they should not own host hardware or system service policy.
  • Helpers and packages remain implementation details unless they need a user-facing NixOS option or are independently consumed. A helper imported by a module is not automatically a reusable module.

The current boundary cases are intentional or tracked for focused follow-up:

  • modules/common/base.nix contains universal host defaults, fleet monitoring, and the service-failure option provider. Backups are composed explicitly by modules/profiles/backup.nix; optional Forgejo MCP and Engram Cloud client support is composed by modules/profiles/ai-agents.nix; Vega-only services are imported by hosts/vega/default.nix.
  • modules/desktop/environment.nix is a desktop-support composition module covering audio, fonts, printing, removable storage, Firefox, and common desktop utilities. It remains under modules/desktop/ because it composes desktop-support capabilities rather than representing a complete user-facing environment; modules/profiles/gui.nix is the higher-level composition point.
  • modules/profiles/gui.nix and modules/profiles/laptop.nix are profiles that compose the desktop and laptop experience. Their system user-account imports remain separate from the per-user Home Manager layout under home/users/.
  • modules/profiles/ai-desktop.nix is the AI experience composition point; modules/profiles/ai-agents.nix owns shared AI client integration and modules/profiles/ai-runtime.nix owns the coupled OpenCode/Paseo runtime, credentials, and Home Manager wiring. Keep host-specific provider overrides at the composition point.
  • modules/profiles/ollama.nix is a workload profile whose GPU-specific service implementation is intentionally kept together with its AMD/NVIDIA loadout. It should not be split merely because it contains both composition and service details.
  • modules/common/*-launcher.nix, package expressions, and generated wrapper definitions are helpers or packages, not new module categories.

Do not introduce Dendritic or flake-parts as part of ordinary boundary cleanup. Prefer a small extraction only when it creates an independently understandable, reusable, or testable capability; otherwise document the intentional boundary.

Home-Manager Integration

Home-manager manages user-level configuration declaratively across all hosts. It's integrated directly into the NixOS configuration (not standalone).

What's Managed by Home-Manager

User Packages (via capability profiles and home-manager):

  • AI tools (Codex CLI, nixd, pylsp, NixOS MCP) — via modules/profiles/ai-agents.nix
  • Gaming packages (Discord, MangoHUD) — via modules/profiles/gaming.nix on electra
  • 3D printing/CAD (Cura, OpenSCAD) — via modules/profiles/maker.nix
  • Desktop apps (Element, Trilium, etc.) — via modules/desktop/apps.nix
  • BookStack shared wiki on vega — via modules/services/bookstack.nix
  • Conditional packages (only on unstable channel): opencode-desktop, whosthere

Shell Environment (programs.bash):

  • Bash-it framework with rjorgenson theme
  • Custom nixos-rebuild-auto function (rebuilds and reactivates current specialisation)
  • History search with up/down arrows
  • Shell initialization

Development Tools:

  • Git configuration (unified email across hosts)
  • Direnv with nix-direnv integration

System Monitoring:

  • btop with ROCm/libdrm wrapper for GPU monitoring
  • Shows gpu0 and gpu1 slots; btop silently omits any GPU it cannot detect

Firefox (programs.firefox):

  • Declarative default profile with privacy/security hardening
  • Extensions from NUR: Bitwarden, Consent-O-Matic
  • Full telemetry/tracking/study opt-out, HTTPS-only mode, Pocket disabled

Desktop Optimization:

  • Autostart applications with delays (Nextcloud delayed 15s)
  • Disabled applications (Discover update notifier)
  • Faster login performance

Configuration Files

  • home/users/<user>/default.nix - Per-user Home Manager entry point
  • home/users/nimmo/ - Nimmo's focused Home Manager modules
  • home/common/ - Reserved for modules genuinely shared by multiple users
  • ~/.bash_it - Symlinked from Nix store (managed by bash-it flake input)
  • Firefox extensions - Declarative via NUR (inputs.nur); manually-managed extensions: Floccus, Karakeep

KDE Plasma Configuration (plasma-manager)

KDE Plasma settings are declaratively managed via home/users/nimmo/plasma.nix using the plasma-manager flake input. This file is generated by capturing the current Plasma state:

just plasma-capture

This captures themes, keybindings, window behaviour, panel configuration, and other Plasma settings into a Nix file that is applied on rebuild. To make Plasma changes:

  1. Adjust settings via the KDE System Settings GUI
  2. Run just plasma-capture to capture the changes
  3. Review the diff, rebuild, commit

What's NOT Managed by Home-Manager

  • Application-specific configurations (managed via GUI)
  • Runtime user data and state

Updating User Configuration

Edit the relevant module under home/users/nimmo/ and rebuild. For example:

# Edit configuration
vim home/users/nimmo/shell.nix

# Apply changes
nixos-rebuild-auto

Changes to home-manager configuration take effect immediately on rebuild - no reboot required.

Common Tasks

Check Configuration Syntax

Validate the flake without building. Note: New untracked files must be staged with git add first, as Nix flakes only see files tracked by git.

git add <new-files>  # If you created new files
nix flake check

Format Configuration

Use the flake-native formatter for Nix and shell files. It is also included in nix flake check, so the same formatting rules run locally and in Forgejo CI.

nix fmt       # Format the repository
nix fmt -- --ci  # Check formatting without changing files

Change Lifecycle

Use this default for cross-cutting, service, workflow, security, and multi-host changes:

  1. Track the work in a Forgejo issue and create a dedicated feature branch.
  2. Run the validation that matches the change: lint/regression tests for scripts and workflows; nix flake check for Nix configuration; both when a change spans them.
  3. Open a merge request that references the issue and shares its milestone. The required Forgejo checks must pass before merge.
  4. For deployment-affecting work, use a canary appropriate to the risk. Test the documented manual command as well as the systemd service where both are public interfaces.
  5. For fleet/auto-update work, record a genuine deployment and an immediate no-op follow-up before closing the issue. Use a closing keyword in the MR only when this evidence is already complete; otherwise close the issue with the MR and deployment evidence afterwards.

Small, bounded changes with limited blast radius—such as adding one application or enabling/disabling one module—can use a direct commit to main after a targeted just test on an affected host. Documentation-only changes need only focused review.

Test Configuration

Build and activate without adding to boot menu (reverts on reboot). The recipe preserves the host's active specialisation when one exists:

just test
# Or explicitly: just test igpu

Avoid raw nixos-rebuild test on hosts with specialisations: it activates the base configuration rather than the running specialisation.

Deploy Configuration

Build, activate, and add to boot menu:

# Auto-detect hostname and reactivate current specialisation (recommended)
nixos-rebuild-auto

# Preferred repository-aware deployment command (also auto-detects the host)
just deploy

# Or specify explicitly
sudo nixos-rebuild switch --flake .#electra
sudo nixos-rebuild switch --flake .#lena

The nixos-rebuild-auto command runs nixos-rebuild switch using the current hostname as the flake target, then reactivates the current specialisation (if any). All three boot entries (base, igpu, dgpu) are always built together in a single rebuild. It is defined as a bash function in home/users/nimmo/shell.nix.

Prefer just deploy or nixos-rebuild-auto when working locally. A flake selector such as .#lena always selects Lena's configuration even when the command is run on another host; NixOS does not prevent cross-host activation. Before any explicit .#<host> deployment, compare <host> with hostname.

Switch Hardware Profile (Electra Specialisations)

Select the boot entry that matches the Framework 16 hardware currently installed, or switch profiles from the running system after changing hardware. Activation controls the default boot entry, so the last activated Electra profile boots into its latest generation.

Electra's igpu and dgpu specialisations are hardware-presence profiles, not workload preferences. The Framework 16 can physically swap the NVIDIA expansion bay in or out:

  • Use dgpu when the NVIDIA expansion bay is installed.

  • Use igpu when the NVIDIA expansion bay is not installed.

  • Do not choose dgpu just because you want more GPU performance if the discrete GPU is absent; the required hardware is not present.

  • Base entry (electra-battery): AMD iGPU, vanilla kernel, locked power-saver, no gaming/maker profiles — default mode

  • Specialisation entry (electra-igpu): AMD iGPU only, stock kernel, dynamic power profile, gaming/maker profiles

  • Specialisation entry (electra-dgpu): NVIDIA + AMD hybrid, same kernel as base, Docker

Update All Packages

Forgejo Actions updates flake inputs and fills the Attic cache at 03:00 and 15:00 UTC. Every Attic run first passes updater lint, shell regressions, and nix flake check (including the isolated NixOS VM state-machine test); closure builds do not start when that gate fails, and the failure is reported through ntfy. Pull requests run the same required checks without starting an Attic build. NixOS hosts check for pull-only updates every 2 hours with a randomised delay so they consume tested lock updates rather than producing them locally. Laptops below 20% battery while discharging warn that an update is available and defer until plugged in. To trigger a host pull-only update manually:

just auto-update-run-now       # Normal run (respects 6h freshness window)
sudo nixos-auto-update --force       # Force update regardless of lock age

To run the Forgejo input-update job inside the six-hour freshness window, manually dispatch Build and Push Attic Cache with both Update flake inputs before building and Bypass the 6-hour freshness check set to true. The override applies only to that manual input-update run; scheduled runs continue to respect the freshness window.

When testing a branch configuration on a remote host, hold its automatic updates first so the pull-only timer cannot replace the test activation with main:

just auto-update-hold "testing feat/vega-change"
# Run `just test` and validate the host.
just auto-update-release

Hold creation shares the updater run lock and waits for any detached activation helper to finish before writing the hold. It therefore cannot report a safe maintenance window while a built system is still being switched.

Hold creation waits for an update already in progress. While held, every scheduled or forced update exits without pulling, building, or activating and posts an ntfy reminder. Holds require explicit release and are cleared by reboot because their state is stored under /run.

All hosts detach the activation switch into a transient helper unit after a successful build. This avoids the updater aborting its own switch when activation replaces the updater unit or temporarily stops services such as Nebula, NetworkManager, or Newt. While that detached helper is active, later timer runs skip cleanly instead of colliding on the switch lock.

To see what changed after an update:

just diff-last-update

Run the updater regression suite without activating a configuration:

just test-auto-update

nix flake check additionally runs an isolated NixOS VM state-machine test. It covers a legacy generation with no recorded revision, one successful detached activation followed by a no-build convergence run, detached switch and helper-startup failures, and the emergency producer against a local bare Git remote. Nova has KVM access and runs this check without requiring a persistent test VM.

Adding Packages

There are three places to add packages, depending on scope:

System Packages (available to all users, all hosts)

Edit modules/common/system.nix. These are available system-wide via environment.systemPackages:

environment.systemPackages = with pkgs; [
  # Add your package here
  neovim
];

User Packages (nimmo, all hosts)

Edit the relevant module under home/users/nimmo/. These are managed by Home Manager and available to nimmo on every host:

home.packages = with pkgs; [
  # Add your package here
  spotify
];

Capability Profiles

modules/profiles/ bundles related system and user-level config into a single import:

Profile What it enables Enable option
gui.nix Common base + graphical KDE Plasma environment Import to enable
laptop.nix GUI profile + laptop hardware, Bluetooth, Wi-Fi Import to enable
gaming.nix Steam, Gamemode, Discord, MangoHUD Import to enable
ai-agents.nix Codex CLI, nixd, pylsp, NixOS/Forgejo/Engram MCP clients, shared memory policy Import to enable
ai-desktop.nix AI desktop experience composition point Import to enable
ai-runtime.nix OpenCode service/client and Paseo runtime Imported by ai-desktop.nix
maker.nix Cura, OpenSCAD (3D printing/CAD) Import to enable

Add a profile to a host by importing it in that host's default.nix:

imports = [
  # ...
  ../../modules/profiles/gaming.nix
];

For packages that don't fit an existing profile, add them directly to that host's default.nix (e.g. hosts/electra/default.nix or hosts/lena/default.nix).

Desktop Applications (all desktop hosts)

Edit modules/desktop/apps.nix. These are system-wide packages for desktop/laptop use (Kate, LibreOffice, VLC, Okular, Nextcloud, Bitwarden, Trilium, etc.). Both electra and lena import this module.

Finding Package Names

Search for packages:

nix search nixpkgs firefox
nix search nixpkgs --json firefox | jq  # Detailed output

Or browse https://search.nixos.org/packages

Adding External Flakes

1. Add the input to flake.nix

inputs = {
  # ... existing inputs ...

  # New flake
  some-tool.url = "github:owner/repo";
  some-tool.inputs.nixpkgs.follows = "nixpkgs";  # Use our nixpkgs
};

2. Pass it through specialArgs

Already configured - all inputs are passed via specialArgs = { inherit inputs; }.

3. Use the package

In any module that has inputs in its arguments:

{ pkgs, inputs, ... }:

{
  environment.systemPackages = [
    inputs.some-tool.packages.${pkgs.stdenv.hostPlatform.system}.default
  ];
}

4. Update the lock file

nix flake update some-tool

Adding a New Host

1. Create host directory

mkdir -p hosts/newhostname

2. Generate hardware config on the new machine

nixos-generate-config --show-hardware-config > hosts/newhostname/hardware-configuration.nix

3. Create host default.nix

{ config, pkgs, inputs, ... }:

{
  imports = [
    ../../modules/common/base.nix
    ./hardware-configuration.nix
    # Add modules as needed
    ../../modules/boot/silent-boot.nix
    ../../modules/desktop/environment.nix
    ../../modules/desktop/kde-plasma.nix
    ../../modules/desktop/apps.nix
    ../../users/nimmo.nix
  ];

  networking.hostName = "newhostname";
  nixosConfig.host = {
    role = "server"; # or "laptop", "desktop", "workstation"
    purpose = "Short description of what this host is for.";
    location = "desk"; # use null or omit for mobile hosts
    tags = [ "server" "intelcpu" "docker" ];
  };

  system.stateVersion = "25.11";  # Set to your NixOS version
}

4. Add to flake.nix

nixosConfigurations = {
  electra = nixpkgs.lib.nixosSystem { ... };

  newhostname = nixpkgs.lib.nixosSystem {
    specialArgs = { inherit inputs; };
    modules = [ ./hosts/newhostname ];
  };
};

5. Deploy

# First deployment (explicit)
sudo nixos-rebuild switch --flake .#newhostname

# Subsequent deployments can use
nixos-rebuild-auto

Provisioning with nixos-anywhere

For a fresh machine, use nixos-anywhere to install NixOS and activate the configuration in one step. nixos-anywhere copies a provisioning age key onto the target before installation so sops-nix can decrypt secrets during first activation — before the SSH host key exists.

One-time setup (do this once, before provisioning any machine):

just setup-provision-key

This generates an age keypair, prints the private key once, registers the public key in .sops.yaml, and re-encrypts secrets. Save the private key to your password manager — it will not be shown again.

Per-machine provisioning steps:

  1. Scaffold the host and write its configuration (steps 14 in Adding a New Host above), including a disko.nix for disk layout — nixos-anywhere requires disko to partition the disk.

  2. Validate and commit the configuration before provisioning:

    just check
    git add -p && git commit -m "feat: Add HOSTNAME host configuration"
    git push
    
  3. Boot the target machine into a NixOS installer or rescue environment with SSH access, then provision:

    just provision HOSTNAME TARGET   # e.g. just provision nova 192.168.1.50
    

    When prompted, paste the provisioning age private key from your password manager. nixos-anywhere will partition the disk, install NixOS, and reboot the machine.

  4. After the machine reboots, register its SSH host key so it can decrypt secrets directly without the provisioning key:

    just add-secret-remote HOSTNAME TARGET
    
  5. Optionally remove the provisioning key from the machine (it lives under /root so is not user-accessible, but removing it reduces key material on disk):

    ssh root@TARGET 'rm /root/.config/sops/age/keys.txt'
    

Adding New Modules

Create a new file in modules/category/:

# modules/services/tailscale.nix
{ config, pkgs, ... }:

{
  services.tailscale.enable = true;
  environment.systemPackages = [ pkgs.tailscale ];
}

Then import it in the appropriate host config:

  • modules/common/base.nix - for all hosts
  • hosts/electra/default.nix - for electra (both modes)
  • hosts/electra/default.nix (under specialisation.dgpu.configuration) or hosts/lena/default.nix - for a single mode/host
imports = [
  # ...
  ../../modules/services/tailscale.nix
];

Rollback

Boot into previous generation

Select from boot menu at startup (systemd-boot shows all available generations).

Rollback from command line

# List generations
sudo nix-env --list-generations --profile /nix/var/nix/profiles/system

# Switch to specific generation
sudo nix-env --switch-generation 42 --profile /nix/var/nix/profiles/system
sudo /nix/var/nix/profiles/system/bin/switch-to-configuration switch

Garbage Collection

Automatic daily cleanup is configured in modules/maintenance/garbage-collection.nix:

  • Deletes generations older than 3 days
  • Runs daily with a randomized 1-hour delay to avoid contention

Additionally, nix-settings.nix configures automatic store GC to maintain 5-10GB free disk space.

Manual cleanup:

# Remove old generations (keeps last 7 days)
sudo nix-collect-garbage --delete-older-than 7d

# Remove ALL old generations (keep only current)
sudo nix-collect-garbage -d

Automated System Services

NixOS Auto-Update (All Hosts)

The updater commands and their deterministic runtime dependencies are packaged in packages/nixos-auto-update; modules/services/nixos-auto-update.nix is the NixOS integration layer for policy, systemd, host-specific nixos-rebuild, and notifications. Auto-update behavior defaults from nixosConfig.autoUpdate.role: producer hosts update and push at 03:00, consumer hosts pull and rebuild every 2 hours, and all timers keep the existing 15-minute randomised delay. Forgejo Actions remains the flake.lock producer and pushes accepted input updates after the Attic cache build succeeds.

Consumer mode:

  1. Skips and sends an ntfy reminder if a deployment hold is active
  2. Skips if the repository has staged, unstaged, or untracked changes (work in progress detected)
  3. Fetches origin/main, accepts only an explicit fast-forward, and refuses local-ahead or diverged checkouts rather than creating an automatic merge
  4. On laptops below 20% battery while discharging, warns that an update is available and defers until a later check
  5. Validates the configuration with a dry-run build
  6. Builds the exact-revision Git flake as the repository owner, while retaining root only for activation
  7. Hands activation to a detached transient helper and verifies that the resulting configurationRevision matches the intended Git commit, so replacing the updater unit during activation cannot abort its own switch
  8. Notifications at key points and on success/failure; warns separately if a reboot is needed for kernel changes

Notifications:

  • Hosts with KDE Plasma (electra, lena) show Plasma notifications for status, success, and failure as before. Success messages now include the filtered nvd diff --selected package changes (Nix store paths stripped).
  • Headless hosts (vega and future servers) post a single ntfy notification on success that includes the same filtered nvd diff, mirroring the Forgejo Attic cache completion message.
  • Failure notifications go to ntfy on every host (in addition to Plasma where applicable) so they are visible even when the laptop is away or closed. The notification body includes the captured tail of the failing command (build error, git output, etc.) so the cause is visible without opening a journal.
  • Deployment holds send a default-priority ntfy status on every blocked run (so headless hosts are never silently stuck), including the hold metadata and the just auto-update-release command. Holds deliberately avoid the high-priority routing used for real failures.
  • Routine no-op runs (no updates, repository dirty, flake.lock fresh, suspension reminders) stay on Plasma only to avoid ntfy spam.
  • The ntfy bearer token comes from the ntfy-token SOPS secret in secrets/ntfy.yaml and must be present before deploying with ntfy notifications enabled. URL and topic are set via nixosConfig.ntfy.url (https://ntfy.nimmog.uk) and nixosConfig.ntfy.topic (nixos-builds).
  • Routing is controlled per-host via services.nixos-auto-update.notifications.{plasma, ntfy.onSuccess, ntfy.onFailure}. Defaults follow Plasma presence: Plasma hosts get Plasma success + ntfy failure, servers get ntfy for both.

The flake.lock is updated either manually or automatically by the Forgejo Attic cache workflow. The auditable input-to-consumer policy lives in lib/auto-update-policy.nix and is exported as lib.autoUpdatePolicy by the flake. Current validation groups are:

  • Shared foundation (nixpkgs, Home Manager, Plasma Manager), system modules, AI tooling, and Bash-it validate the main fleet. The stable package and Home Manager inputs validate Cosmos.
  • Framework tooling and Lanzaboote validate Electra.
  • nixos-hardware validates Electra and Lena.
  • Desktop application inputs validate Electra and Lena.
  • nixpkgs-sbomnix-176 is deliberately pinned to the revision providing sbomnix 1.7.6 and is not automatically updated.
  • nixpkgs-ollama-cuda is deliberately pinned to the last known-good revision for ollama-cuda until unstable regains a working CUDA toolkit path.

When adding or removing a root flake input, update this policy in the same commit. The required auto-update regression gate compares the policy with the actual flake input and host inventories and fails with the unclassified or stale name, preventing silent policy drift and blocking the dependent Attic build.

Producer mode applies all changed policy channels to one candidate lock, then builds each affected host once against that final candidate. A failed candidate restores the original lock and is never published. The required checks set must also evaluate and build successfully. Run summaries record group outcomes, build/cache work, and phase timings. The producer records the exact base commit, resulting lock hash, and built-closure manifest hash. Publication is allowed only while origin/main still equals that validated base, and uses a lease-aware atomic push instead of rebasing the lock commit onto concurrent changes. If main moves, the run aborts and a later run re-evaluates the new tree. The emergency local producer follows the same rule; a fetch or push failure leaves its local lock commit for inspection and deliberately aborts activation rather than deploying a revision that consumers cannot fetch.

Monitor the service:

journalctl -u nixos-auto-update.service -f

Trigger or inspect manually:

just auto-update-run-now   # Trigger immediately (normal run)
just auto-update-last-run  # Show last run status
just auto-update-hold "reason" # Hold updates during remote testing
just auto-update-hold-status   # Show hold metadata
just auto-update-release       # Resume updates
just auto-update-recover-stopped-preview [host] # Show inactive stopped services from the last auto-update run
just auto-update-recover-stopped [host]   # Restart those services locally or over SSH
sudo nixos-auto-update --force   # Force an immediate update check
just diff-last-update            # Show package changes from the last update

If a remote-host switch is interrupted after systemd has already stopped connectivity-related services, the recovery recipes derive the expected units from the most recent stopping the following units: journal entry and only restart those that are still inactive.

Backrest Backup

Defined in modules/services/backrest.nix. Backrest manages the backup schedule and UI; restic handles the repository. The default plan backs up /home/nimmo, with host-specific extra paths via nixosConfig.backrest.extraPaths. Vega also backs up /srv/services/paseo and /srv/stacks.

  • Backups run at most every 4 hours when the NAS restic REST server is reachable
  • Credentials are managed via sops-nix and loaded from /run/secrets/
  • Backrest listens on localhost by default
  • Vega exposes Backrest on 0.0.0.0:9898 for LAN configuration
  • Firewall exposure is guarded so only server and desktop hosts can enable it

Monitor backups:

just backup-status
just backup-logs

Forgejo MCP

Defined in modules/services/forgejo-mcp.nix. Vega hosts a shared gitea-mcp-server instance for Forgejo issue and wiki automation. It is reachable only over Nebula at http://10.10.0.2:8780/mcp, and clients inherit that URL via the repo-managed Codex and OpenCode MCP configuration in modules/profiles/ai-agents.nix and modules/profiles/ai-runtime.nix.

  • Auth to https://git.nimmog.uk is centralized on vega via the forgejo/mcp/token SOPS secret
  • Desktop and daemon-launched agents connect to the internal MCP URL and do not carry the PAT
  • The service is intentionally limited to issues and wiki pages; Forgejo Projects board membership is not exposed through the automation path used here
  • Writes appear as the dedicated Forgejo bot account configured by nixosConfig.forgejoMcp.botUser

Trilium MCP

Vega hosts the shared Trilium web UI and built-in MCP endpoint over Nebula at http://10.10.0.2:37840/mcp.

Codex and OpenCode receive the same remote MCP entry through the AI profiles. The shared ETAPI token from secrets/trilium.yaml is exposed to AI client hosts and used as the bearer token for the built-in Trilium MCP transport. Each current AI host sets nixosConfig.trilium.clientUrl explicitly to Vega's Nebula endpoint rather than inheriting a shared hardcoded default. Nebula reachability still gates access to the endpoint itself.

Trilium Web Clients

Vega runs services.trilium-server from the pinned inputs.trilium-next server package as the shared Nebula-only Trilium instance for AI tooling.

  • The service binds directly to Vega's Nebula overlay IP on port 37840, so it is reachable only from the mesh
  • The NixOS module seeds TRILIUM_SYNC_SYNCSERVERHOST=https://notes.nimmog.uk, so Vega syncs against the main notes instance
  • The package comes from the repo-pinned TriliumNext flake input, so the v0.103+/v0.104 AI/LLM UI is present without a separate repo-side feature flag
  • Optional first-run bootstrap is available via nixosConfig.triliumWeb.syncBootstrapPasswordSopsKey. Point it at a SOPS entry such as trilium/sync-password and Vega will call Trilium's /api/setup/sync-from-server once for an uninitialized local database, then stop touching credentials after Trilium stores its own sync secret
  • The AI / LLM toggle itself still appears to be stored in Trilium application state rather than the server config file, so enabling it remains a one-time manual action in the Trilium UI after deployment

Paseo Forgejo Pull Requests

Paseo uses the tea CLI directly for Forgejo pull-request status, creation, and merge operations; it does not use the Vega-hosted Forgejo MCP service. modules/profiles/ai-runtime.nix installs Tea and renders its login at ~/.config/tea/config.yml from forgejo/paseo/token in secrets/git.yaml. Create this as a separate, repository-restricted personal access token with write:repository and read:issue; add write:issue if the account should also write PR comments. The rendered configuration is mode 0600 and is owned by the primary user on each host that runs Paseo.

Nebula Mobile Full Tunnel

Vega is configured as an IPv4-only Nebula exit node for mobile clients. The repo-managed workflow is:

  • Reissue Vega's cert with the split default-route unsafe networks: just nebula-reissue vega 2 "0.0.0.0/1,128.0.0.0/1"
  • Generate a full-tunnel mobile bundle: just nebula-bundle <client-name> mobile-full-tunnel
  • In the Mobile Nebula app, set DNS resolvers to 10.10.0.2 using the generated mobile-full-tunnel.txt note in the bundle directory

The bundle keeps ordinary Nebula defaults unless mobile-full-tunnel is selected, so non-phone and non-exit-node clients are unchanged.

Pangolin MCP

When secrets/pangolin.yaml exists, Codex and OpenCode also gain a local pangolin MCP entry backed by a pinned mcp-pangolin Rust package. The launcher reads pangolin/mcp/url and pangolin/mcp/token from that SOPS file, derives the API base URL, fetches the instance OpenAPI document at startup, normalizes a few invalid nullable string enums that Pangolin currently emits, optionally rewrites /org/{orgId}/... paths to a fixed org ID, and then starts the stdio MCP server locally on the client.

  • The integration stays dormant until secrets/pangolin.yaml is present, so hosts without Pangolin credentials are unaffected
  • The default mode is read-only (nixosConfig.pangolinMcp.readOnly = true), which limits the generated toolset to GET operations
  • Org-scoped tools default to nixosConfig.pangolinMcp.organizationId = "main-self-hosted", so the MCP client does not need a root-scoped key just to supply orgId repeatedly
  • The launcher accepts either the Pangolin instance root URL or an explicit /v1 API URL in pangolin/mcp/url
  • OpenAPI discovery currently tries the common Pangolin/Swagger paths derived from that base URL

BookStack

Vega can host BookStack using the native NixOS module through modules/services/bookstack.nix.

  • The service uses the nixpkgs-managed services.bookstack stack instead of a custom Docker deployment
  • On vega, nginx binds to 127.0.0.1:80 and is intended to sit behind Pangolin/newt from the start, with https://wiki.nimmog.uk as the public URL
  • The BookStack MCP client can use a separate internal base URL so API-token traffic does not go through Pangolin SSO; on vega this is http://127.0.0.1
  • Write tools in the community bookstack-mcp server are disabled by default; enable them with nixosConfig.bookstack.mcpEnableWrite = true; when you want Codex/OpenCode to create or update BookStack content
  • Persistent BookStack data lives under /mnt/storage/bookstack
  • Runtime app secrets, OIDC client credentials, and BookStack API token credentials live in secrets/bookstack.yaml
  • Community MCP integration is provided separately from BookStack itself using a packaged bookstack-mcp wrapper and BookStack API credentials
  • The MCP credentials live in secrets/bookstack.yaml under bookstack/api-token-id and bookstack/api-token-secret

Karakeep MCP

AI clients receive the official Karakeep MCP server through a local launcher when secrets/karakeep.yaml is present. The launcher runs ghcr.io/karakeep-app/karakeep-mcp:0.32.0 with the API URL and key loaded from SOPS, so the key is not written into Codex or OpenCode configuration. The launcher requires the Docker daemon and a primary user with permission to access it; this matches the Docker-backed AI hosts where the profile is currently enabled. The official MCP process is an API client and does not need project-directory or cache volume mounts; it communicates with Karakeep through its configured URL and standard input/output.

The Karakeep instance is hosted on media1 and is reached at https://links.nimmog.uk. Project workflows use ordinary Karakeep tags as inboxes: for example, an agent can search for nixos-config, review the results, and attach nixos-config-seen after actioning a bookmark. Other projects can use the same pattern with their own tag pair. Smart lists in Karakeep can provide the corresponding seen/unseen views.

Engram Agent Memory

Codex and OpenCode use a pinned local engram binary and launch engram mcp --tools=agent over stdio on each client. There is no shared server or per-client API wrapper in the MCP path itself.

Vega also hosts the self-run Engram Cloud replication service backed by local PostgreSQL. AI client hosts run a per-user engram serve daemon with ENGRAM_CLOUD_AUTOSYNC=1, so local SQLite remains authoritative on each host while cloud sync replicates the nixos-config project automatically. Mesh hosts use Vega's Nebula address; lena currently uses Vega's LAN address because it is not on Nebula.

The shared modules/profiles/memory-policy.md is installed as the global AGENTS.md for both clients. It directs agents to retrieve relevant context at the start of substantive work, record only durable confirmed information, avoid duplicates and secrets, and treat retrieved memories as potentially stale. Writes are tagged with the client source and host; agents add repository and category metadata when applicable. Repository-scoped work should use the Engram project pinned in .engram/config.json; this repo sets that to nixos-config so project detection stays deterministic.

Project Tracking In Forgejo

Planned and in-progress work lives in Forgejo. Durable operational knowledge lives in the repository wiki.

Recommended structure:

  • Create one milestone per project or workstream, e.g. Vega SillyTavern Cutover.
  • Create one issue per phase or deliverable rather than one giant project issue.
  • Use prefixed labels so automation can group issues reliably, e.g. host:vega, service:sillytavern, phase:prep, phase:cutover, phase:validation.
  • Put the operative checklist or runbook into the issue body so the issue is executable on its own.

When an issue produces a reusable procedure, move that procedure into the wiki before closing the issue. Keep the issue focused on work state and acceptance criteria; keep the wiki focused on the resulting operating knowledge.

Restore Process

Electra has three recovery tiers for /home data:

  1. Main-drive btrfs snapshots at /home/.btrbk-snapshots
  2. Replicated btrfs snapshots on the second NVMe at /run/media/snapshots
  3. Network Backrest/restic backups of /home/nimmo

Use the closest good copy first. For accidental deletion or a bad edit, start with btrfs snapshots because they are local and fast:

just btrfs-snapshots
just btrfs-snapshot-usage
just btrfs-restore home.20260429T1218 /home/nimmo/Documents/example.txt

just btrfs-snapshot-usage is the quickest way to see whether the replicated snapshots on /run/media/snapshots are genuinely consuming a lot of unique space or mostly sharing extents as expected.

Electra keeps about one month of btrfs snapshot history locally and on the replicated target. For anything older, use Backrest/restic.

The restore command copies into /tmp/restore-<snapshot>/... and refuses to overwrite existing files. Inspect the restored file, then copy it back manually when you are sure it is the version you want.

If both local drives are unavailable, or the needed version is only in the network backup, browse and restore from restic:

just backup-snapshots
just backup-ls latest /home/nimmo/Documents
just backup-restore-path latest /home/nimmo/Documents/example.txt

Restic restores under /tmp/backup-restore-<snapshot>/home/nimmo/... by default, again without touching the live home directory.

Disaster Recovery (Backrest/Restic Restore)

After a catastrophic failure, once a fresh NixOS install has been provisioned and the configuration deployed:

just backup-restore

This reads credentials from /run/secrets/ (populated by sops-nix at boot), shows the latest snapshot for confirmation, then restores all backed-up files to their original paths with restic restore latest --target / --overwrite always.

Prerequisites before running:

  1. NixOS installed and nixos-rebuild switch completed (sops-nix must have activated)
  2. Network access to the restic repository

Silent Boot (All hosts)

Defined in modules/boot/silent-boot.nix. Plymouth splash screen with quiet kernel parameters for a clean boot experience.

Hibernation (Electra)

Electra has a 50 GiB persistent LUKS-backed swap mapping for hibernation, separate from the LUKS-encrypted root and cryptdata volumes. The cryptdata LUKS contains btrfs subvols @games and @snapshots. Hibernation is configured via the standard NixOS swapDevices + boot.resumeDevice in hosts/electra/default.nix; initrd unlocks cryptswap before resume, with stable UUIDs used instead of NVMe device names.

  • Swap: /dev/mapper/cryptswap, priority 0. Zram stays at priority 5 so it continues to absorb working-set pressure; the encrypted disk swap is the hibernation target.
  • Resume: boot.resumeDevice points at /dev/mapper/cryptswap; NixOS auto-adds the resume path to the kernel cmdline for all three specialisations. IGPU uses Lanzaboote's managed PCR 4+7 policy; DGPU uses a separate PCR 7 + PIN token.
  • Triggers (configured via systemd-logind + upower):
    • Lid close → suspend; after 30 min suspended → hibernate (HibernateDelaySec=30min).
    • Power button → hibernate.
    • Battery at 5% → hibernate.

After the first nixos-rebuild switch on the new LUKS-backed swap, re-enroll TPM2 tokens:

just tpm2-enrol root
just tpm2-enrol data
just tpm2-enrol swap
just tpm2-enrol-pin root
just tpm2-enrol-pin data
just tpm2-enrol-pin swap

Run managed-policy rebuilds and enrolment in IGPU mode. The first three commands restore managed auto-unlock; the PIN commands add the DGPU fallback without removing the managed tokens. The LUKS passphrase remains independent recovery.

Auto Power Profile (Electra igpu/dgpu and Lena)

Defined in modules/power/auto-power-profile.nix. Triggered by udev on AC plug/unplug events and switches profiles:

  • Charging/Full + >25% battery: performance mode
  • Charging/Full + <=25% battery: balanced mode
  • Discharging + >50% battery: balanced mode
  • Discharging + <=50% battery: power-saver mode

Monitor with:

journalctl -u auto-power-profile.service -f

Current Hosts

Host Machine Channel Key Differences
electra Framework 16 unstable Base: endurance mode (vanilla kernel, locked power-saver, iGPU runtime PM, maker). Specialisation igpu: dynamic power profile, gaming/maker. Specialisation dgpu: NVIDIA/AMD hybrid, dual Ollama, Docker
lena Lenovo Ideapad 5 2-in-1 unstable Tablet mode, touchscreen, Disko partitioning, auto-update (pull-only), no Ollama/gaming
vega Intel mini server unstable Headless server for storage-backed services, AI agents, Attic cache, Docker, and pull-only auto-update
lyra Intel/NVIDIA GPU server unstable Headless GPU/media service host with NVIDIA Docker, Ollama, NFS/CIFS-backed stacks, and pull-only auto-update

Host Metadata

Every NixOS build records the configuration Git revision in system.configurationRevision. Clean builds use self.rev; dirty builds use self.dirtyRev, so the value includes -dirty when tracked config changes were present at build time. This is metadata only and does not affect boot menu labels.

Host inventory metadata lives under nixosConfig.host in each host's default.nix:

  • role: broad role such as laptop, server, or desktop
  • purpose: short human-readable purpose
  • location: fixed physical location when meaningful; laptops usually leave this as null
  • tags: filterable hardware/capability tags such as amdcpu, intelcpu, amdgpu, nvidiagpu, docker, ollama, attic, or media

Current tags:

Host Role Location Tags
electra laptop null laptop, amdcpu, amdgpu, nvidiagpu, docker, ollama
lena laptop null laptop, amdcpu, amdgpu
vega server desk server, intelcpu, docker, attic
lyra server desk, on top of vega server, intelcpu, nvidiagpu, docker, ollama, media

Print the evaluated inventory with:

just inventory
just inventory me
just inventory lyra

Show a tabular host-status view across every host (running kernel with an out-of-date marker, uptime, running revision + specialisation, divergence vs origin/main, deployed timestamp) with:

just fleet-status

Each host is probed over SSH with a short timeout; offline hosts render as (unreachable). The KERNEL column shows the running kernel and appends (!) when it differs from the kernel in the active specialisation of the current generation. UPTIME shows the host's human-readable uptime. The diff column follows the convention +N ahead, -N behind, (clean) when equal — (no origin/main) if the upstream ref is unreachable. Beszel remains the source for liveness and resource metrics.

Shared Across All Hosts (via modules/common/base.nix)

  • Systemd-boot, NetworkManager
  • Garbage collection (daily, 3-day age limit)
  • btrfs scrub (monthly, all btrfs mounts)
  • Beszel monitoring agent
  • Service-failure monitoring option provider (the monitor is enabled per host)
  • Portability options (modules/common/default-config.nix)
  • sops-nix host key configuration
  • UK locale (en_GB.UTF-8, Europe/London)

Backups (all current hosts via modules/profiles/backup.nix):

  • Backrest/restic service and secrets

Server baseline (vega and lyra via modules/server/base.nix):

  • SSH, mosh, server administration tools, and the backup profile

Desktop environment support (electra, lena — via modules/desktop/environment.nix):

  • Fonts (Noto, Fira Code, JetBrains Mono Nerd Font)
  • Printing (CUPS with Brother drivers, Avahi network discovery)
  • Removable storage (udisks2, NTFS support, polkit mount rules)
  • KDE Plasma 6 (login manager is host-specific: plasma-login-manager on electra, SDDM on lena)
  • Silent boot (Plymouth)
  • Auto power profile switching (udev-triggered)
  • Bluetooth
  • Desktop apps (modules/desktop/apps.nix)
  • Universal system packages (modules/common/system.nix)

Electra (All Modes)

  • Framework 16 hardware module (AMD GTT 32GB, fingerprint, PipeWire, zram, ROCm)
  • Expansion card automount (1TB NTFS at /run/media/nimmo/Expansion1TB)
  • AI desktop profile (modules/profiles/ai-desktop.nix) plus Ollama service: OpenCode, AI user tools, and Ollama
  • Maker profile (modules/profiles/maker.nix): Cura, OpenSCAD
  • NixOS auto-update
  • ROCm HSA override for AMD 780M iGPU (HSA_OVERRIDE_GFX_VERSION=11.0.0)
  • Docker with AMD 780M iGPU access
  • Hibernation: 50 GiB persistent LUKS-backed swap, suspend-then-hibernate on lid close, hibernate on power button and at 5% battery

Electra Base (battery)

  • Linux vanilla kernel (mainline, no latency tuning)
  • Laptop endurance mode enabled through modules/hardware/laptop.nix (laptop.enduranceMode.enable = true)
  • iGPU runtime power management (amdgpu.runpm=1)
  • Gaming profile disabled

Electra igpu Specialisation Only

  • Dynamic power profile switching (modules/power/auto-power-profile.nix)
  • Gaming profile (modules/profiles/gaming.nix): Steam, Gamemode, Discord, MangoHUD
  • Maker profile (modules/profiles/maker.nix): Cura, OpenSCAD

Electra dgpu Specialisation Only

  • NVIDIA GPU support (nvidia-open, Vulkan) with AMD iGPU + NVIDIA PRIME offload
  • Dynamic power profile switching (modules/power/auto-power-profile.nix)
  • Gaming profile (modules/profiles/gaming.nix): Steam, Gamemode, Discord, MangoHUD
  • NVIDIA Ollama instance (port 11435, CUDA acceleration)
  • Docker with NVIDIA Container Toolkit
  • nvtop for GPU monitoring

Lena-Specific

  • Tablet mode via iio-sensor-proxy (automatic screen rotation)
  • Fingerprint reader via fprintd
  • Btrfs with Disko-managed partitioning (subvolumes: @, @home, @nix, @log)
  • Both nimmo (admin) and claire (standard user) accounts
  • AI desktop profile (modules/profiles/ai-desktop.nix): Codex CLI, OpenCode and the composed OpenCode/Paseo runtime (modules/profiles/ai-runtime.nix)
  • Maker profile (modules/profiles/maker.nix): Cura, OpenSCAD
  • NixOS 25.11 stable channel

User Accounts

nimmo (All hosts)

  • Type: Administrator account
  • Groups: wheel (sudo), networkmanager, video, audio, render, docker (all electra modes)
  • Shell: bash with bash-it framework (rjorgenson theme)
  • Configuration: Managed by Home Manager in home/users/nimmo/
  • Packages: Via capability profiles (modules/profiles/); AI tools, gaming, and maker packages are per-host based on which profiles are imported
  • Features:
    • Git configuration unified across all hosts (nimmo@nimmog.uk)
    • Direnv with nix-direnv for automatic environment loading
    • Custom nixos-rebuild-auto function (rebuilds and reactivates current specialisation)
    • Optimized autostart: Nextcloud delayed 15 seconds, Discover disabled for faster login

claire (Lena only)

  • Type: Standard user account
  • Groups: networkmanager, video, audio (no wheel/sudo)
  • Purpose: Guest/family account with restricted privileges
  • Packages: System packages only

Ollama

Electra runs Ollama for local LLM inference using native NixOS services.

Architecture

Base / both modes (modules/profiles/ollama.nix): AMD Ollama instance

  • AMD iGPU Instance (port 11434): For large models with 48GB shared RAM
    • Context window: 65,536 tokens
    • Vulkan acceleration
    • Keep-alive: 24 hours

The same AMD-backed instance runs in both igpu and dgpu specialisations and uses the shared models directory at /run/media/models (btrfs subvolume).

Usage

Direct API access:

# Ollama instance (electra, all tiers)
curl http://localhost:11434/api/generate -d '{"model":"llama3.2","prompt":"Hello"}'

View logs:

journalctl -u ollama.service -f

Hardware Acceleration

dgpu specialisation:

  • AMD instance uses Vulkan with GTT extended to 32GB (amdgpu.gtt_size=32768)
  • The NVIDIA expansion bay remains available for graphics and container workloads, but Electra no longer runs a second CUDA-backed Ollama service

Base (iGPU mode):

  • Uses Vulkan with GTT extended to 32GB
  • HSA override for RDNA3 780M compatibility (HSA_OVERRIDE_GFX_VERSION=11.0.0)

Model Recommendations

For AMD iGPU with 48GB shared RAM - Large models:

  • Qwen 2.5 72B (high capability)
  • CodeLlama 70B (specialized for code)
  • QwQ 32B (reasoning focused)
  • Llama 3.1 70B (general purpose)
  • DeepSeek Coder 33B (advanced code generation)

Data Persistence

/run/media/models/        # Shared models directory (btrfs @models subvolume)
/var/lib/ollama/          # Ollama instance state

Flake Inputs

Input Purpose
nixpkgs Default NixOS unstable channel
nixpkgs-ollama-cuda Pinned nixpkgs revision supplying a known-good ollama-cuda
home-manager User environment management (unstable)
bash-it Bash framework for themes and plugins
codex-cli-nix Codex CLI (OpenAI)
paseo Paseo daemon, desktop app, and NixOS module
gitea-mcp-server Vega-hosted Forgejo MCP for issues and wiki over Nebula
nixos-hardware Hardware-optimized configs (Lenovo Ideapad, Framework)
disko Declarative disk partitioning
nur Nix User Repository (Firefox extensions)
sops-nix Encrypted secrets management (age-based)
plasma-manager Declarative KDE Plasma configuration via home-manager
framework-system Official Framework hardware tool (upstream flake)
trilium-next TriliumNext desktop notes package
nix-flatpak Declarative Flatpak management module
budslink-companion-widget BudsLink Plasma widget source

Learning More

The repository wiki contains the learning manual, operational playbooks, recovery procedures, and governance references. Active work is tracked in Forgejo milestones/issues/labels.

  • Need to do a specific task right now? Jump to Chapter 2 (Workflows) or Chapter 8 (Quick Reference).
  • Want to understand how this repo is organised? Start at Chapter 1 (Configuration Structure).
  • Something broke? Go to Chapter 7 (Troubleshooting).
  • New to Nix entirely? Start at Chapter 9 (Nix Language) and work through Part 2.