- Nix 56.5%
- Shell 27.3%
- Just 16.2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
Validated-base:
|
||
| .agents/skills/update-wiki | ||
| .engram | ||
| .forgejo | ||
| home | ||
| hosts | ||
| justfiles | ||
| lib | ||
| modules | ||
| nebula | ||
| packages/nixos-auto-update | ||
| potential | ||
| scripts | ||
| secrets | ||
| tests | ||
| users | ||
| .gitignore | ||
| .sops.yaml | ||
| AGENTS.md | ||
| flake.lock | ||
| flake.nix | ||
| Justfile | ||
| paseo.json | ||
| README.md | ||
| treefmt.nix | ||
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 syntaxjust deploy— Deploy current host configuration (auto-detects specialisation)just deploy igpu— Deploy and activate a specific specialisationjust deploy --host vega— Delegate a deploy to a remote host over SSHjust switch-spec igpu— Switch specialisation without rebuildingjust plasma-capture— Capture current KDE Plasma settings to home/users/nimmo/plasma.nixjust diff-last-update— Show package changes from the last updatejust generations— List system generations (before rollback)just gc— Run garbage collectionjust firmware-update— Update firmware via LVFS
Timer management:
just backup-snapshots— List Backrest/restic backups for this hostjust backup-snapshots-all— List Backrest/restic backups for all hostsjust backup-unlock— Remove stale restic locksjust backup-logs— Show Backrest service logsjust backup-status— Show Backrest service statusjust btrfs-snapshots— List local and second-NVMe btrfs snapshotsjust btrfs-snapshot-usage— Show per-snapshot btrfs space usage for both snapshot locationsjust btrfs-restore SNAPSHOT PATH— Restore one path from btrfs into/tmpjust backup-ls— Browse files in a restic snapshotjust backup-restore-path— Restore one path from restic into/tmpjust backup-restore— Restore latest snapshot (disaster recovery)just nixos-gc-last-run— Show last GC run detailsjust nixos-gc-run-now— Trigger GC immediatelyjust auto-update-last-run— Show last auto-update run detailsjust auto-update-run-now— Trigger auto-update immediatelyjust auto-update-hold "reason"— Block automatic updates during deployment testingjust auto-update-release— Release a deployment testing holdjust auto-update-hold-status— Show the active deployment holdjust auto-update-recover-stopped— Restart still-inactive services from the last auto-update stop listjust 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 underhome/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.nixcontains universal host defaults, fleet monitoring, and the service-failure option provider. Backups are composed explicitly bymodules/profiles/backup.nix; optional Forgejo MCP and Engram Cloud client support is composed bymodules/profiles/ai-agents.nix; Vega-only services are imported byhosts/vega/default.nix.modules/desktop/environment.nixis a desktop-support composition module covering audio, fonts, printing, removable storage, Firefox, and common desktop utilities. It remains undermodules/desktop/because it composes desktop-support capabilities rather than representing a complete user-facing environment;modules/profiles/gui.nixis the higher-level composition point.modules/profiles/gui.nixandmodules/profiles/laptop.nixare profiles that compose the desktop and laptop experience. Their system user-account imports remain separate from the per-user Home Manager layout underhome/users/.modules/profiles/ai-desktop.nixis the AI experience composition point;modules/profiles/ai-agents.nixowns shared AI client integration andmodules/profiles/ai-runtime.nixowns the coupled OpenCode/Paseo runtime, credentials, and Home Manager wiring. Keep host-specific provider overrides at the composition point.modules/profiles/ollama.nixis 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.nixon 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-autofunction (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 pointhome/users/nimmo/- Nimmo's focused Home Manager moduleshome/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:
- Adjust settings via the KDE System Settings GUI
- Run
just plasma-captureto capture the changes - 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:
- Track the work in a Forgejo issue and create a dedicated feature branch.
- Run the validation that matches the change: lint/regression tests for scripts
and workflows;
nix flake checkfor Nix configuration; both when a change spans them. - Open a merge request that references the issue and shares its milestone. The required Forgejo checks must pass before merge.
- 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.
- 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
dgpuwhen the NVIDIA expansion bay is installed. -
Use
igpuwhen the NVIDIA expansion bay is not installed. -
Do not choose
dgpujust 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:
-
Scaffold the host and write its configuration (steps 1–4 in Adding a New Host above), including a
disko.nixfor disk layout — nixos-anywhere requires disko to partition the disk. -
Validate and commit the configuration before provisioning:
just check git add -p && git commit -m "feat: Add HOSTNAME host configuration" git push -
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.50When prompted, paste the provisioning age private key from your password manager. nixos-anywhere will partition the disk, install NixOS, and reboot the machine.
-
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 -
Optionally remove the provisioning key from the machine (it lives under
/rootso 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 hostshosts/electra/default.nix- for electra (both modes)hosts/electra/default.nix(underspecialisation.dgpu.configuration) orhosts/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:
- Skips and sends an ntfy reminder if a deployment hold is active
- Skips if the repository has staged, unstaged, or untracked changes (work in progress detected)
- Fetches
origin/main, accepts only an explicit fast-forward, and refuses local-ahead or diverged checkouts rather than creating an automatic merge - On laptops below 20% battery while discharging, warns that an update is available and defers until a later check
- Validates the configuration with a dry-run build
- Builds the exact-revision Git flake as the repository owner, while retaining root only for activation
- Hands activation to a detached transient helper and verifies that the resulting
configurationRevisionmatches the intended Git commit, so replacing the updater unit during activation cannot abort its own switch - 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 --selectedpackage 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-releasecommand. 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-tokenSOPS secret insecrets/ntfy.yamland must be present before deploying with ntfy notifications enabled. URL and topic are set vianixosConfig.ntfy.url(https://ntfy.nimmog.uk) andnixosConfig.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-hardwarevalidates Electra and Lena.- Desktop application inputs validate Electra and Lena.
nixpkgs-sbomnix-176is deliberately pinned to the revision providing sbomnix 1.7.6 and is not automatically updated.nixpkgs-ollama-cudais deliberately pinned to the last known-good revision forollama-cudauntil 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:9898for LAN configuration - Firewall exposure is guarded so only
serveranddesktophosts 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.ukis centralized on vega via theforgejo/mcp/tokenSOPS 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 astrilium/sync-passwordand Vega will call Trilium's/api/setup/sync-from-serveronce 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.2using the generatedmobile-full-tunnel.txtnote 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.yamlis 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 supplyorgIdrepeatedly - The launcher accepts either the Pangolin instance root URL or an explicit
/v1API URL inpangolin/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.bookstackstack instead of a custom Docker deployment - On vega, nginx binds to
127.0.0.1:80and is intended to sit behind Pangolin/newt from the start, withhttps://wiki.nimmog.ukas 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-mcpserver are disabled by default; enable them withnixosConfig.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-mcpwrapper and BookStack API credentials - The MCP credentials live in
secrets/bookstack.yamlunderbookstack/api-token-idandbookstack/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:
- Main-drive btrfs snapshots at
/home/.btrbk-snapshots - Replicated btrfs snapshots on the second NVMe at
/run/media/snapshots - 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:
- NixOS installed and
nixos-rebuild switchcompleted (sops-nix must have activated) - 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.resumeDevicepoints at/dev/mapper/cryptswap; NixOS auto-adds the resume path to the kernel cmdline for all three specialisations. IGPU uses Lanzaboote's managed PCR4+7policy; DGPU uses a separate PCR7+ 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.
- Lid close → suspend; after 30 min suspended → 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 aslaptop,server, ordesktoppurpose: short human-readable purposelocation: fixed physical location when meaningful; laptops usually leave this asnulltags: filterable hardware/capability tags such asamdcpu,intelcpu,amdgpu,nvidiagpu,docker,ollama,attic, ormedia
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-autofunction (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.