Nix Config
This is my interpretation of the perfect nix flake, this powers my desktops, laptops, servers, vms, containers, and all aspects of my computers.
Features
- Automated Discovery: Hosts and users are automatically configured from filesystem structure
- Automated Persistence: TempFS with persistable components using [Impermanence] or BTRFS snapshotting
- Secret Management: Encrypted secrets in [NixOS] ([sops-nix]) and [home-manager] ([sops-nix] with [sops])
- Automated Updates: Flake dependency updates through Github Actions using [Update-flake-lock]
- Hardware Acceleration: Automatic hardware acceleration support detection and configuration
- Modular Architecture: Custom modules and overlays for extensibility
Supported Configurations
- [NixOS]-managed systems with automatic configuration discovery:
- Desktops - Personal workstations and development environments
- Servers - Infrastructure services and automation
- Laptops - Portable systems with power management
Repository Structure
The repository uses an automatic discovery system that scans the filesystem to build configurations:
.
├─ home # Root for all user homes (auto-discovered)
│ ├─── {username} # User-specific configurations
│ └─── shared # Shared home-manager modules
├─ hosts # Root for all hosts (auto-discovered by device type)
│ ├─── shared # Auto-imported modules for all hosts
│ │ ├─── global # Core system configuration (locale, networking, etc.)
│ │ └─── optional# Optional modules for specific use cases
│ ├─── desktop # Desktop NixOS systems
│ │ ├─── shared # Auto-imported modules for desktops
│ │ ├─── {host} # Individual desktop host configurations
│ ├─── laptop # Laptop NixOS Systems
│ │ ├─── shared # Auto-imported modules for laptops
│ │ └─── {host} # Individual laptop host configurations
│ └─── server # Server NixOS Systems
│ ├─── shared # Auto-imported modules for servers
│ └─── {host} # Individual server host configurations
├─ lib # Extensions to nixpkgs lib and custom builders
│ └─── builders # System and home-manager configuration builders
├─ modules # Custom NixOS and home-manager modules
├─ overlays # NixPkgs overlays for package modifications
├─ pkgs # Custom packages not in nixpkgs
└─ docs # Additional documentation
Auto-Discovery Mechanism
The flake automatically discovers:
- Hosts: By scanning
hosts/{device-type}/directories (excludingshared/) - Users: By scanning
home/directories and matching with existing hosts - Hardware Acceleration: Support based on predefined host lists
Welcome to the docs!
Over to the left, you’ll find the sidebar. There you’ll see several sections including “User guides”, “Modules”, “Packages”, “Overlays”, “Hosts”, and “Lib”.
If you’re looking for a specific option you may find the dedicated RacciDev Option Search (at the bottom of the sidebar) has more relevant results.
Installation Guide
This guide covers various installation scenarios for the nix-config repository.
Windows Subsystem for Linux (WSL)
Prerequisites
- Windows 10 version 2004 and higher (Build 19041 and higher) or Windows 11
- WSL 2 enabled
- Administrator access to Windows
Installation Steps
1. Install WSL 2
# Run in PowerShell as Administrator
wsl --install
# If WSL is already installed, ensure you're using WSL 2
wsl --set-default-version 2
2. Setup NixOS for WSL
Download and install NixOS-WSL via NixOS-WSL:
# Download the latest NixOS-WSL tarball
# Import the NixOS-WSL distribution
wsl --import NixOS .\NixOS\ nixos-wsl.tar.gz --version 2
# Start the NixOS instance
wsl -d NixOS
3. Configure NixOS-WSL
After starting your NixOS-WSL instance:
# Clone this repository
sudo git clone https://github.com/DaRacci/nix-config.git /etc/nixos
# Apply the WSL configuration
# Replace YOUR_WSL_HOSTNAME with your actual WSL hostname
sudo nixos-rebuild switch --flake /etc/nixos#YOUR_WSL_HOSTNAME
4. WSL-Specific Features
The WSL configuration includes:
- SSH agent relay between Windows and WSL
- Hardware acceleration support for development
- Remote desktop capabilities
- Optimized for headless operation
Native NixOS Installation
Prerequisites
- NixOS installation media
- Target hardware
- Network connectivity
- Backup of important data
Installation Process
1. Boot from NixOS Installation Media
- Download NixOS ISO from nixos.org
- Create bootable USB/DVD
- Boot from installation media
2. Network Configuration
# For WiFi connections
sudo systemctl start wpa_supplicant
wpa_cli
> add_network
> set_network 0 ssid "YourSSID"
> set_network 0 psk "YourPassword"
> enable_network 0
> quit
# Verify connectivity
ping nixos.org
3. Disk Setup
Follow standard NixOS installation procedures for disk partitioning and filesystem setup as described in the NixOS manual.
4. Generate Hardware Configuration
# Generate hardware configuration
nixos-generate-config --root /mnt
# Copy to your host configuration
mkdir -p /mnt/etc/nixos/hosts/{device-type}/{hostname}
cp /mnt/etc/nixos/hardware-configuration.nix /mnt/etc/nixos/hosts/{device-type}/{hostname}/hardware.nix
# Clone this repository
cd /mnt/etc/nixos
git clone https://github.com/DaRacci/nix-config.git .
5. Customize Host Configuration
Edit hosts/{device-type}/{hostname}/default.nix and hardware.nix according to your needs.
6. Install NixOS
# Install with your specific host configuration
nixos-install --flake .#{hostname}
# Set root password when prompted
7. Post-Installation
# Reboot into new system
reboot
# After reboot, ensure configuration is applied
sudo nixos-rebuild switch --flake /etc/nixos#{hostname}
Existing NixOS System Migration
From Traditional NixOS Configuration
1. Backup Current Configuration
# Backup current configuration (adjust path if using flakes)
sudo cp -r /etc/nixos /etc/nixos.backup
2. Clone This Repository
# Clone to a working directory
git clone https://github.com/DaRacci/nix-config.git /tmp/nix-config
sudo cp -r /tmp/nix-config/* /etc/nixos/
3. Create Host Configuration
# Create your host directory
sudo mkdir -p /etc/nixos/hosts/{device-type}/{hostname}
# Migrate your hardware configuration
sudo cp /etc/nixos.backup/hardware-configuration.nix /etc/nixos/hosts/{device-type}/{hostname}/hardware.nix
# Create default.nix based on your old configuration
# Edit to follow the new structure
4. Test and Apply
# Test the new configuration
sudo nixos-rebuild build --flake .#{hostname}
# Apply if build succeeds
sudo nixos-rebuild switch --flake .#{hostname}
IO Guardian - Database Availability System
Purpose
The IO Guardian system ensures that services across the infrastructure are aware of the availability of centralized databases hosted on the Database Coordinator. It provides graceful startup and shutdown coordination between the database host and dependent services on other servers.
Entry Point
- Main file:
modules/nixos/server/database/guardian.nix
Architecture / Services / Scope
The system consists of two components:
-
Guardian Server (runs on client servers)
- WebSocket server that listens for commands from the coordinator
- Secures connections with a pre-shared key (PSK) from the
DB_GUARDIAN_PSKsecret - Executes drain/undrain commands by controlling
db-databases.target
-
Guardian Client (runs on the Database Coordinator)
- WebSocket client that connects to all guardian servers
- Sends
undraincommand after databases are online (start dependent services) - Sends
draincommand before database shutdown (stop dependent services)
How It Works
System Startup
- Client servers boot and run
wait-for-db-databases.service - This service waits (with retries) until PostgreSQL and Redis on the Database Coordinator are reachable
- Once databases are confirmed available, the service completes
- The
db-databases.targetis now ready to be activated - When the Database Coordinator’s
db-database-coordinator.servicestarts, it sendsundrainto all clients - Clients start
db-databases.target, which starts all dependent services
Database Shutdown (Graceful Drain)
- When
db-database-coordinator.servicestops (before databases stop) - It connects to all guardian servers via WebSocket
- Sends
draincommand to each server - Guardian servers stop
db-databases.target - Dependent services stop gracefully before databases go down
Database Startup (Undrain)
- When databases come online on the Database Coordinator
db-database-coordinator.servicestarts- It sends
undraincommand to all guardian servers - Guardian servers start
db-databases.target - All dependent services start
Systemd Units
On Client Servers
| Unit | Type | Description |
|---|---|---|
db-guardian.service | simple | WebSocket server for receiving commands |
db-databases.target | target | Represents “databases are online” |
wait-for-db-databases.service | oneshot | Waits for databases at boot (runs once) |
On the Database Coordinator
| Unit | Type | Description |
|---|---|---|
db-database-coordinator.service | oneshot | Sends undrain on start, drain on stop |
Protocol Reference
The guardian uses a simple JSON-based WebSocket protocol:
Authentication
Client sends:
{ "type": "auth", "key": "<psk>" }
Server responds:
{ "type": "auth", "status": "ok", "message": "Authentication successful" }
Commands
Coordinator sends one of the supported actions (drain, undrain, or ping):
{ "type": "command", "action": "drain" }
Server responds:
{ "type": "response", "action": "<action>", "status": "ok", "message": "..." }
Secrets
Communication is secured using a Pre-Shared Key (PSK) that must be at least 32 characters. All WebSocket connections must authenticate with this key before commands are accepted.
Declared secrets
| Secret key | Owner | Group | Restart unit | Purpose |
|---|---|---|---|---|
DB_GUARDIAN_PSK | root | root | db-guardian.service | Authenticate DB Guardian WebSocket API |
Operational Notes / Assumptions
Configuration
Port: The guardian WebSocket server listens on port 9876 by default. This port is automatically opened to local subnets on servers with database dependencies.
Dependent Services: Dependent Services will be automatically populated with service names where there is a systemd.service.<name> defined from the names in server.database.postgres or server.database.redis.
To manually add a service bind to the database availability target, add it to the server.database.dependentServices option.
Services listed here will start only when db-databases.target is active, stop when it stops, and restart when the target restarts.
Troubleshooting
Checking Guardian Status
On client servers:
systemctl status db-guardian.service
systemctl status db-databases.target
systemctl status wait-for-db-databases.service
journalctl -u db-guardian.service -f
On the Database Coordinator:
systemctl status db-database-coordinator.service
journalctl -u db-database-coordinator.service
Manual Commands
To manually start/stop dependent services on a client:
systemctl start db-databases.target
systemctl stop db-databases.target
Common Issues
Guardian server won’t start:
- Check that
DB_GUARDIAN_PSKsecret is properly configured - Verify the SOPS decryption is working and the secret is non-empty:
test -s /run/secrets/DB_GUARDIAN_PSK
Services not starting after boot:
- Check wait service logs:
journalctl -u wait-for-db-databases.service - Verify network connectivity to the Database Coordinator on ports 5432 (Postgres) and 6379 (Redis)
- Ensure the Database Coordinator has sent the undrain command
Authentication failures in logs:
- Ensure the same PSK is deployed to all servers
- Re-encrypt secrets if the key was changed
References
Server Cluster Monitoring — Observability Stack
Purpose
The monitoring module provides a comprehensive observability stack for the server cluster using Prometheus (metrics), Loki (logs), Grafana (visualization), and Grafana Alloy for authenticated OTLP ingestion. All components are configured as reusable NixOS modules with automatic cross-host discovery.
The system consists of three layers:
-
Exporters (run on all servers)
- node_exporter for system-level metrics (CPU, memory, disk, network, per-process stats)
- Grafana Alloy for shipping journald logs and Caddy access logs to Loki
- Conditional Exporters: The following exporters are enabled if their corresponding services are configured on the host:
- Caddy access logs are parsed and sent to Loki
- fail2ban exporter available on the IO Coordinator
- PostgreSQL exporter available on the Database Coordinator
- Redis exporter available on the Database Coordinator
- Proxmox exporter available on the Monitoring Coordinator
-
Collectors (run on the Monitoring Coordinator)
- Prometheus for metrics aggregation
- Loki for log aggregation with 90-day retention
- Alertmanager for alert routing and notifications
- OTLP/HTTP ingestion on
otlp.<domain>with bearer-token authentication
-
Visualization (runs on the Monitoring Coordinator)
- Grafana with provisioned datasources and dashboards
Entry Point
- Main file:
modules/nixos/server/monitoring/default.nix
Architecture / Services / Scope
Configuration
Enabling Monitoring
Monitoring is enabled by default on all servers, this can be disabled with server.monitoring.enable = false.
Options
server.monitoring.collector.alerting.enable
| Type | boolean |
| Default | cfg.enable |
| Example | true |
Whether to enable Alertmanager and alert rules.
server.monitoring.collector.alerting.homeAssistant.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable Home Assistant webhook alerting.
server.monitoring.collector.alerting.nextcloudTalk.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable Nextcloud Talk webhook alerting.
server.monitoring.collector.enable
| Type | boolean |
| Default | thisIsMonitoringPrimaryHost && cfg.enable |
| Example | true |
Whether to enable monitoring collector services (Prometheus, Loki, Grafana).
server.monitoring.collector.grafana.kanidm.enable
| Type | boolean |
| Default | true |
| Example | true |
Whether to enable Kanidm OAuth2 authentication for Grafana.
server.monitoring.collector.otlp.bearerTokenSecret
| Type | string |
| Default | "MONITORING/OLTP/BEARER_TOKEN" |
SOPS secret path used as the bearer token for OTLP/HTTP ingestion.
server.monitoring.collector.otlp.enable
| Type | boolean |
| Default | isThisMonitoringPrimaryHost && cfg.enable |
| Example | true |
Whether to enable OTLP/HTTP ingestion via Grafana Alloy.
server.monitoring.collector.otlp.port
| Type | signed integer |
| Default | 4318 |
Port for the OTLP/HTTP ingestion endpoint.
server.monitoring.collector.otlp.subdomain
| Type | string |
| Default | "otlp" |
Subdomain used for the OTLP/HTTP ingestion endpoint.
server.monitoring.collector.proxmox.enable
| Type | boolean |
| Default | isThisMonitoringPrimaryHost && cfg.enable |
| Example | true |
Whether to enable Proxmox VE metrics collection.
server.monitoring.enable
| Type | boolean |
| Default | true |
| Example | true |
Whether to enable monitoring for this server.
server.monitoring.exporters.caddy.enable
| Type | boolean |
| Default | cfg.enable && config.services.caddy.enable |
| Example | true |
Whether to enable Caddy metrics exporter.
server.monitoring.exporters.fail2ban.enable
| Type | boolean |
| Default | cfg.enable && isThisIOPrimaryHost && config.server.fail2ban.enable |
| Example | true |
Whether to enable fail2ban metrics exporter.
server.monitoring.exporters.node.enable
| Type | boolean |
| Default | cfg.enable |
| Example | true |
Whether to enable node_exporter for system-level metrics.
server.monitoring.exporters.postgres.enable
| Type | boolean |
| Default | cfg.enable && thisIsIOPrimaryHost && hasPostgresDatabases |
| Example | true |
Whether to enable PostgreSQL exporter.
server.monitoring.exporters.process.enable
| Type | boolean |
| Default | cfg.enable |
| Example | true |
Whether to enable Process exporter for monitoring specific processes.
server.monitoring.exporters.redis.enable
| Type | boolean |
| Default | cfg.enable && thisIsIOPrimaryHost && hasRedisInstances |
| Example | true |
Whether to enable Redis exporter.
server.monitoring.logs.enable
| Type | boolean |
| Default | cfg.enable |
| Example | true |
Whether to enable Alloy log shipping.
server.monitoring.logs.extraConfiguration
| Type | strings concatenated with "\n" |
| Default | "" |
Additional configuration for the alloy log processor. This is useful for adding custom Loki stages, relabeling rules, or write targets.
Note that the default configuration for processing the system journal is always included and does not need to be specified here.
server.monitoring.retention.logs
| Type | string |
| Default | "90d" |
Loki log retention period.
server.monitoring.retention.metrics
| Type | string |
| Default | "90d" |
Prometheus TSDB retention period.
server.monitoring.scrapeConfigs
| Type | attribute set of (submodule) |
| Default | { } |
Declarative scrape configs for services running on this host. These are collected by the monitoring primary host and converted into Prometheus scrape configurations.
server.monitoring.scrapeConfigs.<name>.bearer_token_secret
| Type | null or string |
| Default | null |
SOPS secret path for bearer token authentication. When set, the secret will be created on the monitoring primary host.
server.monitoring.scrapeConfigs.<name>.host
| Type | string |
| Default | config.host.name |
Host to scrape metrics from.
server.monitoring.scrapeConfigs.<name>.job_name
| Type | string |
| Default | "‹name›" |
Prometheus job name for this scrape target.
server.monitoring.scrapeConfigs.<name>.metrics_path
| Type | string |
| Default | "/metrics" |
HTTP path to the metrics endpoint.
server.monitoring.scrapeConfigs.<name>.port
| Type | signed integer |
Port the metrics endpoint listens on.
server.monitoring.scrapeConfigs.<name>.scheme
| Type | one of "http", "https" |
| Default | "http" |
URL scheme for scraping.
Secrets
Declared secrets
| Secret key | Purpose |
|---|---|
MONITORING/OLTP/BEARER_TOKEN | Bearer token for OTLP HTTP ingestion |
MONITORING/GRAFANA/SECRET_KEY | Grafana secret key |
MONITORING/GRAFANA/OAUTH_SECRET | Kanidm OAuth2 secret for Grafana |
MONITORING/HOME_ASSISTANT/WEBHOOK_URL | Home Assistant alert webhook |
MONITORING/NEXTCLOUD_TALK/WEBHOOK_URL | Nextcloud Talk alert webhook |
PROXMOX/USER | Proxmox metrics user |
PROXMOX/TOKEN_ID | Proxmox token name |
PROXMOX/TOKEN_SECRET | Proxmox token secret |
Generating Secrets
Generating secure random secrets can be done with the following command:
cat /dev/urandom | tr -dc 'A-Za-z0-9' | head -c 48
The MONITORING/GRAFANA/OAUTH_SECRET must match the value in hosts/server/<Application Server>/secrets.yaml under KANIDM/OAUTH2/GRAFANA_SECRET (the Kanidm provisioning side).
Caddy Virtual Hosts
The module configures four virtual hosts on Monitoring Coordinator:
| Service | Subdomain | Access |
|---|---|---|
| Grafana | grafana.<domain> | Public |
| OTLP | otlp.<domain> | Public, bearer token required |
| Prometheus | prometheus.<domain> | LAN |
| Loki | loki.<domain> | LAN |
Grafana remains protected by the existing Kanidm-backed login flow. The OTLP
ingestion endpoint is intended for machine-to-machine clients and requires an
Authorization: Bearer <token> header on every request. The exposed OTLP/HTTP
paths are the standard /v1/metrics and /v1/logs endpoints.
These are defined in hosts/server/nixmon/default.nix and collected by the IO
Coordinator’s Caddy configuration.
Alert Rules
The following alerts are configured by default:
| Alert | Condition | Severity |
|---|---|---|
HostDown | up{job="node"} == 0 for 2 minutes | Critical |
DiskSpaceCritical | Root filesystem < 10% free for 5 minutes | Critical |
HighCPUUsage | CPU usage > 90% for 5 minutes | Warning |
HighMemoryUsage | Memory usage > 90% for 5 minutes | Warning |
ServiceDown | up{job!="node"} == 0 for 2 minutes | Critical |
Alerts are routed to:
- Home Assistant: All critical and warning alerts via webhook (requires
collector.alerting.homeAssistant.enable = true) - Nextcloud Talk: Critical alerts only via webhook (requires
collector.alerting.nextcloudTalk.enable = true)
Module Structure
modules/nixos/server/monitoring/
├── default.nix # Entry point, imports sub-modules
├── options.nix # All server.monitoring.* options
├── collector/
│ ├── default.nix # Imports collector sub-modules
│ ├── prometheus.nix # Prometheus server + scrape targets
│ ├── loki.nix # Loki server + storage config
│ ├── grafana.nix # Grafana + Kanidm OAuth2
│ ├── otlp.nix # OTLP ingestion
│ ├── alerting.nix # Alertmanager + alert rules
│ └── dashboards.nix # Dashboard provisioning
├── exporters/
│ ├── default.nix # Imports exporter sub-modules
│ ├── node.nix # node_exporter
│ ├── caddy.nix # Caddy metrics
│ ├── postgres.nix # PostgreSQL exporter
│ ├── redis.nix # Redis exporter
│ └── fail2ban.nix # fail2ban metrics exporter
├── logs/
│ └── alloy.nix # Alloy log shipping
└── integrations/
└── proxmox.nix # PVE exporter for Proxmox API
Operational Notes / Assumptions
Troubleshooting
Checking Service Status
On the monitoring host (Monitoring Coordinator):
systemctl status prometheus.service
systemctl status loki.service
systemctl status grafana.service
systemctl status prometheus-alertmanager.service
systemctl status prometheus-pve-exporter.service
On any server:
systemctl status prometheus-node-exporter.service
systemctl status prometheus-fail2ban-exporter.service
systemctl status alloy.service
Verifying Metrics Collection
Check Prometheus targets are up:
curl -s http://localhost:9090/api/v1/targets | jq '.data.activeTargets[] | {instance: .labels.instance, health: .health}'
Verifying Log Collection
Alloy applies ingest-time parsing for journal stdout logs and Caddy access logs before forwarding to Loki:
-
Caddy access logs are read as JSON, not plain text
-
Legacy timestamps in form
YYYY/MM/DD HH:MM:SSare parsed and used as event timestamps -
ISO-8601 timestamps with a log level prefix are parsed and normalized
-
detected_leveldefaults toinfowhen the source log line does not provide one -
Caddy JSON fields
level,ts,logger, andstatusare extracted into Loki labels and timestamps -
Caddy access logs are read from
/var/log/caddy-access-*.logand use the timestamp and level prefix in each line when present
node_exporter also enables the processes collector, which exposes per-process metrics such as CPU and memory usage for running processes.
Check Alloy is shipping logs:
journalctl -u alloy.service -f
Query Loki directly:
curl -s 'http://localhost:3100/loki/api/v1/labels' | jq
Common Issues
Grafana OAuth login fails:
- Verify
MONITORING/GRAFANA/OAUTH_SECRETin the Monitoring Coordinator matchesKANIDM/OAUTH2/GRAFANA_SECRETin the Identity Coordinator - Check Kanidm provisioning has the Grafana OAuth2 client configured
- Verify DNS resolves
auth.<domain>correctly
Prometheus targets showing as down:
- Check firewall rules allow traffic on exporter ports from the monitoring host
- Verify the exporter service is running on the target host
- Check network connectivity between Monitoring Coordinator and the target host
Proxmox metrics missing:
- Verify
PROXMOX/TOKEN_IDandPROXMOX/TOKEN_SECRETare valid - Check PVE API is accessible from Monitoring Coordinator:
curl -k https://pve.<domain>/api2/json - Review PVE exporter logs:
journalctl -u prometheus-pve-exporter.service
References
Creating New Users
To add a new user configuration:
1. Create User Directory
mkdir -p home/newuser
2. Create User Configuration Files
Create host-specific configurations in home/newuser/{hostname}.nix:
{ pkgs, lib, ... }:
{
imports = [
# Import shared configurations
./features/cli # Common CLI tools
./features/desktop/common # Desktop environment basics
];
# User-specific configuration
home = {
username = "newuser";
homeDirectory = "/home/newuser";
stateVersion = "25.05"; # Set this to the Home Manager state version from initial setup; keep it unchanged during normal NixOS or Home Manager upgrades, and only change it during a planned migration
};
# Add user-specific packages and configuration
programs = {
git = {
userName = "Your Name";
userEmail = "your.email@domain.com";
};
};
}
Create feature modules in home/newuser/features/:
mkdir -p home/newuser/features/{cli,desktop,development}
3. Link User to Hosts
The auto-discovery system will automatically link users to hosts if:
- A file
home/{username}/{hostname}.nixexists - The hostname matches an existing host configuration
4. Test User Configuration
# Build home-manager configuration
home-manager build --flake .#newuser@hostname
# Switch to new configuration
home-manager switch --flake .#newuser@hostname
Creating New Hosts
To add a new host to your configuration:
1. Create Host Directory Structure
# For a new desktop host named "mydesktop"
mkdir -p hosts/desktop/mydesktop
# For a new server host named "myserver"
mkdir -p hosts/server/myserver
# For a new laptop host named "mylaptop"
mkdir -p hosts/laptop/mylaptop
2. Create Required Configuration Files
Create hosts/{device-type}/{hostname}/default.nix:
{ self, pkgs, ... }:
{
imports = [
# Hardware configuration (required)
./hardware.nix
# Optional: device-specific modules
# "${self}/hosts/shared/optional/containers.nix"
# "${self}/modules/nixos/custom-module.nix"
];
# Host-specific configuration
host = {
device.isHeadless = false; # Set to true for servers
};
# Add your system configuration here
# networking.hostName is automatically set from directory name
}
Create hosts/{device-type}/{hostname}/hardware.nix:
{ inputs, ... }:
{
imports = [
# Include relevant hardware modules
inputs.nixos-hardware.nixosModules.common-cpu-amd
inputs.nixos-hardware.nixosModules.common-pc-ssd
# For laptops, also include:
# inputs.nixos-hardware.nixosModules.common-pc-laptop
];
# Boot configuration
boot.loader = {
systemd-boot.enable = true;
efi.canTouchEfiVariables = true;
};
# Filesystem configuration (use disko for declarative disk setup)
fileSystems."/" = {
device = "/dev/disk/by-label/nixos";
fsType = "ext4";
};
# Add hardware-specific configuration
}
3. Add Hardware Acceleration (Optional)
If your host supports hardware acceleration, add it to the acceleration lists in flake.nix:
accelerationHosts = {
cuda = [
"your-new-host" # Add here for CUDA support
];
rocm = [
"your-amd-host" # Add here for ROCm support
];
};
4. Build and Test
# Build the configuration (don't switch yet)
sudo nixos-rebuild build --flake .#your-new-host
# Test the configuration
sudo nixos-rebuild test --flake .#your-new-host
# Switch to the new configuration
sudo nixos-rebuild switch --flake .#your-new-host
Updating SOPS Rules
update-sops first regenerates managed sops-keys.nix, then regenerates .sops.yaml from repository layout. Host and home entries are sorted by name for stable output. Run it after adding, removing, or rotating a host key or home user.
Host Discovery
A host is discovered only when this file exists:
hosts/{device-type}/{hostname}/ssh_host_ed25519_key.pub
The script converts each OpenSSH public key with ssh-to-age and creates rules for:
hosts/secrets.yaml— every discovered hosthosts/server/secrets.yaml— every discovered serverhosts/{device-type}/{hostname}/— each discovered host directory
Host-specific rules also cover nested SOPS files under that host directory.
Managed Key Map
update-sops writes sops-keys.nix as a readable inventory of discovered recipients:
{
deployer = "age1gmc8dd4mj5q0zncy5gq4lccjlq9v84t8cqnlananmxt8g0jezv6szawll8";
homes = {
racci = "...";
};
hosts = {
server = {
nixauth = "...";
};
};
}
Host recipients come from ssh_host_ed25519_key.pub. Home recipients come from home/{username}/id_ed25519.pub. The current user’s name comes from whoami; that user’s home recipient is used as the personal recipient in every generated SOPS rule. The current user must have a home public key file.
Every directory under home/, except home/shared/, gets a rule for:
home/{username}/secrets.yaml
Home directories without id_ed25519.pub still get SOPS rules, but do not get an entry in sops-keys.nix.
Common Recipients
Every generated rule starts with two always-present recipients, in this order:
- current user’s
home/{username}/id_ed25519.pubconverted withssh-to-age— personal age key sops-keys.nixdeployervalue — automated deployer age key
The updater defines this policy in get-always-present-age-keys. Host-specific rules append that host’s age recipient. Global host rules append all applicable host recipients in the same stable order used by sops-keys.nix.
Usage
Regenerate rules:
update-sops
Check for drift without writing:
update-sops --check
By default, updater changes only sops-keys.nix and .sops.yaml. To also update recipient metadata in every encrypted SOPS file under hosts/ and home/, opt in explicitly:
update-sops --update-secrets
This runs sops updatekeys --yes for each file reported as encrypted by sops filestatus, including non-YAML formats. It can re-encrypt file metadata and requires an available SOPS decryption identity. --update-secrets cannot be combined with --check.
Using a Nix Package or NixOS Module from a Separate Fork of Nixpkgs
This guide will show you how to use a Nix package or NixOS module from a separate fork of nixpkgs.
Step 1: Define the Forked Repository
In your Nix file, define the forked repository using fetchFromGitHub function:
nixpkgs.overlays = [
(self: super: {
<your-package> = (import
(pkgs.fetchzip (
let owner = "<owner>"; branch = "<branch>"; in {
url = "https://github.com/${owner}/nixpkgs/archive/${branch}.tar.gz";
# Change to 52 zeros when archive needs to be redownloaded.
sha256 = "<sha256>";
}
))
{ overlays = [ ]; config = super.config; }).<your-package>;
})
];
In this example, replace <your-package>, <owner>, <branch>, and <sha256> with the actual values from the forked repository.
Step 2: Use Packages or Modules from the Forked Repository
Now you can use packages or modules from the forked repository in your Nix expressions. For example, if you want to use a package from the forked repository, you can refer to it using the <your-package> attribute. Here’s an example:
{
environment.systemPackages = with pkgs; [
<your-pckage>
];
}
In this example, replace <your-package with the actual name of the package you want to use.
Declarative GNOME DConf
Description
When changing GNOME or GNOME extension settings, it is recommended to use dconf2nix and cherry pick its output. This allows for easy configuration using the GUI, but requires copying the settings back into the respective dconf settings in home-manager to save them.
DConf Locations
The locations for where to save DConf settings to is:
- Base.nix for standard GNOME DConf Settings.
- Extensions.nix for Extensions DConf Settings
- Per User Settings should be saved in the format of
home/${username}/desktop/gnome.nix
Getting the Output
dconf2nix will be installed as part of this flakes dev shell.
Running the following will output the current dconf settings into a temporary file so you can Cherry Pick your changes.
dconf dump / | dconf2nix > dconf.nix
Using a Package/Module from a Fork
Default Development Shell
The default devenv shell in this repository includes common CLI, Nix, and setup tools used across day-to-day development.
Entering the Shell
Automatic shell entry via direnv, to enable this, run:
direnv allow
Or directly with nix:
nix develop --override-input devenv-root file+file://<path-to-nix-config>/.devenv/root
Helper Commands
The shell also exposes repository helper commands from flake/dev/scripts/.
rebuild-target accepts action host, or just host; when the action is omitted it defaults to switch. Extra args are forwarded to nh / nix after --accept-flake-config, and args that start with -- must be separated with a literal --:
rebuild-target build nixmi -- --fallback
rebuild-target switch nixmi
Generated .luarc.json
Entering the default shell refreshes .luarc.json in the repository root.
The shell task manages the "workspace.library" key by:
- creating
.luarc.jsonwhen missing - ensuring the current Hyprland stub path is present
- removing stale entries containing
share/hypr/stubs/ - respect and preserve any other existing keys in
.luarc.jsonthat are not managed by the shell task.
Python Development Shell
This repository contains multiple Python scripts and packages. A dedicated Python development environment is provided via devenv to simplify development of these components, by providing all necessary dependencies for proper LSP support.
Entering the Python Shell
To access the Python development environment with all required dependencies:
direnv allow
devenv shell python
Or with devenv directly:
devenv shell --file devenv.nix --shell python
Included Packages
The Python shell inherits from the default development environment and adds:
Python Runtime & Tools
- python312 - Python 3.12 interpreter
- pip - Package installer
- virtualenv - Virtual environment management
- pytest - Testing framework
- black - Code formatter
- ruff - Fast Python linter
- mypy - Static type checker
Python Libraries
Libraries are organized by the components they support:
Image Compression
pillow- Image processingrich- Terminal formatting & progress barspython-magic- File type detection
Memory/Knowledge Systems
pyyaml- YAML parsingcryptography- Encryption utilitiesanyio- Async I/O framework
I/O Guardian & Networking
websockets- WebSocket protocolpystemd- Systemd D-Bus interface
Utilities
requests- HTTP library (for Lidarr plugin updates)
Development Workflow
Testing Scripts
Run pytest on project Python files:
pytest pkgs/scripts/test_image_compressor.py -v
Code Quality
Format Python code:
nix fmt -- pkgs/scripts/compressor.py
Check types with mypy:
mypy pkgs/scripts/
Running Scripts Directly
Scripts can be executed directly in the shell:
python3 pkgs/scripts/compressor.py --help
python3 docs/preprocessor/gen-options-md.py --help
Adding New Python Dependencies
To add a new Python library:
- Identify the
python312Packages.attribute in nixpkgs - Add it to the
packageslist in/persist/nix-config/flake/dev/devenv.nixunderdevenv.shells.python - Run
nix fmt flake/dev/devenv.nixto format - Test with
nix flake check - Update this documentation
Example:
# In devenv.nix, add to packages list:
python312Packages.your-new-package
Troubleshooting
ModuleNotFoundError
If you see ModuleNotFoundError: No module named 'xxx', the package may not be in the python shell. Verify it’s included in the packages list in flake/dev/devenv.nix.
Python Version Mismatch
Some packages require Python 3.12. If you need a different version:
- Modify
python312reference inflake/dev/devenv.nix - Update corresponding
python312Packagesreferences - Run
nix flake checkto validate
Permissions Errors with pystemd
The pystemd package requires D-Bus access. Ensure you’re running within a proper devenv session, not in a sandboxed environment.
See Also
Modules Overview
Purpose
This section provides an overview of the custom NixOS and Home-Manager modules defined in this repository. These modules allow for modular and reusable configurations across different hosts and users.
Architecture / Services / Scope
Entry Points
modules/nixos/: Contains NixOS-specific modules.modules/flake/: Flake-level modules for cross-host configuration.modules/home-manager/: Contains Home-Manager-specific modules.
NixOS Modules — Overview
Purpose
This section covers all NixOS modules provided by this flake, including core baseline configuration, server orchestration, custom services, and AI infrastructure.
Entry Point
- Main file:
modules/nixos/default.nix
Architecture / Services / Scope
Modules are categorized into directories based on their target:
core/: Shared baselines for all hosts.server/: Cluster-aware server modules.services/: Custom NixOS services (e.g., Dashy, Tailscale).ai/: AI infrastructure daemons.
References
NixOS Services — Overview
Purpose
This section documents the custom NixOS service modules available in this configuration. These modules provide specialised integrations and monitoring capabilities.
Entry Point
- Main file:
modules/nixos/services/default.nix
Architecture / Services / Scope
Nested service modules emit generated fragments for options, which are included in their respective documentation pages.
Operational Notes / Assumptions
- Services should be toggled per-host.
References
AI Agent — Hermes Autonomous Agent
Purpose
Autonomous AI Agent service powered by Hermes, providing intelligent task automation with security controls for code review and development tasks.
Entry Point
- Main file: ai-agent.nix
- Upstream: Hermes Agent
- Package: The module routes
services.hermes-agent.packagethrough the localpkgs.hermes-agentoverlay, which carries a few upstream patches.
Options
services.ai-agent.apiServer.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable the OpenAI comptable endpoint.
services.ai-agent.apiServer.host
| Type | string |
| Default | "127.0.0.1" |
The host/IP for the API server to bind to.
services.ai-agent.apiServer.port
| Type | signed integer |
| Default | 8642 |
The port for the API server to listen on.
services.ai-agent.apiServer.tokenReference
| Type | string |
| Default | "AI_AGENT/API_SERVER_TOKEN" |
The sops secret attribute for the API server authentication token.
services.ai-agent.containerPostStart
| Type | list of (string or (submodule)) |
| Default | [ ] |
Shell commands to run inside the AI agent container after startup.
A plain string runs inside the container as root via docker exec.
An attrset { command = "..."; host = true; } runs on the host.
Commands to run after the AI agent container starts. Container commands get automatic retry to wait for Docker + container readiness.
services.ai-agent.dashboard.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable Hermes web dashboard.
services.ai-agent.dashboard.oidc.clientId
| Type | string |
The OIDC client ID for dashboard authentication.
services.ai-agent.dashboard.oidc.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable OpenID Connect authentication for the dashboard.
services.ai-agent.dashboard.oidc.issuer
| Type | string |
The OIDC issuer URL for dashboard authentication.
services.ai-agent.dashboard.oidc.provider
| Type | string |
| Default | "self-hosted" |
The OIDC plugin to use for dashboard authentication.
services.ai-agent.dashboard.oidc.scopes
| Type | list of string |
| Default | [ "openid" "profile" "email" ] |
The OIDC scopes to request for dashboard authentication.
services.ai-agent.dashboard.port
| Type | signed integer |
| Default | 9119 |
The port for the dashboard to listen on.
services.ai-agent.dashboard.publicURL
| Type | null or string |
| Default | null |
The public URL for the dashboard, used for generating links in notifications and similar. If not set, localhost URLs will be used.
If set, must be a valid URL starting with http:// or https://.
services.ai-agent.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable autonomous AI Agent service.
services.ai-agent.extras.browser
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable headless browser for web scraping and automation.
services.ai-agent.extras.plugins
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable extra plugins for the agent.
services.ai-agent.extras.scraper.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable web scraping and search plugins for the agent.
services.ai-agent.extras.scraper.searxEndpoint
| Type | null or string |
| Default | null |
The SearxNG endpoint to use for web search.
services.ai-agent.memory.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable long-term memory
When enabled this disables the builtin user profile and memory markdown features, to nudge the agent towards using the configured long-term memory provider for all memory. .
services.ai-agent.models.brains
| Type | string |
| Default | "deepseek/deepseek-v4-pro-0813" |
The smartest model to use for complex reasoning and decision-making tasks.
Used for auxiliary models:
services.ai-agent.models.compression
| Type | string |
| Default | "~deepseek/deepseek-v4-flash-latest" |
The model to use for compression of context, summorisation and similar tasks that don’t require reasoning. This model still needs a decently sized context window to be effective.
Used for auxiliary models:
services.ai-agent.models.primary
| Type | string |
| Default | "~deepseek/deepseek-v4-flash-latest" |
The primary language model to use for the AI agent.
services.ai-agent.models.provider
| Type | string |
| Default | "openrouter" |
The model provider to use.
services.ai-agent.models.simpleton
| Type | string |
| Default | "inclusionai/ling-3.0-flash" |
The simpleton model to delegate tasks to that require less reasoning, basic understanding and small context windows.
Used for auxiliary models:
services.ai-agent.models.vision
| Type | string |
| Default | "xiaomi/mimo-v2.5" |
The vision model to delegate image understanding tasks to.
services.ai-agent.platform.discord.allowedUsers
| Type | list of string |
| Default | [ ] |
A list of Discord user IDs that the agent is allowed to interact with.
services.ai-agent.platform.discord.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable Discord as a messaging channel.
services.ai-agent.platform.discord.homeChannel
| Type | null or string |
| Default | null |
The Discord channel ID to use as the home channel for the agent.
services.ai-agent.platform.discord.tokenReference
| Type | string |
| Default | "AI_AGENT/DISCORD_BOT_TOKEN" |
The sops secret attribute for the Discord bot token.
services.ai-agent.platform.hassio.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable Home Assistant as a tool and notification channel.
services.ai-agent.platform.hassio.tokenReference
| Type | string |
| Default | "AI_AGENT/HASSIO_TOKEN" |
The sops secret attribute for the Home Assistant long-lived access token.
services.ai-agent.platform.hassio.url
| Type | string |
The URL for the Home Assistant instance, including the scheme.
services.ai-agent.platform.webhook.port
| Type | signed integer |
| Default | 8654 |
The port for the webhook listener to listen on.
services.ai-agent.settings
| Type | Hermes config attrs, deep-merged with list concatenation. |
| Default | { } |
Hermes config to merge into services.hermes-agent.settings.
This is a local option that merges correctly across feature toggles.
The upstream services.hermes-agent.settings uses lib.recursiveUpdate for its config type,
which replaces lists instead of concatenating them, so separate blocks would clobber each other.
This option deep-merges with list concatenation and is emitted once as the final value of services.hermes-agent.settings.
services.ai-agent.voice.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable voice input and output using the TTS and STT.
services.ai-agent.voice.wyoming-stt.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable use existing Wyoming faster-whisper server for STT instead of running a separate Whisper instance.
services.ai-agent.voice.wyoming-stt.host
| Type | string |
| Default | "localhost" |
The host of the Wyoming faster-whisper server.
services.ai-agent.voice.wyoming-stt.port
| Type | signed integer |
| Default | 10300 |
The port of the Wyoming faster-whisper server.
Architecture / Services / Scope
The module enables services.hermes-agent (running inside a Docker container) and adds optional components on top:
- Dashboard — a separate
hermes-dashboardsystemd service runsdocker execinto thehermes-agentcontainer to serve the dashboard under thehermesuser. Environment files configured viaservices.hermes-agent.environmentFilesare loaded by systemd’sEnvironmentFiledirective (read as root) and passed into the container viadocker exec --env-file. The dashboard stays local by default and does not open a browser. - Voice & STT — optional voice input and output. With
services.ai-agent.voice.wyoming-stt.enable, Hermes reuses an existing Wyoming faster-whisper server instead of running a separate Whisper instance:HERMES_LOCAL_STT_COMMANDis set to invokewyoming-transcribe, which sends audio over the Wyoming protocol and returns the transcript. No second Whisper process needed. - OIDC Authentication — optional OpenID Connect authentication for the dashboard using a public PKCE client (no
client_secret). The client ID is a public identifier — it does not need to be stored as a secret. The module generates aHERMES_DASHBOARD_OIDC_ENVenvironment file with the OIDC settings, loaded by thehermes-dashboardservice. - Memory (Mnemosyne) — with
services.ai-agent.memory.enable, the memory provider switches from the built-in user profile (USER.md injection) to Mnemosyne, a local SQLite-backed memory system with semantic recall (SQLite with FTS5 hybrid ranking + vector search).
Secrets
Hermes requires API keys via environment files. Configure via sops-nix:
sops = {
secrets = {
"AI_AGENT/OPENROUTER_API_KEY" = { };
};
templates."HERMES_ENV".content = ''
OPENROUTER_API_KEY=${config.sops.placeholder."AI_AGENT/OPENROUTER_API_KEY"}
'';
};
services.hermes-agent.environmentFiles = [ config.sops.templates."HERMES_ENV".path ];
The module itself declares secrets for the enabled optional components:
- API server token (default
AI_AGENT/API_SERVER_TOKEN) — authenticates the OpenAI-compatible API server. - Discord bot token (default
AI_AGENT/DISCORD_BOT_TOKEN) — Discord platform. - Home Assistant token (default
AI_AGENT/HASSIO_TOKEN) — Home Assistant platform. - Dashboard OIDC uses a public PKCE client, so no client secret is stored.
Operational Notes / Assumptions
Usage Example
{ ... }: {
services.ai-agent = {
enable = true;
};
}
Voice & STT
Enable voice input and output with services.ai-agent.voice.enable = true;. To reuse an existing Wyoming faster-whisper server instead of running a separate Whisper instance:
{ ... }: {
services.ai-agent = {
enable = true;
voice = {
enable = true;
wyoming-stt.enable = true;
};
};
}
Dashboard & OIDC
Enable the web dashboard with services.ai-agent.dashboard.enable = true;, and OIDC authentication with services.ai-agent.dashboard.oidc.enable = true;:
{ ... }: {
services.ai-agent = {
enable = true;
dashboard = {
enable = true;
publicURL = "https://dashboard.example.com";
oidc = {
enable = true;
provider = "self-hosted";
issuer = "https://auth.example.com/oauth2/openid/hermes";
clientId = "hermes";
scopes = [ "openid" "profile" "email" ];
};
};
};
}
Memory (Mnemosyne)
Enable long-term memory with services.ai-agent.memory.enable = true;:
{ ... }: {
services.ai-agent = {
enable = true;
memory.enable = true;
};
}
References
Huntress — Managed EDR
Purpose
Managed EDR (Endpoint Detection and Response) platform that protects systems by detecting malicious footholds used by attackers.
Entry Point
- Main file: huntress.nix
- Upstream: Huntress Managed EDR
Options
services.huntress.accountKeyFile
| Type | string |
The account key for the Huntress agent.
services.huntress.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable Huntress service.
services.huntress.organisationKeyFile
| Type | string |
The organisation key for the Huntress agent.
services.huntress.package
| Type | package |
| Default | <derivation huntress-0.14.74> |
The Huntress package to use.
Architecture / Services / Scope
The module runs a single huntress-agent systemd service as root. The agent configuration is generated at /etc/huntress/agent_config.yaml during the service’s preStart phase: a default configuration is written on first start, after which the account and organisation keys are merged in using yaml-merge.
Secrets
accountKeyFile— Huntress account key, loaded into the service via systemdLoadCredential.organisationKeyFile— Huntress organisation key, loaded into the service via systemdLoadCredential.
Operational Notes / Assumptions
- Both keys are validated during
preStart; the service fails to start if either is empty. - The merged configuration persists across restarts in
/etc/huntress/agent_config.yaml.
Usage Example
{ config, ... }: {
services.huntress = {
enable = true;
accountKeyFile = config.sops.secrets.huntress_account_key.path;
organisationKeyFile = config.sops.secrets.huntress_org_key.path;
};
}
References
MCPO — Model Context Protocol Orchestrator
Purpose
Orchestrates Model Context Protocol (MCP) servers, providing a centralized way to manage and expose multiple MCP servers.
Entry Point
- Main file: mcpo.nix
- Upstream: MCPO GitHub Repository
Options
services.mcpo.apiTokenFile
| Type | null or absolute path |
| Default | null |
Path to a file containing the API token for the mcpo service. This file will be exposed to the service through a systemd credential named “apiToken”.
services.mcpo.configuration
| Type | attribute set of (submodule) |
| Default | { } |
This option has no description.
services.mcpo.configuration.<name>.args
| Type | list of string |
| Default | [ ] |
Arguments to pass to the command.
services.mcpo.configuration.<name>.command
| Type | null or string |
| Default | null |
Command to render the config file.
services.mcpo.configuration.<name>.headers
| Type | attribute set of string |
| Default | { } |
Headers to pass to the command.
services.mcpo.configuration.<name>.type
| Type | null or one of "sse", "streamable-http" |
| Default | null |
This option has no description.
services.mcpo.configuration.<name>.url
| Type | null or string |
| Default | null |
This option has no description.
services.mcpo.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable mcpo (Model Context Protocol Orchestrator) service.
services.mcpo.environment
| Type | attribute set of string |
| Default | { } |
Additional environment variables for the service.
services.mcpo.extraPackages
| Type | list of package |
| Default | [ ] |
Additional packages to include in the service’s PATH.
services.mcpo.helpers
| Type | attribute set |
| Default | { npxServer = <function>; npxServerWithArgs = <function>; uvxServer = <function>; uvxServerWithArgs = <function>; } |
Helper functions for constructing mcpo server command blocks.
services.mcpo.package
| Type | package |
| Default | <derivation mcpo-0.0.18> |
Package providing the mcpo executable.
Architecture / Services / Scope
MCPO runs as a DynamicUser with a state directory at /var/lib/mcpo. The configuration is rendered via sops.templates and loaded into the service via systemd credentials. The service’s PATH includes bash, nodejs, and uv by default to support various MCP server types; additional packages can be added with services.mcpo.extraPackages.
Secrets
apiTokenFile(optional) — API token exposed to the service as the systemd credentialapiToken.- Server configuration and environment are rendered through sops templates (
mcpoConfiguration,mcpoEnvironment) and consumed viaLoadCredential/EnvironmentFile.
Operational Notes / Assumptions
Usage Example
{ config, ... }: {
services.mcpo = {
enable = true;
configuration = {
everything = config.services.mcpo.helpers.npxServer "@modelcontextprotocol/server-everything";
};
};
}
Package Patches
- mcpo-union-repr-compat.patch — Applied via overlay in
overlays/patches/. Upstream testsrc/mcpo/tests/test_main.pyassertsUnionrepr starts with"typing.Union[", but Python 3.12+ may stringify unions asstr | float. Patch usesget_origin(result_type) is Unioninstead. Build/test compatibility only; no runtime impact.
References
Metrics & Hacompanion — Metrics Collection & Home Assistant Integration
Purpose
Comprehensive metrics collection and integration with Home Assistant via hacompanion.
Entry Point
- Main file: metrics.nix
- Upstream: Hacompanion GitHub Repository
Options
services.metrics.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable Metrics collection service.
services.metrics.hacompanion.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable Enable Home Assistant Companion service.
services.metrics.hacompanion.script
| Type | attribute set of (submodule) |
This option has no description.
services.metrics.hacompanion.script.<name>.device_class
| Type | null or one of "absolute_humidity", "apparent_power", "aqi", "area", "atmospheric_pressure", "battery", "blood_glucose_concentration", "carbon_dioxide", "carbon_monoxide", "current", "data_rate", "data_size", "date", "distance", "duration", "energy", "energy_distance", "energy_storage", "enum", "frequency", "gas", "humidity", "illuminance", "irradiance", "moisture", "monetary", "nitrogen_dioxide", "nitrogen_monoxide", "nitrous_oxide", "ozone", "ph", "pm1", "pm10", "pm25", "power", "power_factor", "precipitation", "precipitation_intensity", "pressure", "reactive_energy", "reactive_power", "signal_strength", "sound_pressure", "speed", "sulphur_dioxide", "temperature", "timestamp", "volatile_organic_compounds", "volatile_organic_compounds_parts", "voltage", "volume", "volume_flow_rate", "volume_storage", "water", "weight", "wind_direction", "wind_speed" |
| Default | null |
The device class for the script in Home Assistant.
services.metrics.hacompanion.script.<name>.icon
| Type | string |
| Default | "mdi:script-text-outline" |
The icon to use for the script in Home Assistant.
services.metrics.hacompanion.script.<name>.name
| Type | string |
The name of the script as it will appear in Home Assistant.
services.metrics.hacompanion.script.<name>.path
| Type | absolute path |
The path to the script to execute.
services.metrics.hacompanion.script.<name>.type
| Type | one of "sensor", "switch" |
| Default | "sensor" |
The type of the script in Home Assistant.
services.metrics.hacompanion.script.<name>.unit_of_measurement
| Type | null or string |
| Default | null |
The unit of measurement for the script in Home Assistant.
services.metrics.hacompanion.sensor.audio_volume.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable Enable the audio_volume sensor.
services.metrics.hacompanion.sensor.companion_running.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable Enable the companion_running sensor.
services.metrics.hacompanion.sensor.cpu_temp.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable Enable the cpu_temp sensor.
services.metrics.hacompanion.sensor.cpu_usage.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable Enable the cpu_usage sensor.
services.metrics.hacompanion.sensor.load_avg.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable Enable the load_avg sensor.
services.metrics.hacompanion.sensor.memory.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable Enable the memory sensor.
services.metrics.hacompanion.sensor.online_check.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable Enable the online_check sensor.
services.metrics.hacompanion.sensor.power.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable Enable the power sensor.
services.metrics.hacompanion.sensor.uptime.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable Enable the uptime sensor.
services.metrics.hacompanion.sensor.webcam.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable Enable the webcam sensor.
services.metrics.hacompanion.storage
| Type | attribute set of (submodule) |
| Default | { } |
Storage devices and ZFS pools to monitor
services.metrics.hacompanion.storage.<name>.name
| Type | null or string |
| Default | null |
The pretty display name for this storage device in Home Assistant.
services.metrics.hacompanion.storage.<name>.sensors.avail
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable Enable available space sensor.
services.metrics.hacompanion.storage.<name>.sensors.read
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable Enable read speed sensor.
services.metrics.hacompanion.storage.<name>.sensors.temperature
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable Enable temperature sensor.
services.metrics.hacompanion.storage.<name>.sensors.used
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable Enable used space sensor.
services.metrics.hacompanion.storage.<name>.sensors.write
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable Enable write speed sensor.
services.metrics.hacompanion.test
| Type | anything |
| Default | hacompanionConfig |
This option has no description.
services.metrics.upgradeStatus.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable Enable Upgrade Status service.
services.metrics.upgradeStatus.uptimeKuma.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable Enable Uptime Kuma tracking for Upgrade Status.
Architecture / Services / Scope
- hacompanion — Home Assistant companion daemon that publishes system sensors (CPU, memory, storage, uptime, and more) to Home Assistant. It uses a generated TOML configuration file and loads the Home Assistant API token from
sops.secrets.HACOMPANION_ENV. - upgradeStatus — reports NixOS upgrade state (idle / running / failed / dirty). When
upgradeStatus.uptimeKuma.enableis set, it also sends heartbeat notifications to Uptime Kuma on successful upgrades.
Secrets
HACOMPANION_ENV— Home Assistant API token, declared inhosts/secrets.yamland consumed viaEnvironmentFile.UPGRADE_STATUS_ID— Uptime Kuma push monitor ID (host-levelsecrets.yaml), required whenupgradeStatus.uptimeKuma.enableis set.
Operational Notes / Assumptions
- Hacompanion runs as a
DynamicUserwith its state in/var/lib/hacompanion. - The
upgradeStatusfeature can integrate with Uptime Kuma to provide heartbeat notifications for successful system upgrades.
Usage Example
{ ... }: {
services.metrics.hacompanion = {
enable = true;
sensor.cpu_temp.enable = true;
sensor.memory.enable = true;
storage.main = {
name = "Main OS Drive";
sensors.used = true;
};
};
}
References
Tailscale — Tag Management Extensions
Purpose
Extensions to the standard NixOS Tailscale module, providing easier tag management.
Entry Point
- Main file: tailscale.nix
Options
services.tailscale.tags
| Type | list of string |
| Default | [ ] |
Additional tags to advertise for this device.
Tags are used for access control and routing in Tailscale. See https://tailscale.com/kb/1018/tags/ for more information.
Architecture / Services / Scope
This module extends the standard NixOS services.tailscale module by automatically constructing the --advertise-tags flag from the configured services.tailscale.tags list.
Operational Notes / Assumptions
- Ensure the device has the necessary permissions in your Tailscale ACLs to apply the requested tags.
Usage Example
{ ... }: {
services.tailscale = {
enable = true;
tags = [ "server" "internal" ];
};
}
References
Core Module
Documents shared NixOS core modules used across hosts.
Purpose
modules/nixos/core/ contains reusable host-level defaults and feature modules.
It also defines top-level baseline options under core.* that control this shared behavior for most hosts.
Options
core.activation.enable
| Type | boolean |
| Default | config.core.enable |
| Example | true |
Whether to enable report diff on activation.
core.audio.enable
| Type | boolean |
| Default | !config.host.device.isHeadless |
| Example | true |
Whether to enable Enable audio support.
core.auto-upgrade.enable
| Type | boolean |
| Default | true |
| Example | true |
Whether to enable auto-upgrade.
core.auto-upgrade.hostName
| Type | string |
| Default | config.networking.hostName |
The hostName to use for auto-upgrade
core.bluetooth.enable
| Type | boolean |
| Default | !config.host.device.isHeadless |
| Example | true |
Whether to enable Enable Bluetooth support.
core.containers.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable container support.
core.defaultGroups
| Type | list of string |
| Default | [ ] |
Additional groups to add all users to by default.
core.display-manager.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable display manager configuration.
core.enable
| Type | boolean |
| Default | true |
| Example | true |
Whether to enable Enable core features.
core.gaming.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable Enable gaming features.
core.hm-helper._1password.enableCli
| Type | boolean |
| Default | anyoneHasPackage pkgs._1password-cli |
| Example | true |
Whether to enable Enable 1Password Cli support.
core.hm-helper._1password.enableGUI
| Type | boolean |
| Default | anyoneHasPackage pkgs._1password-gui |
| Example | true |
Whether to enable Enable 1Password GUI support.
core.hm-helper.enable
| Type | boolean |
| Default | config ? home-manager |
| Example | true |
Whether to enable Home Manager helper functions.
core.hm-helper.ff2mpv.enable
| Type | boolean |
| Default | anyoneHasPackage pkgs.ff2mpv-rust |
| Example | true |
Whether to enable Enable ff2mpv native messaging host for Firefox..
core.hm-helper.hmUsers
| Type | list of string |
| Default | [ ] |
List of Home Manager users that also exist in config.users.users.
core.hm-helper.kde-connect.enable
| Type | boolean |
| Default | anyoneHasOption (user: user.services.kdeconnect.enable) |
| Example | true |
Whether to enable Enable KDE Connect firewall rules if any user has KDE Connect enabled..
core.hm-helper.nautilus.enable
| Type | boolean |
| Default | anyoneHasPackage pkgs.nautilus |
| Example | true |
Whether to enable Enable Nautilus extensions and integration helpers..
core.locale.enable
| Type | boolean |
| Default | true |
| Example | true |
Whether to enable locale configuration.
core.network.enable
| Type | boolean |
| Default | !config.host.device.isVirtual |
| Example | true |
Whether to enable Enable network support.
core.networking.enable
| Type | boolean |
| Default | config.core.enable |
| Example | true |
Whether to enable opinionated networking defaults.
core.networking.tailscale.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable tailscale configuration.
core.openssh.enable
| Type | boolean |
| Default | true |
| Example | true |
Whether to enable OpenSSH server and client opinionated configuration.
core.openssh.hostPrivateKeyPath
| Type | string |
| Default | "/var/lib/provisioning/ssh/ssh_host_ed25519_key" |
Canonical path of the provisioned ed25519 host private key used by OpenSSH, SOPS age decryption, and server-to-server SSH.
core.printing.enable
| Type | boolean |
| Default | config.host.device.role != "server" && !config.host.device.isVirtual |
| Example | true |
Whether to enable printing support.
core.remote.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable remote features.
core.remote.remoteDesktop
| Type | submodule |
| Default | { } |
This option has no description.
core.remote.remoteDesktop.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable remote desktop.
core.remote.remoteDesktop.startCommand
| Type | string |
| Default | "gnome-session" |
Command to start remote desktop session.
core.remote.streaming
| Type | submodule |
| Default | { } |
This option has no description.
core.remote.streaming.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable remote streaming.
core.security.enable
| Type | boolean |
| Default | true |
| Example | true |
Whether to enable security features.
core.security.userLimit
| Type | unsigned integer, meaning >=0 |
| Default | 131072 |
The maximum number of open files per user.
This is used to set the limits for both PAM and systemd.
core.sops.enable
| Type | boolean |
| Default | config.core.enable |
| Example | true |
Whether to enable SOPS auto configuration.
core.sops.hostSecretsFile
| Type | absolute path |
| Default | "/nix/store/jq8636fkq2anq8f33kfqa816d0nrqw4m-source/hosts/secrets.yaml" |
Where the SOPS secret file of this host is located in the flake.
core.stylix.enable
| Type | boolean |
| Default | !config.host.device.isHeadless |
| Example | true |
Whether to enable Stylix configuration.
core.virtualisation.bridgeInterface
| Type | string |
| Default | "br0" |
Bridge interface used for libvirt networking.
core.virtualisation.cpuCores
| Type | signed integer |
| Default | 24 |
Total CPU core/thread count used for isolation helpers. Must be >= 4.
core.virtualisation.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable virtualisation support.
core.virtualisation.externalInterface
| Type | string |
| Default | "eth0" |
Physical interface attached to bridge.
core.virtualisation.gpu.audio
| Type | string |
| Default | "10de:1bef" |
PCI address for passthrough GPU audio device.
core.virtualisation.gpu.video
| Type | string |
| Default | "10de:1b06" |
PCI address for passthrough GPU video device.
core.virtualisation.isolatedGuests
| Type | list of string |
| Default | [ "win11" "win11-gaming" ] |
List of guests to apply isolation helpers to.
core.virtualisation.vmUsers
| Type | list of string |
| Default | [ ] |
Users that should receive kvm and libvirtd group membership for VM management.
core.wsl.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable WSL specific configurations, optimisations, and fixes.
core.wsl.user
| Type | string |
The default user to use for WSL.
Architecture / Services / Scope
Baseline Behaviour
When core.enable is true, module applies shared defaults from modules/nixos/core/default.nix:
- sets
services.dbus.implementation = "broker", - enables PipeWire audio stack and disables PulseAudio when
core.audio.enableis on, - enables Bluetooth stack, Blueman, and persisted Bluetooth state when
core.bluetooth.enableis on, - enables NetworkManager and adds
networkto shared default groups whencore.network.enableis on, and - on non-headless hosts, adds
videoandi2cgroups and enablesdleyna,gnome-keyring,udisks2,colord,xserver.updateDbusEnvironment, andpolkit.
Audio baseline also enables security.rtkit, adds audio, pipewire, and rtkit groups, installs udev rules for rtc0 and hpet, and sets PAM limits for realtime audio workloads.
Bluetooth baseline unblocks rfkill during activation and persists /var/lib/bluetooth.
Usage Example
{ ... }: {
core = {
enable = true;
audio.enable = true;
bluetooth.enable = true;
network.enable = true;
};
}
References
- Activation
- Auto Upgrade
- Containers
- Display Manager
- Gaming
- Generators
- Default Groups
- Locale
- Nix
- OpenSSH
- Printing
- Remote Access
- Security
- SOPS
- Stylix
- Virtualisation
- WSL
Operational Notes / Assumptions
These modules are imported through modules/nixos/core/default.nix. Most feature pages document their own core.<name> option namespaces, while some baseline modules such as Nix apply unconditionally once imported.
Activation
Reports system generation changes during NixOS activation.
Purpose
Provide visibility into what changed between system generations by comparing the previous and newest generation during NixOS activation, so each upgrade shows a readable package and closure diff.
Entry Point
- Main file: activation.nix
Architecture / Services / Scope
When enabled, the module installs an activation script (system.activationScripts.report-changes) that:
- locates the previous and newest system profile generations under
/nix/var/nix/profiles, - resolves both links to their store paths, and
- runs
nvd diffbetween them.
If no previous generation exists yet, the script does nothing.
Operational Notes / Assumptions
- Diff output is informational only; the script tolerates a non-zero
nvdexit so activation never fails because of a diff error. - Default follows top-level
core.enable, so most hosts get generation diff reporting automatically.
Auto Upgrade
Schedules automatic NixOS upgrades from flake host outputs.
Purpose
Keep hosts current by rebuilding them from the flake on a schedule, with randomized start times to spread load across hosts and resource limits so upgrades do not starve interactive workloads.
Entry Point
- Main file: auto-upgrade.nix
Options
core.auto-upgrade.enable
| Type | boolean |
| Default | true |
| Example | true |
Whether to enable auto-upgrade.
core.auto-upgrade.hostName
| Type | string |
| Default | config.networking.hostName |
The hostName to use for auto-upgrade
Architecture / Services / Scope
When enabled, the module configures system.autoUpgrade to rebuild the host from the configured GitHub flake output for that host, using the --refresh, --accept-flake-config, and --no-update-lock-file flags, and applies CPU and IO resource limits to nixos-upgrade.service.
Operational Notes / Assumptions
- Auto-upgrade only turns on when the flake has a revision (
self.rev), meaning the repository is in a clean, revisioned state. Dirty working trees or non-revisioned evaluations leavesystem.autoUpgrade.enable = false. - Upgrades always target the GitHub flake source, not the local checkout.
Containers
Enables Docker-based container runtime defaults.
Purpose
Provide a Docker-backed container runtime for workloads that still rely on Docker-specific features, with automatic image pruning and persistent state.
Entry Point
- Main file: containers.nix
Options
core.containers.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable container support.
Architecture / Services / Scope
When enabled, the module:
- enables
virtualisation.dockerwith the default Docker package, - enables CDI device support in the Docker daemon,
- enables weekly automatic image pruning,
- sets
virtualisation.oci-containers.backend = "docker", - adds
dockertocore.defaultGroups, and - persists Docker state directories under
/var/lib/docker(overlay storage, images, volumes, containers, containerd, and buildkit data).
Operational Notes / Assumptions
- Docker is intentionally preferred because current workloads still need features not covered by Podman or
podman-compose. - Users receive Docker access through the shared
core.defaultGroupshandling.
Display Manager
Configures the display manager for graphical sessions on desktop and laptop hosts.
Purpose
Provide a consistent, terminal-based login experience via greetd and tuigreet on hosts with a display.
Entry Point
- Main file: display-manager.nix
Options
core.display-manager.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable display manager configuration.
Architecture / Services / Scope
Enabled by default on hosts where host.device.isHeadless = false. The greetd greeter runs as the greeter user and:
- shows the current time,
- remembers the last logged-in user and last selected session, and
- exposes both Wayland and X11 session directories when
services.displayManager.sessionPackagesis non-empty.
Greeter cache is persisted to /var/cache/tuigreet through host.persistence.directories.
Operational Notes / Assumptions
- Both Wayland (
wayland-sessions) and X11 (xsessions) session paths are built dynamically from installed session packages, so adding a new session package is enough to make it appear in the greeter.
Gaming — Gaming, VR, and Steam-focused desktop features
Purpose
Enable a desktop gaming stack around Steam, 32-bit graphics, Android ADB tooling, and WiVRn VR streaming, plus firewall, udev, and optional Decky Loader lifecycle integration.
Entry Point
- Main file: gaming.nix
Options
core.gaming.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable Enable gaming features.
Architecture / Services / Scope
When enabled, module:
- adds
adbuserstocore.defaultGroups, - enables
hardware.steam-hardwareand 32-bit graphics support, - installs
android-tools, - enables Steam with Steam Deck style launch arguments,
extest,xwayland-run/xwininfoextras, andproton-ge-bincompatibility, - opens Steam Remote Play and local transfer firewall rules,
- enables WiVRn with
highPriority, OpenXR runtime import, firewall access, and JSON config, - installs udev rules for PlayStation controller, Oculus Quest, and tty ACM devices, and
- overlays
gamescope-sessionfor 4K resolution and wider refresh limits.
Firewall ports opened: UDP 41492, 9943, 9944 plus TCP 8082, 9943, 9944, 24070.
Decky Loader Integration
If config.jovian.decky-loader.enable is true, module additionally:
- prevents
decky-loader.servicefrom auto-starting at boot, - adds Polkit rule so active local user can start and stop
decky-loader.service, and - when Home Manager is present, installs user service that polls Steam PID file, starts Decky Loader once Steam is running, and stops it after Steam exits.
WiVRn Socket Activation
WiVRn is activated on-demand through systemd user socket activation instead of running for the full session.
systemd.user.sockets.wivrnlistens on%t/wivrn/comp_ipc(UNIX socket, mode0770).- Socket is
WantedBy=default.target, so it’s available throughout the session, but WiVRn itself only starts when a client connects. - Service env override:
IPC_EXIT_ON_DISCONNECT=on— WiVRn exits after client disconnects. - Steam’s OpenXR runtime path resolves to the socket; activating a SteamVR/OpenXR game triggers socket activation.
- Parent dir created with
0750, socket with0770.
Operational Notes / Assumptions
- Module assumes desktop-class host with graphics stack and Steam support.
- WiVRn config uses NVENC H.265 encoder entries and enables
wayvras application. - Some extra behavior only appears when related modules already exist, such as Jovian Decky Loader and Home Manager.
Generators
Groups
Locale
Sets shared timezone and locale defaults.
Purpose
Provide opinionated regional defaults for timezone and locale so all hosts start from a consistent baseline without forcing per-host overrides.
Entry Point
- Main file: locale.nix
Options
core.locale.enable
| Type | boolean |
| Default | true |
| Example | true |
Whether to enable locale configuration.
Architecture / Services / Scope
The module applies an Australian timezone default and enables Australian and US English UTF-8 locales.
Operational Notes / Assumptions
- Module is enabled by default.
- Because settings use
mkDefault, the module acts as a baseline rather than a hard override.
Nix
Defines shared Nix daemon, cache, and registry defaults.
Purpose
Establish global Nix configuration for every host in the flake: overlays, state version, trusted users, experimental features, binary caches, garbage collection, registry-derived nixPath, plus automatic uploads to the remote Attic cache.
Entry Point
- Main file: nix.nix
Architecture / Services / Scope
The module applies shared baseline configuration directly (no core.* options). It:
- installs the
nix4vscodeoverlay, - sets
system.stateVersionfrom thestate.versionfile at the flake root, - configures trusted users, automatic store optimisation, experimental features, substituters and trusted public keys for the project’s binary caches, daily automatic GC, and a
nixPathderived fromconfig.nix.registry.
It also:
- enables
services.angrrto retain recent system profiles, and - creates
systemd.services.attic-watch-store, which waits fornetwork-online.target, restarts on failure, logs into Attic using a SOPS-managed cache push key, and watches the store for uploads.
Secrets
The module declares the SOPS secret CACHE_PUSH_KEY (from hosts/secrets.yaml) and restarts attic-watch-store.service when it changes. This key authenticates automatic uploads to the remote Attic cache.
Operational Notes / Assumptions
- Because the module has no enable flag, it is always active and applied to all hosts.
attic-watch-storedepends onsops.secrets.CACHE_PUSH_KEYfromhosts/secrets.yaml.services.angrrkeeps a bounded set of recent system profile generations.
OpenSSH
Configures opinionated SSH server and client defaults.
Purpose
Provide a hardened, consistent SSH experience across hosts: ed25519-only host keys, no password authentication, automatically generated known-host entries, PAM ssh-agent authentication, and GatewayPorts = "clientspecified" so clients may request non-loopback forwarding binds when needed. That forwarding flexibility can expose tunnels beyond localhost if the client explicitly asks for it.
Entry Point
- Main file: openssh.nix
Options
core.openssh.enable
| Type | boolean |
| Default | true |
| Example | true |
Whether to enable OpenSSH server and client opinionated configuration.
core.openssh.hostPrivateKeyPath
| Type | string |
| Default | "/var/lib/provisioning/ssh/ssh_host_ed25519_key" |
Canonical path of the provisioned ed25519 host private key used by OpenSSH, SOPS age decryption, and server-to-server SSH.
Architecture / Services / Scope
When enabled, the module:
- enables
services.opensshwith socket activation disabled so SSH runs as a traditional always-on service (avoiding disconnects duringnixos-rebuild switchover SSH), - disables password authentication and sets
PermitRootLogin = "prohibit-password", - configures the ed25519 host key from
core.openssh.hostPrivateKeyPath, persists that file viahost.persistence.files, and publishes the matching public key at/etc/ssh/ssh_host_ed25519_key.pub, - enables
security.pam.sshAgentAuth, - adds the current host’s public host key to root’s authorized keys, and
- generates
programs.ssh.knownHostsentries for every host inoutputs.nixosConfigurations.
Client configuration restricts host key algorithms and accepted public key types to ssh-ed25519.
Operational Notes / Assumptions
- Module expects a matching host public key file to exist in the flake for each host.
- The current host gets
localhostas an extra known-host alias in the generated SSH client config. - Root authorization uses host key material from the flake, not per-user login keys.
- Socket activation is disabled (
startWhenNeeded = false): the default NixOS setup spawns per-connectionsshd@...serviceinstances, and restarting them during a configuration switch disconnects active SSH sessions. An always-on service prevents remote disconnection duringnixos-rebuild switch.
Printing
Enables shared printer support for workstation-class NixOS hosts.
Purpose
Provide CUPS printing and common printer drivers on non-server, non-virtual hosts so local and network printing work out of the box.
Entry Point
- Main file: printing.nix
Options
core.printing.enable
| Type | boolean |
| Default | config.host.device.role != "server" && !config.host.device.isVirtual |
| Example | true |
Whether to enable printing support.
Architecture / Services / Scope
When both top-level core.enable and core.printing.enable are on, the module:
- enables
services.printingwith HP and Gutenprint driver stacks plus Brother colour laser driver packages, and - adds
lptocore.defaultGroups.
Operational Notes / Assumptions
- Module does not activate unless top-level
core.enableis also enabled. - Default is tuned for physical desktop or laptop systems where local or network printer access is expected.
lpgroup membership is granted through the sharedcore.defaultGroupshandling.
Remote Access — Optional remote desktop and game-streaming capabilities for desktop hosts
Purpose
Expose the host remotely through two independent sub-features: Remote Desktop (xrdp) for full desktop access over RDP, and Streaming (Sunshine) for low-latency game or desktop streaming.
Entry Point
- Main file: remote.nix
Options
core.remote.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable remote features.
core.remote.remoteDesktop
| Type | submodule |
| Default | { } |
This option has no description.
core.remote.remoteDesktop.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable remote desktop.
core.remote.remoteDesktop.startCommand
| Type | string |
| Default | "gnome-session" |
Command to start remote desktop session.
core.remote.streaming
| Type | submodule |
| Default | { } |
This option has no description.
core.remote.streaming.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable remote streaming.
Architecture / Services / Scope
| Sub-feature | Implementation | Purpose |
|---|---|---|
| Remote Desktop | xrdp | Full desktop access over RDP |
| Streaming | Sunshine | Low-latency game or desktop streaming |
When core.remote.enable = true:
remoteDesktop.enableturns onservices.xrdp, setsdefaultWindowManager, and opens firewall for RDP.streaming.enableturns onservices.sunshine, opens firewall for TCP 47989, and setscapSysAdmin = true.- Sunshine does not run continuously. A socket-activated TCP proxy on port 47989 starts Sunshine on-demand when a client connects.
- When the client disconnects and no new connection arrives for 5 minutes, the proxy exits and Sunshine stops.
- if Home Manager is present, streaming also persists
.config/sunshinethrough shared Home Manager module.
Hyprland Integration
When both core.remote.streaming.enable and programs.hyprland.enable are true, module additionally:
- sets
services.sunshine.settings.output_name, - adds two Sunshine application entries named
Shared DesktopandExclusive Desktop, - creates headless output at login via Home Manager, and
- keeps
HEADLESS-1disabled by default until Sunshine prep commands enable it.
Lua Mode Handling
If Home Manager Hyprland is in Lua mode (wayland.windowManager.hyprland.configType = "lua"), the shared module uses Lua-safe equivalents:
- Startup hook:
settings.onwithhyprland.startevent triggershyprctl output create headless. - Monitor rule:
{ output = "HEADLESS-1"; disabled = true; } - Screencopy permission: routed through
wayland.windowManager.hyprland.custom-settings.permission.screenCopy, which handles both Lua and hyprlang modes automatically.
| Application | Behaviour |
|---|---|
| Shared Desktop | Enables HEADLESS-1 at client resolution and leaves physical monitors active. |
| Exclusive Desktop | Enables HEADLESS-1, saves active physical monitor state to $XDG_STATE_HOME/hyprland-disabled-monitors-pre-sunshine.json, disables those monitors, then restores them on disconnect. |
The socket proxy and Hyprland monitor helpers are shipped together in the shared sunshine-tools package so Sunshine startup and prep/undo hooks reuse one tested implementation across hosts.
On-Demand Activation & Idle Stop
Sunshine stays on its standard port family rooted at TCP/UDP 47989–47990+ (no port-family offset).
External inbound TCP 47989 is firewall-redirected to internal proxy port 48989. The proxy wakes Sunshine and forwards to 127.0.0.1:47989.
| Stage | What happens |
|---|---|
| Firewall redirect | iptables NAT prerouting rule redirects inbound TCP :47989 to local :48989. A conntrack-based filter accept allows only redirected traffic into :48989; :48989 is not broadly exposed. |
| Socket activation | sunshine-proxy.socket listens on TCP :48989. First connection activates sunshine-proxy.service. |
| Proxy start | sunshine-proxy.service pulls in sunshine.service via systemd dependencies, then the packaged sunshine-proxy-wrapper helper polls until port 47989 is open and exec-s into systemd-socket-proxyd forwarding to 127.0.0.1:47989. |
| Active streaming | Sunshine handles Moonlight/Sunshine client traffic on its standard port family. The proxy relays only the initial control connection transparently. Proxy bindsTo Sunshine — if Sunshine crashes proxy goes with it. |
| Idle stop | After 300s with no connection, systemd-socket-proxyd exits. Sunshine has Restart=no and StopWhenUnneeded=true with no remaining active referrer, so systemd stops it. |
Dependency Model
sunshine-proxy.socket (:48989)
↓ activates
sunshine-proxy.service ──bindsTo──→ sunshine.service (:47989)
No cycle: proxy starts Sunshine, proxy bindsTo Sunshine (proxy dies if Sunshine fails), Sunshine uses StopWhenUnneeded (stops when proxy exits).
Firewall Flow
External client → TCP :47989
↓ (NAT PREROUTING REDIRECT)
Local port :48989
↓ (conntrack ctorigdstport 47989 match → nixos-fw-accept)
sunshine-proxy.socket
↓
sunshine-proxy.service → sunshine.service (:47989)
Redirect covers only inbound network traffic. Locally-originated traffic to :47989 (e.g. from Moonlight running on the same machine) is unaffected.
Operational Notes / Assumptions
- Two sub-features are independent. You can enable streaming without RDP, or RDP without streaming.
- Sunshine persistence and Hyprland settings are only added when Home Manager is present in system configuration.
- Hyprland-specific Sunshine application entries are only added when both streaming and Hyprland are enabled.
Caveats
- No LAN discovery while idle. Sunshine needs to run for mDNS/SSDP advertisements to appear on LAN. While stopped (idle), clients will not auto-discover the host. Users must add the host manually by IP/hostname in Moonlight or use a previously-added host entry (Moonlight remembers known hosts).
- TCP wake only. This proxy covers the control/initial TCP connection on 47989. Sunshine’s UDP audio/video streams (standard port range) will only work after Sunshine runs. Since Sunshine sets up its UDP sockets itself after startup, no UDP wake is needed in practice (the rendezvous happens over TCP first).
- Delayed first connect. The first TCP connection may stall ~1–2 seconds while Sunshine starts up. Clients (Moonlight) retry or timeout gracefully.
- Firewall. Sunshine opens its standard ports via
openFirewall. The redirect only touches TCP 47989 for the wake path. No port-family offset anymore — all media/data ports remain at standard values. - Redirect scope. The firewall redirect applies to inbound network traffic only. Local loopback connections to
:47989bypass the redirect and reach Sunshine directly if it is already running.
Security — Shared host security defaults
Purpose
Enable baseline host security: sudo-rs, TPM2 support, Polkit, kernel protection flags, and open-file limits for users.
Entry Point
- Main file: security.nix
Architecture / Services / Scope
When enabled, module:
- enables
sudo-rsin place of sudo, restricted to the wheel group, - enables TPM2 and Polkit,
- enables kernel image protection while leaving
lockKernelModulesoff, - sets PAM and user systemd service open-file limits from
core.security.userLimit, and - raises
fs.file-maxto a multiple ofuserLimit.
Operational Notes / Assumptions
- Module leaves
security.lockKernelModules = falseeven while enabling other hardening defaults. userLimitaffects both PAM sessions and user systemd services, keeping file descriptor limits aligned.
SOPS — Shared SOPS and age decryption defaults
Purpose
Provide a shared baseline for SOPS-managed secrets on every host: point sops-nix at the host’s secrets file and teach age to use the host’s provisioned SSH keys.
Entry Point
- Main file: sops.nix
Architecture / Services / Scope
When enabled, module:
- imports
sops-nix(skipped when function argumentimportExternals = false), - sets
sops.defaultSopsFiletocore.sops.hostSecretsFile, - builds
sops.age.sshKeyPathsfromcore.openssh.hostPrivateKeyPathfirst, then appends configured ed25519 OpenSSH host keys without duplicating the canonical path.
Operational Notes / Assumptions
- Default age key path is
core.openssh.hostPrivateKeyPath. - Only ed25519 entries from
config.services.openssh.hostKeysare appended to the age key paths. - Secrets file defaults to
secrets.yamlinside the host directory, overridable viacore.sops.hostSecretsFile.
Stylix — Shared system theme defaults via Stylix
Purpose
Apply a consistent dark theme across graphical hosts by importing Stylix and enabling dark Tokyo Night theming by default.
Entry Point
- Main file: stylix.nix
Architecture / Services / Scope
When enabled, module:
- imports
stylix(skipped when function argumentimportExternals = false), - enables Stylix with dark polarity, and
- selects the Tokyo Night dark Base16 scheme from the
tinted-schemesinput.
Operational Notes / Assumptions
- Intended for graphical (non-headless) hosts; enabled by default there.
- Theme source comes from the
tinted-schemesinput.
Virtualisation — libvirt, VFIO passthrough, bridge networking, and guest isolation helpers
Purpose
Enable libvirt/QEMU virtualisation with VFIO GPU passthrough, Looking Glass shared memory, bridge networking, custom OVMF firmware metadata, and per-guest isolation helpers that reserve host CPUs, detach GPUs, and block host sleep while guests run.
Entry Point
- Main file: virtualisation.nix
Architecture / Services / Scope
When enabled, module:
- imports external virtualisation helpers from
crtified.modules.virtualisation.nixand../desktop/vfio.nix, - enables
virtualisation.libvirtd, Spice USB redirection, andservices.spice-autorandr, - enables VFIO with AMD IOMMU,
disableEFIfb, and the configured GPU devices, - configures Looking Glass shared memory file owned by the libvirt group,
- adds
virt-manager,virtiofsd,virtio-win, andwin-spiceto system packages, - sets
LIBVIRT_DEFAULT_URItoqemu:///system, - creates bridge networking with DHCP on
bridgeInterfaceandexternalInterfaceenslaved into the bridge, - adds
kvmfrkernel module package and modprobe config, - installs udev rule for
/dev/kvmfraccess, and - persists libvirt and swtpm state under
host.persistence.directories.
Isolation and Hook Helpers
For each guest in core.virtualisation.isolatedGuests, module creates libvirt hook entries backed by the shared virtualisation-tools package that:
- restrict host
user.slice,system.slice, andinit.scopeCPU sets during guest startup, - restore full CPU set when guest stops,
- for
<guest>-single, detach GPU and stop display-related services before launch, and - reattach GPU, reload drivers, restart saved services, and rebind VT consoles after shutdown.
It also creates libvirt-nosleep@<guest> service that uses systemd-inhibit to block sleep while guest is running.
Guest-specific CPU ranges are passed through tiny generated wrappers, while shared hook logic stays in the packaged helpers.
Firmware and Persistence
Module extends libvirt startup to populate /run/libvirt/nix-ovmf with secure-boot and Microsoft-enrolled OVMF firmware files, then publishes matching firmware JSON metadata under /var/lib/qemu/firmware.
Persisted paths include:
/var/lib/libvirt/qemu/var/lib/libvirt/images/var/lib/libvirt/swtpm/var/lib/libvirt/secrets/var/lib/swtpm-localca
Operational Notes / Assumptions
core.virtualisation.cpuCoresis validated by both option type and assertion, so values below4fail evaluation.vmUsersis opt-in. Only listed users receivekvmandlibvirtdaccess.- Hook generation assumes guest naming convention where
<name>-singlemeans single-GPU passthrough workflow.
WSL — Windows Subsystem for Linux integration and fixes
Purpose
Make the NixOS host behave correctly inside WSL: default user, Windows interop, graphics library paths, nix-ld, and Start Menu launcher syncing.
Entry Point
- Main file: wsl.nix
Architecture / Services / Scope
Base layer (always applied when enabled):
- allows passwordless login,
- installs
wslu, - enables
nix-ldwith a C toolchain library for VS Code Remote WSL compatibility, - sets session variables for WSL graphics and library paths,
- enables
hardware.graphicswith the configured graphics packages andlibvdpau-va-gl, and - when NVIDIA graphics are present, appends CUDA and NVIDIA library paths.
If the wsl module exists in the option tree, module additionally:
- enables WSL with
core.wsl.useras default user, - enables Start Menu launchers and Windows driver usage,
- enables Windows interop and PATH appending,
- exposes
dirname,readlink, andunamethroughwsl.extraBinfor VS Code Remote WSL compatibility, and - copies per-user Home Manager
applicationsandiconsinto/usr/shareduring activation so launchers appear in the Windows Start Menu.
Operational Notes / Assumptions
core.wsl.useris required when WSL integration is enabled.- Extra binaries
dirname,readlink, andunameare exposed for VS Code Remote WSL compatibility. - Behavior is conditional on the separate
wslmodule being available inoptions.
Server Module
The Server module provides a cluster-aware configuration for server hosts in the flake. It must be explicitly enabled using the server.enable option.
Purpose
The primary purpose of this module is to establish a shared environment for servers in the cluster, defining coordinator nodes for discrete roles (IO, monitoring, database, storage, identity) and providing helper functions for inter-server communication and attribute collection.
Entry Point
- Main file:
modules/nixos/server/default.nix
Options
server.dashboard.displayData
| Type | JSON value |
| Default | { } |
Display data for the section in the dashboard.
server.dashboard.icon
| Type | null or string |
| Default | null |
Icon for the section in the dashboard.
server.dashboard.items
| Type | attribute set of (submodule) |
Additional configuration for items managed by the IO Hosts dashy instance. This will be merged with the automatically generated configuration that is nested in a section with the name of the machine.
server.dashboard.items.<name>.icon
| Type | string |
Icon for the item.
server.dashboard.items.<name>.title
| Type | string |
Title of the item.
server.dashboard.items.<name>.url
| Type | string |
URL for the item.
server.dashboard.name
| Type | string |
| Default | let withoutPrefix = removePrefix "nix" config.host.name; nixPrefixed = builtins.stringLength withoutPrefix < builtins.stringLength config.host.name; in if nixPrefixed then "Nix${lib.mine.strings.capitalise withoutPrefix}" else lib.capitalize config.host.name; |
Name of the section in the dashboard.
server.database.dependentServices
| Type | list of string |
| Default | [ ] |
List of systemd service names that depend on io databases. These services will be automatically bound to the io-databases.target and will stop/start when databases become unavailable/available.
server.database.host
| Type | string |
| Default | if isThisIOPrimaryHost then "localhost" else config.server.ioPrimaryHost |
The hostname or IP address to use when connecting to managed databases.
This is “localhost” when running on the host,
and
server.database.postgres
| Type | attribute set of (submodule) |
| Default | { } |
This option has no description.
server.database.postgres.<name>.database
| Type | string |
| Default | "‹name›" |
This option has no description.
server.database.postgres.<name>.host
| Type | string |
| Default | config.server.database.host |
This option has no description.
server.database.postgres.<name>.password
| Type | submodule |
| Default | { } |
This option has no description.
server.database.postgres.<name>.password.group
| Type | null or string |
| Default | null |
This option has no description.
server.database.postgres.<name>.password.owner
| Type | null or string |
| Default | null |
This option has no description.
server.database.postgres.<name>.password.path
| Type | absolute path |
| Default | config.sops.secrets."POSTGRES/${ toUpper config.server.database.postgres.${name}.database |> builtins.replaceStrings [ "-" ] [ "_" ] }_PASSWORD".path; |
This option has no description.
server.database.postgres.<name>.port
| Type | signed integer |
| Default | config.server.database.postgres.‹name›.port |
This option has no description.
server.database.postgres.<name>.user
| Type | string |
| Default | "‹name›" |
This option has no description.
server.database.redis
| Type | attribute set of (submodule) |
| Default | { } |
This option has no description.
server.database.redis.<name>.database_id
| Type | signed integer |
| Default | staticDbIdMappings.‹name› or (-1) |
This option has no description.
server.database.redis.<name>.host
| Type | string |
| Default | config.server.database.host |
This option has no description.
server.database.redis.<name>.port
| Type | signed integer |
| Default | (getIOPrimaryHostAttr "services.redis.servers")."".port |
This option has no description.
server.database.redis.<name>.prefix
| Type | string |
| Default | "‹name›" |
This option has no description.
server.distributedBuilds.builderUser
| Type | string |
| Default | "builder" |
The user to use when connecting to remote build daemons.
server.distributedBuilds.builders
| Type | list of string |
| Default | [ ] |
A list of hostnames of remote build daemons to connect to for distributed builds.
server.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable enable the server module.
server.fail2ban.enable
| Type | boolean |
| Default | isThisIOPrimaryHost && config.services.caddy.enable |
| Example | true |
Whether to enable fail2ban intrusion detection.
server.fail2ban.exporterPort
| Type | 16 bit unsigned integer; between 0 and 65535 (both inclusive) |
| Default | 9191 |
Port for the fail2ban Prometheus exporter.
server.ioPrimaryHost
| Type | null or string |
| Default | null |
Which host is the primary coordinator for IO in the cluster.
This host will run the primary instances of databases, Operate the reverse proxy for handling incoming traffic, and will run the MinIO distributed storage cluster’s master node.
server.monitoring.collector.alerting.enable
| Type | boolean |
| Default | cfg.enable |
| Example | true |
Whether to enable Alertmanager and alert rules.
server.monitoring.collector.alerting.homeAssistant.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable Home Assistant webhook alerting.
server.monitoring.collector.alerting.nextcloudTalk.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable Nextcloud Talk webhook alerting.
server.monitoring.collector.enable
| Type | boolean |
| Default | thisIsMonitoringPrimaryHost && cfg.enable |
| Example | true |
Whether to enable monitoring collector services (Prometheus, Loki, Grafana).
server.monitoring.collector.grafana.kanidm.enable
| Type | boolean |
| Default | true |
| Example | true |
Whether to enable Kanidm OAuth2 authentication for Grafana.
server.monitoring.collector.otlp.bearerTokenSecret
| Type | string |
| Default | "MONITORING/OLTP/BEARER_TOKEN" |
SOPS secret path used as the bearer token for OTLP/HTTP ingestion.
server.monitoring.collector.otlp.enable
| Type | boolean |
| Default | isThisMonitoringPrimaryHost && cfg.enable |
| Example | true |
Whether to enable OTLP/HTTP ingestion via Grafana Alloy.
server.monitoring.collector.otlp.port
| Type | signed integer |
| Default | 4318 |
Port for the OTLP/HTTP ingestion endpoint.
server.monitoring.collector.otlp.subdomain
| Type | string |
| Default | "otlp" |
Subdomain used for the OTLP/HTTP ingestion endpoint.
server.monitoring.collector.proxmox.enable
| Type | boolean |
| Default | isThisMonitoringPrimaryHost && cfg.enable |
| Example | true |
Whether to enable Proxmox VE metrics collection.
server.monitoring.enable
| Type | boolean |
| Default | true |
| Example | true |
Whether to enable monitoring for this server.
server.monitoring.exporters.caddy.enable
| Type | boolean |
| Default | cfg.enable && config.services.caddy.enable |
| Example | true |
Whether to enable Caddy metrics exporter.
server.monitoring.exporters.fail2ban.enable
| Type | boolean |
| Default | cfg.enable && isThisIOPrimaryHost && config.server.fail2ban.enable |
| Example | true |
Whether to enable fail2ban metrics exporter.
server.monitoring.exporters.node.enable
| Type | boolean |
| Default | cfg.enable |
| Example | true |
Whether to enable node_exporter for system-level metrics.
server.monitoring.exporters.postgres.enable
| Type | boolean |
| Default | cfg.enable && thisIsIOPrimaryHost && hasPostgresDatabases |
| Example | true |
Whether to enable PostgreSQL exporter.
server.monitoring.exporters.process.enable
| Type | boolean |
| Default | cfg.enable |
| Example | true |
Whether to enable Process exporter for monitoring specific processes.
server.monitoring.exporters.redis.enable
| Type | boolean |
| Default | cfg.enable && thisIsIOPrimaryHost && hasRedisInstances |
| Example | true |
Whether to enable Redis exporter.
server.monitoring.logs.enable
| Type | boolean |
| Default | cfg.enable |
| Example | true |
Whether to enable Alloy log shipping.
server.monitoring.logs.extraConfiguration
| Type | strings concatenated with "\n" |
| Default | "" |
Additional configuration for the alloy log processor. This is useful for adding custom Loki stages, relabeling rules, or write targets.
Note that the default configuration for processing the system journal is always included and does not need to be specified here.
server.monitoring.retention.logs
| Type | string |
| Default | "90d" |
Loki log retention period.
server.monitoring.retention.metrics
| Type | string |
| Default | "90d" |
Prometheus TSDB retention period.
server.monitoring.scrapeConfigs
| Type | attribute set of (submodule) |
| Default | { } |
Declarative scrape configs for services running on this host. These are collected by the monitoring primary host and converted into Prometheus scrape configurations.
server.monitoring.scrapeConfigs.<name>.bearer_token_secret
| Type | null or string |
| Default | null |
SOPS secret path for bearer token authentication. When set, the secret will be created on the monitoring primary host.
server.monitoring.scrapeConfigs.<name>.host
| Type | string |
| Default | config.host.name |
Host to scrape metrics from.
server.monitoring.scrapeConfigs.<name>.job_name
| Type | string |
| Default | "‹name›" |
Prometheus job name for this scrape target.
server.monitoring.scrapeConfigs.<name>.metrics_path
| Type | string |
| Default | "/metrics" |
HTTP path to the metrics endpoint.
server.monitoring.scrapeConfigs.<name>.port
| Type | signed integer |
Port the metrics endpoint listens on.
server.monitoring.scrapeConfigs.<name>.scheme
| Type | one of "http", "https" |
| Default | "http" |
URL scheme for scraping.
server.monitoringPrimaryHost
| Type | null or string |
| Default | null |
Which host is the primary collector for monitoring in the cluster.
This host will run Prometheus, Loki, Grafana, and Alertmanager for centralized observability of the entire server cluster.
server.network.openPortsForSubnet.tcp
| Type | list of 16 bit unsigned integer; between 0 and 65535 (both inclusive) |
| Default | [ ] |
List of TCP ports to open on the firewall for each subnet.
server.network.openPortsForSubnet.udp
| Type | list of 16 bit unsigned integer; between 0 and 65535 (both inclusive) |
| Default | [ ] |
List of UDP ports to open on the firewall for each subnet.
server.network.subnets
| Type | list of (submodule) |
| Default | { } |
This option has no description.
server.network.subnets.*.dns
| Type | string |
DNS server for the subnet.
server.network.subnets.*.domain
| Type | string |
Domain name for the subnet.
server.network.subnets.*.ipv4
| Type | submodule |
| Default | { } |
IPv4 configuration for the subnet.
server.network.subnets.*.ipv4.arpa
| Type | null or string |
| Default | null |
ARPA notation for reverse DNS lookups.
server.network.subnets.*.ipv4.cidr
| Type | null or string |
| Default | null |
CIDR notation for the IP range.
server.network.subnets.*.ipv6
| Type | submodule |
| Default | { } |
IPv6 configuration for the subnet.
server.network.subnets.*.ipv6.arpa
| Type | null or string |
| Default | null |
ARPA notation for reverse DNS lookups.
server.network.subnets.*.ipv6.cidr
| Type | null or string |
| Default | null |
CIDR notation for the IP range.
server.proxy.domain
| Type | string |
The base domain for all virtual hosts.
server.proxy.extensions
| Type | attribute set of (submodule) |
| Default | { } |
Registry of proxy extensions. Each extension provides config functions that are injected into vhost Caddy blocks, sorted by priority.
server.proxy.extensions.<name>.config
| Type | function that evaluates to a(n) function that evaluates to a(n) function that evaluates to a(n) string |
Function: vhostName -> vhostAttrSet -> hostConfig -> string. Returns Caddy directives to inject, or ‘’ for no-op. The vhostAttrSet includes the resolved extraConfig (already localhost-replaced) as _resolvedExtraConfig.
server.proxy.extensions.<name>.consumesExtraConfig
| Type | boolean |
| Default | false |
Whether this extension embeds extraConfig inside its output. When true, config.nix skips the post-extension extraConfig append for this vhost.
server.proxy.extensions.<name>.enable
| Type | boolean |
| Default | false |
Whether this extension is globally enabled.
Each extension SHOULD auto-detect whether it has work to do and set this to true via mkDefault in its module config.
User can explicitly override to force-disable (higher merge priority than mkDefault).
server.proxy.extensions.<name>.globalConfig
| Type | function that evaluates to a(n) string |
| Default | <function> |
Function: hostConfig -> string. Returns Caddy directives to inject into the top-level globalConfig block. Only called on the IO primary host. Sorted by priority across extensions.
server.proxy.extensions.<name>.priority
| Type | signed integer |
| Default | 100 |
Lower values = earlier in Caddy config. Priority ranges: 0-49 reserved, 50-99 auth, 100-199 general, 200+ post-processing.
server.proxy.extensions.<name>.vhostModule
| Type | null or module |
| Default | null |
Optional module to inject into each vhost submodule. Use options.<extensionName> (relative to vhost scope) to declare per-vhost options.
server.proxy.kanidmContexts
| Type | attribute set of (submodule) |
| Default | { } |
Shared Kanidm OAuth2 context configurations.
server.proxy.kanidmContexts.<name>.allowGroups
| Type | list of string |
| Default | [ ] |
| Example | [ "idm_all_persons@auth.racci.dev" "admins@auth.racci.dev" ] |
Default list of Kanidm groups allowed to access virtualHosts using this context.
server.proxy.kanidmContexts.<name>.authDomain
| Type | null or string |
| Default | null |
| Example | "auth.example.com" |
The domain where Kanidm is hosted. Defaults to auth.<server.proxy.domain> if not specified.
server.proxy.kanidmContexts.<name>.scopes
| Type | list of string |
| Default | [ "openid" "email" "profile" "groups" ] |
OAuth scopes to request from Kanidm.
server.proxy.kanidmContexts.<name>.tokenLifetime
| Type | signed integer |
| Default | 3600 |
Token lifetime in seconds for the authentication portal.
server.proxy.virtualHosts
| Type | attribute set of (submodule) |
| Default | { } |
Virtual hosts to be handled by the IO server and forwarded to the respective backend.
server.proxy.virtualHosts.<name>.aliases
| Type | list of string |
| Default | [ ] |
A list of virtual host names that should be routed using this configuration. Options added here will inherit the base domain specified in <server.proxy.domain>.
server.proxy.virtualHosts.<name>.baseUrl
| Type | string |
| Default | ${subdomain}.${getIOPrimaryHostAttr "server.proxy.domain"} |
The base url including the configured base domain name.
server.proxy.virtualHosts.<name>.extensions
| Type | null or (list of string) |
| Default | null |
List of extension names to enable for this virtual host. When null (default), all globally enabled extensions apply. When set to a list, only those named extensions apply. Set to [] to disable all extensions for this vhost.
server.proxy.virtualHosts.<name>.extraConfig
| Type | string |
| Default | "" |
Configuration to be placed in the caddy virtualHost extraConfig.
server.proxy.virtualHosts.<name>.kanidm
| Type | null or (submodule) |
| Default | null |
Enable Kanidm OAuth2 authentication for this virtual host.
server.proxy.virtualHosts.<name>.kanidm.allowGroups
| Type | list of string |
| Default | [ ] |
| Example | [ "idm_all_persons@auth.racci.dev" "admins@auth.racci.dev" ] |
Default list of Kanidm groups allowed to access virtualHosts using this context.
server.proxy.virtualHosts.<name>.kanidm.authDomain
| Type | null or string |
| Default | null |
| Example | "auth.example.com" |
The domain where Kanidm is hosted. Defaults to auth.<server.proxy.domain> if not specified.
server.proxy.virtualHosts.<name>.kanidm.bypassPaths
| Type | list of string |
| Default | [ ] |
| Example | [ "/health" "/api/webhooks/*" ] |
List of path patterns that should bypass authentication.
server.proxy.virtualHosts.<name>.kanidm.context
| Type | string |
| Default | "‹name›" |
The OAuth context name for this virtual host.
server.proxy.virtualHosts.<name>.kanidm.scopes
| Type | list of string |
| Default | [ "openid" "email" "profile" "groups" ] |
OAuth scopes to request from Kanidm.
server.proxy.virtualHosts.<name>.kanidm.tokenLifetime
| Type | signed integer |
| Default | 3600 |
Token lifetime in seconds for the authentication portal.
server.proxy.virtualHosts.<name>.l4
| Type | null or (submodule) |
| Default | null |
This option has no description.
server.proxy.virtualHosts.<name>.l4.config
| Type | string |
| Default | "" |
Configuration for the L4 plugin.
server.proxy.virtualHosts.<name>.l4.listenPort
| Type | 16 bit unsigned integer; between 0 and 65535 (both inclusive) |
Port to listen on for L4 traffic.
server.proxy.virtualHosts.<name>.l4.protocol
| Type | one of "tcp", "udp" |
| Default | "tcp" |
Protocol for L4 listener.
server.proxy.virtualHosts.<name>.listenPorts
| Type | non-empty (list of 16 bit unsigned integer; between 0 and 65535 (both inclusive)) |
| Default | [ 443 ] |
Port(s) to listen on for incoming traffic for this virtual host. If multiple ports are specified, the virtual host will be accessible on all of them.
server.proxy.virtualHosts.<name>.ports
| Type | list of 16 bit unsigned integer; between 0 and 65535 (both inclusive) |
| Default | [ ] |
Ports to be opened from the host for IO Hosts to forward traffic to.
server.proxy.virtualHosts.<name>.public
| Type | boolean |
| Default | false |
When enabled this service will be accessible to the public via Cloudflared Tunnels.
server.proxy.virtualHosts.<name>.requireApiKey
| Type | null or (submodule) |
| Default | null |
This option has no description.
server.proxy.virtualHosts.<name>.requireApiKey.bypassPaths
| Type | list of string |
| Default | [ ] |
| Example | [ "/health" "/api/webhooks/*" ] |
List of path patterns that bypass API key authentication.
server.proxy.virtualHosts.<name>.requireApiKey.enable
| Type | boolean |
| Default | false |
Enable API key authentication for this virtual host.
server.proxy.virtualHosts.<name>.useAcmeCerts
| Type | boolean |
| Default | true |
Whether to generate and use ACME certificates for this virtual host. If false, you must provide your own TLS configuration in extraConfig via the caddy tls directive.
server.sshShell.enable
| Type | boolean |
| Default | true |
| Example | true |
Whether to enable Auto-enter a session-only devShell for root on interactive SSH logins..
server.sshShell.shellFile
| Type | absolute path |
| Default | /nix/store/jq8636fkq2anq8f33kfqa816d0nrqw4m-source/modules/nixos/server/ssh-shell/shell.nix |
Path to a single-file that defines a session-only environment.
This file is evaluated by nix-shell and should import
server.storage.swfsMount
| Type | attribute set of (submodule) |
| Default | { } |
Declarative storage mounts backed by MinIO or SeaweedFS.
Each entry creates a systemd-managed FUSE mount service plus an optional health-check timer.
server.storage.swfsMount.<name>.backend
| Type | one of "minio", "seaweedfs" |
The storage backend to mount.
server.storage.swfsMount.<name>.gid
| Type | null or signed integer |
| Default | null |
Group ID that should own the mounted path.
server.storage.swfsMount.<name>.healthCheck.enable
| Type | boolean |
| Default | true |
Whether to monitor this mount and attempt automated recovery.
server.storage.swfsMount.<name>.healthCheck.interval
| Type | string |
| Default | "15min" |
Systemd timer interval between mount health probes.
server.storage.swfsMount.<name>.healthCheck.reloadServices
| Type | list of string |
| Default | [ ] |
Additional systemd services to reload after recovering this mount
server.storage.swfsMount.<name>.healthCheck.restartServices
| Type | list of string |
| Default | [ ] |
Additional systemd services to restart after recovering this mount.
server.storage.swfsMount.<name>.healthCheck.timeout
| Type | string |
| Default | "30s" |
Timeout applied to the mount health probe.
server.storage.swfsMount.<name>.minio.bucketName
| Type | string |
| Default | "‹name›" |
The MinIO bucket to mount with s3fs.
server.storage.swfsMount.<name>.minio.credentialsFile
| Type | null or string |
| Default | null |
| Example | "/run/secrets/s3fs-credentials" |
Path to the MinIO credentials file in ACCESS_KEY_ID:SECRET_ACCESS_KEY format.
When left null, the module provisions and uses the S3FS_AUTH/<NAME_IN_UPPERCASE> sops secret.
server.storage.swfsMount.<name>.minio.endpoint
| Type | string |
| Default | "https://minio.racci.dev" |
The S3-compatible MinIO endpoint used by s3fs.
server.storage.swfsMount.<name>.minio.extraOptions
| Type | list of string |
| Default | [ ] |
Additional -o options passed to s3fs.
server.storage.swfsMount.<name>.mountLocation
| Type | string |
| Default | "/mnt/storage/${name}" |
Path where the backend should be mounted.
server.storage.swfsMount.<name>.requiredByServices
| Type | list of string |
| Default | [ ] |
Systemd services that must wait for this mount before starting.
server.storage.swfsMount.<name>.seaweedfs.allowOthers
| Type | boolean |
| Default | true |
Whether to allow non-owning users to access the SeaweedFS mount.
server.storage.swfsMount.<name>.seaweedfs.dirAutoCreate
| Type | boolean |
| Default | true |
Whether weed mount should create the mount directory when needed.
server.storage.swfsMount.<name>.seaweedfs.extraArgs
| Type | list of string |
| Default | [ ] |
Additional arguments passed directly to weed mount.
server.storage.swfsMount.<name>.seaweedfs.filer
| Type | string |
| Default | "" |
SeaweedFS filer address in host:port form.
server.storage.swfsMount.<name>.seaweedfs.filerPath
| Type | string |
| Default | "/" |
Remote filer path to expose through the mount.
server.storage.swfsMount.<name>.seaweedfs.gidMap
| Type | null or string |
| Default | null |
Optional local-to-filer GID mapping string for weed mount.
server.storage.swfsMount.<name>.seaweedfs.metadataFlushSeconds
| Type | signed integer |
| Default | 120 |
How often weed mount flushes metadata to the filer.
server.storage.swfsMount.<name>.seaweedfs.readOnly
| Type | boolean |
| Default | false |
Whether the SeaweedFS mount should be read-only.
server.storage.swfsMount.<name>.seaweedfs.uidMap
| Type | null or string |
| Default | null |
Optional local-to-filer UID mapping string for weed mount.
server.storage.swfsMount.<name>.seaweedfs.writeBufferSizeMB
| Type | null or signed integer |
| Default | null |
Optional write buffer cap passed to weed mount in megabytes.
server.storage.swfsMount.<name>.uid
| Type | null or signed integer |
| Default | null |
User ID that should own the mounted path.
server.storage.swfsMount.<name>.umask
| Type | signed integer |
| Default | 22 |
Umask applied to files and directories inside the mount.
Architecture / Services / Scope
Special Options and Behaviors
The main configuration entry point is server.enable. Once enabled, it sets up the server-specific baseline:
- Journald Persistence: Configured with a 7-day retention period, 256MB total max disk usage, and 512MB keep-free threshold. Per-file size is set to 32MB (1/8 of max use) to allow proper log rotation with ~7 archived files. All limits are defined as
letvariables in the module for consistency between the daemon config and the activation vacuum script. The activation script runsjournalctl --vacuumon every deploy to immediately enforce the limits on existing logs. - Pre-Switch Checks: Runs
dixon system activation to report changes between generations. server.ioPrimaryHost: Specifies the hostname of the IO Coordinator. This host operates the reverse proxy for handling incoming traffic and manages IO-level coordination. This option is typically set on the coordinator host and used by other servers in the cluster for synchronization.server.monitoringPrimaryHost: Specifies the hostname of the Monitoring Coordinator. This host runs Prometheus, Loki, Grafana, and Alertmanager for centralized observability across the cluster.server.databasePrimaryHost: Specifies the hostname of the Database Coordinator. This host runs primary database instances for centralized data persistence across the cluster.server.storagePrimaryHost: Specifies the hostname of the Storage Coordinator. This host runs primary file and block storage services for centralized data serving across the cluster.server.authPrimaryHost: Specifies the hostname of the Identity Coordinator. This host runs primary authentication and authorization services for centralized identity management across the cluster.
Example Usage
To use the server module, it must be explicitly enabled in the host configuration.
# hosts/server/nixmon/default.nix
{
server = {
enable = true;
# Set to the hostname of the cluster's coordinator node
ioPrimaryHost = "nixio";
};
}
Operational Notes / Assumptions
Generic Primary-Host Helpers
Generic helpers parameterized by any primary-host option value. These are used by submodules to check role assignment and fetch remote configuration:
isPrimaryHost primaryHost value: ReturnstrueifvaluematchesprimaryHost. Accepts either a raw hostname string or an attrset with ahost.nameattribute.isThisPrimaryHost primaryHost: Shorthand forisPrimaryHost primaryHost config— checks if the current host is the primary for a given role.getPrimaryHostConfig primaryHost: Returns the NixOS configuration of the host designated as the primary for a given role. On the primary host itself this returnsconfiglocally; on other hosts it fetches the remote configuration viaself.nixosConfigurations.getPrimaryHostAttr primaryHost attrPath: Retrieves a specific attribute (expressed as a dot-separated path) from the primary host’s configuration.getOthersWhereExcept primaryHost func: Returns a list of server hostnames (excluding the given primary host) wherefuncreturnstrue. Useful for discovering non-primary nodes that match certain criteria.
Backward-Compatible IO Helpers
The existing IO-specific helpers (isIOPrimaryHost, isThisIOPrimaryHost, primaryIOHostConfig, getIOPrimaryHostAttr, getOthersWhere) remain available and now delegate to the generic helpers. They behave identically but are hard-wired to server.ioPrimaryHost. New submodules should prefer the generic helpers for role-agnostic code.
Server Attribute Collection
- This module provides many helper functions (like
getAllAttrsFunc,collectAllAttrs, etc.) that are used by submodules to gather configuration data from other servers in the cluster. - These helpers allow for dynamic configuration based on the state of other cluster nodes, such as building a global dashboard or a reverse proxy configuration.
- The IO Coordinator is a critical component of the cluster, as many services (like Dashy or shared ingress) rely on it as the central point of coordination.
References
Server Dashboard — Cluster-Wide Service Dashboard
Purpose
The dashboard module integrates with Dashy and collects dashboard sections from all servers in the cluster to display on the IO Coordinator (server.ioPrimaryHost). This allows each server to define its own dashboard items, which are then automatically collected and displayed on a single unified dashboard.
Entry Point
- Main file:
modules/nixos/server/dashboard.nix
Options
server.dashboard.displayData
| Type | JSON value |
| Default | { } |
Display data for the section in the dashboard.
server.dashboard.icon
| Type | null or string |
| Default | null |
Icon for the section in the dashboard.
server.dashboard.items
| Type | attribute set of (submodule) |
Additional configuration for items managed by the IO Hosts dashy instance. This will be merged with the automatically generated configuration that is nested in a section with the name of the machine.
server.dashboard.items.<name>.icon
| Type | string |
Icon for the item.
server.dashboard.items.<name>.title
| Type | string |
Title of the item.
server.dashboard.items.<name>.url
| Type | string |
URL for the item.
server.dashboard.name
| Type | string |
| Default | let withoutPrefix = removePrefix "nix" config.host.name; nixPrefixed = builtins.stringLength withoutPrefix < builtins.stringLength config.host.name; in if nixPrefixed then "Nix${lib.mine.strings.capitalise withoutPrefix}" else lib.capitalize config.host.name; |
Name of the section in the dashboard.
Architecture / Services / Scope
- This module uses
getAllAttrsFuncto gatherserver.dashboardconfigurations from all servers in the cluster. - The aggregated configuration is only applied to the IO Coordinator (
server.ioPrimaryHost), which runs the primary Dashy instance.
Operational Notes / Assumptions
- Each server defines its own
server.dashboardsection (name, icon, and items) in its host configuration; the items are aggregated cluster-wide.
References
Server Network — Centralized Subnets and Firewall
Purpose
The network module coordinates network subnet definitions and firewall rules, allowing for centralized configuration of subnets and automatic propagation of these settings to other servers in the cluster.
Entry Point
- Main file:
modules/nixos/server/network.nix
Options
server.network.openPortsForSubnet.tcp
| Type | list of 16 bit unsigned integer; between 0 and 65535 (both inclusive) |
| Default | [ ] |
List of TCP ports to open on the firewall for each subnet.
server.network.openPortsForSubnet.udp
| Type | list of 16 bit unsigned integer; between 0 and 65535 (both inclusive) |
| Default | [ ] |
List of UDP ports to open on the firewall for each subnet.
server.network.subnets
| Type | list of (submodule) |
| Default | { } |
This option has no description.
server.network.subnets.*.dns
| Type | string |
DNS server for the subnet.
server.network.subnets.*.domain
| Type | string |
Domain name for the subnet.
server.network.subnets.*.ipv4
| Type | submodule |
| Default | { } |
IPv4 configuration for the subnet.
server.network.subnets.*.ipv4.arpa
| Type | null or string |
| Default | null |
ARPA notation for reverse DNS lookups.
server.network.subnets.*.ipv4.cidr
| Type | null or string |
| Default | null |
CIDR notation for the IP range.
server.network.subnets.*.ipv6
| Type | submodule |
| Default | { } |
IPv6 configuration for the subnet.
server.network.subnets.*.ipv6.arpa
| Type | null or string |
| Default | null |
ARPA notation for reverse DNS lookups.
server.network.subnets.*.ipv6.cidr
| Type | null or string |
| Default | null |
CIDR notation for the IP range.
Architecture / Services / Scope
- This module uses
getIOPrimaryHostAttrto fetch theserver.network.subnetsconfiguration from the IO Coordinator (server.ioPrimaryHost), ensuring all servers in the cluster are aware of the network structure defined there. - The module automatically generates
iptablesandip6tablesrules for the specified ports, allowing traffic only from the defined subnets. - These rules are added to the
nixos-fwchain and are managed through thenetworking.firewall.extraCommandsandnetworking.firewall.extraStopCommandsoptions.
Operational Notes / Assumptions
- Subnets and per-subnet open ports are declared on the IO Coordinator via
server.network.subnetsandserver.network.openPortsForSubnet.
References
Server Distributed Builds — Remote Nix Build Distribution
Purpose
The distributed builds module allows for distributed building of Nix derivations using remote build machines, providing a coordinator host and several build machines to distribute the build load across the server cluster.
Entry Point
- Main file:
modules/nixos/server/distributed-builds.nix
Options
server.distributedBuilds.builderUser
| Type | string |
| Default | "builder" |
The user to use when connecting to remote build daemons.
server.distributedBuilds.builders
| Type | list of string |
| Default | [ ] |
A list of hostnames of remote build daemons to connect to for distributed builds.
Architecture / Services / Scope
- This module coordinates the creation of a system user (
builder) on the build server and adds the necessary SSH keys to allow other hosts to connect. - On the hosts using the build server, the module automatically configures
nix.distributedBuilds, sets up the build machines usingnix.buildMachines, and points outbound SSH authentication atcore.openssh.hostPrivateKeyPath. - The
builderuser is automatically added tonix.settings.trusted-userson the build server. - The module uses
self.nixosConfigurationsto dynamically discover the system architecture of the build machines.
Operational Notes / Assumptions
- A host declares the build server via
server.distributedBuilder.buildersin its host configuration.
References
Server Database — Managed PostgreSQL and Redis
Purpose
The database submodule provides a managed interface for PostgreSQL and Redis across the server infrastructure. It centralizes database configuration on the Database Coordinator (config.server.databasePrimaryHost) while allowing client services to declaratively request databases. It automates provisioning of PostgreSQL databases and roles, management of Redis database IDs via static mappings, synchronization of service lifecycle with database availability using the IO Guardian, and automated password handling via SOPS secrets.
Entry Point
The module is implemented across several files in modules/nixos/server/database/:
- Main file: default.nix: Core options and connection management.
- Supporting file: postgres.nix: PostgreSQL-specific provisioning and secrets.
- Supporting file: redis.nix: Redis-specific ID mappings and security.
- Supporting file: guardian.nix: Lifecycle synchronization and the IO Guardian.
Options
server.database.dependentServices
| Type | list of string |
| Default | [ ] |
List of systemd service names that depend on io databases. These services will be automatically bound to the io-databases.target and will stop/start when databases become unavailable/available.
server.database.host
| Type | string |
| Default | if isThisIOPrimaryHost then "localhost" else config.server.ioPrimaryHost |
The hostname or IP address to use when connecting to managed databases.
This is “localhost” when running on the host,
and
server.database.postgres
| Type | attribute set of (submodule) |
| Default | { } |
This option has no description.
server.database.postgres.<name>.database
| Type | string |
| Default | "‹name›" |
This option has no description.
server.database.postgres.<name>.host
| Type | string |
| Default | config.server.database.host |
This option has no description.
server.database.postgres.<name>.password
| Type | submodule |
| Default | { } |
This option has no description.
server.database.postgres.<name>.password.group
| Type | null or string |
| Default | null |
This option has no description.
server.database.postgres.<name>.password.owner
| Type | null or string |
| Default | null |
This option has no description.
server.database.postgres.<name>.password.path
| Type | absolute path |
| Default | config.sops.secrets."POSTGRES/${ toUpper config.server.database.postgres.${name}.database |> builtins.replaceStrings [ "-" ] [ "_" ] }_PASSWORD".path; |
This option has no description.
server.database.postgres.<name>.port
| Type | signed integer |
| Default | config.server.database.postgres.‹name›.port |
This option has no description.
server.database.postgres.<name>.user
| Type | string |
| Default | "‹name›" |
This option has no description.
server.database.redis
| Type | attribute set of (submodule) |
| Default | { } |
This option has no description.
server.database.redis.<name>.database_id
| Type | signed integer |
| Default | staticDbIdMappings.‹name› or (-1) |
This option has no description.
server.database.redis.<name>.host
| Type | string |
| Default | config.server.database.host |
This option has no description.
server.database.redis.<name>.port
| Type | signed integer |
| Default | (getIOPrimaryHostAttr "services.redis.servers")."".port |
This option has no description.
server.database.redis.<name>.prefix
| Type | string |
| Default | "‹name›" |
This option has no description.
server.database.postgres
| Type | attribute set of (submodule) |
| Default | { } |
This option has no description.
server.database.postgres.<name>.database
| Type | string |
| Default | "‹name›" |
This option has no description.
server.database.postgres.<name>.host
| Type | string |
| Default | config.server.database.host |
This option has no description.
server.database.postgres.<name>.password
| Type | submodule |
| Default | { } |
This option has no description.
server.database.postgres.<name>.password.group
| Type | null or string |
| Default | null |
This option has no description.
server.database.postgres.<name>.password.owner
| Type | null or string |
| Default | null |
This option has no description.
server.database.postgres.<name>.password.path
| Type | absolute path |
| Default | config.sops.secrets."POSTGRES/${ toUpper config.server.database.postgres.${name}.database |> builtins.replaceStrings [ "-" ] [ "_" ] }_PASSWORD".path; |
This option has no description.
server.database.postgres.<name>.port
| Type | signed integer |
| Default | config.server.database.postgres.‹name›.port |
This option has no description.
server.database.postgres.<name>.user
| Type | string |
| Default | "‹name›" |
This option has no description.
server.database.redis
| Type | attribute set of (submodule) |
| Default | { } |
This option has no description.
server.database.redis.<name>.database_id
| Type | signed integer |
| Default | staticDbIdMappings.‹name› or (-1) |
This option has no description.
server.database.redis.<name>.host
| Type | string |
| Default | config.server.database.host |
This option has no description.
server.database.redis.<name>.port
| Type | signed integer |
| Default | (getIOPrimaryHostAttr "services.redis.servers")."".port |
This option has no description.
server.database.redis.<name>.prefix
| Type | string |
| Default | "‹name›" |
This option has no description.
Architecture / Services / Scope
Connection Management
The server.database.host option determines how services connect to databases. On the Database Coordinator (config.server.databasePrimaryHost), it defaults to localhost. On all other hosts, it defaults to the value of config.server.databasePrimaryHost.
PostgreSQL Management
When a service defines a database in server.database.postgres:
- Automatic Provisioning: The Database Coordinator automatically creates the database and a role with the same name.
- Password Management: A SOPS secret is expected at
POSTGRES/<DB_NAME_UPPER>_PASSWORD. Database names containing hyphens (-) replace them with underscores (_) when constructing the secret path. The system automatically sets this password for the role during thepostgresql-setupservice. - Aggregated Configuration: The Database Coordinator collects all PostgreSQL requirements from across the entire flake to ensure all necessary extensions and initial scripts are loaded.
Redis Management
Redis management uses a similar aggregation pattern:
- Database IDs: Because Redis uses numeric IDs (0-15), the system uses a static mapping file (
redis-mappings.json) on the Database Coordinator to ensure consistent ID assignment across the fleet. - Password Management: A shared password for the primary Redis instance is managed via
REDIS/PASSWORDin SOPS. - Tooling: Use the
update-redis-mappingscommand on the Database Coordinator to update the mapping file when adding new Redis clients.
IO Guardian Coordination
Lifecycle management is handled by the IO Guardian. A pre-shared key for guardian communication is managed via the DB_GUARDIAN_PSK SOPS secret.
- On Clients: Services that use these database modules are automatically bound to
io-databases.target. This ensures they only start when the remote databases are reachable and stop before the databases go offline. Reachability and readiness checks are handled by the sharedwait-for-io-toolspackage (wait-for-ioandwait-for-io-databases). - On Database Coordinator: The
io-database-coordinatorservice manages thedrainandundrainsignals sent to clients during system startup and shutdown.
Secrets
POSTGRES/<DB_NAME_UPPER>_PASSWORD: Per-database role password, provisioned duringpostgresql-setup.REDIS/PASSWORD: Shared password for the primary Redis instance.DB_GUARDIAN_PSK: Pre-shared key for IO Guardian communication.
Operational Notes / Assumptions
- The host designated as the Database Coordinator (
config.server.databasePrimaryHost) is responsible for running the actual database engines. It aggregates all database requirements from every host in the flake and applies them locally. - Client services request databases by declaring entries in
server.database.postgresorserver.database.redis; the submodule wires up connection info, secrets, and lifecycle binding.
References
Server Storage — Mount Abstractions and Storage Services
Purpose
The storage module manages persistent storage abstractions for the server fleet. Today that includes the server.storage.swfsMount mount abstraction and an evaluation-only SeaweedFS deployment. This area provides:
- declarative MinIO-backed and SeaweedFS-backed mounts through
server.storage.swfsMount - a SeaweedFS evaluation deployment on the storage primary host
Entry Point
- Main file:
modules/nixos/server/storage/default.nix - Supporting file:
modules/nixos/server/storage/bucket.nix - Supporting file:
modules/nixos/server/storage/seaweedfs.nix
Architecture / Services / Scope
swfsMount
The swfsMount option is the repository’s declarative storage mount interface. It is a breaking rename from server.storage.bucketMounts and each entry chooses a backend explicitly.
- Backend Selection: Set
backend = "minio"to mount a MinIO bucket throughs3fs, orbackend = "seaweedfs"to mount a SeaweedFS filer path throughweed mount. - Use Scope: Use
swfsMountfor bucket/object-style workloads or external filer mounts. Do not point it at app state that expects normal local filesystem semantics, frequent metadata updates, or permission changes. - Common Mount Controls: Each entry supports
mountLocation,uid,gid,umask, andrequiredByServicesso consuming services can wait for the generated mount unit. - Health Recovery: Each entry also supports
healthCheck.*options. By default the module uses the shared packagedswfs-mount-hookhelper for prepare/stop/health actions, including lazy unmount of stale FUSE mounts, mount-service restart, and optional restart or reload of dependent services.
MinIO backend
- Credential Management: By default the MinIO backend provisions and uses sops secrets with the pattern
S3FS_AUTH/<NAME_IN_UPPERCASE>. These secrets must containACCESS_KEY_ID:SECRET_ACCESS_KEY. - Runtime Model: MinIO mounts now run as generated systemd services instead of
fileSystemsentries so they can share the same recovery model as SeaweedFS.
SeaweedFS backend
- Mount Command: SeaweedFS mounts use
weed mountdirectly against a filer endpoint and filer path. - Runtime Inputs: Configure the SeaweedFS backend through
seaweedfs.filer,seaweedfs.filerPath, and optional runtime flags such as UID/GID mapping or write-buffer limits.
SeaweedFS Evaluation
SeaweedFS is documented separately because it is not part of the current software filesystem workflow. The repository uses it as an evaluation deployment gated by server.storagePrimaryHost — SeaweedFS services and their Caddy proxy rules only activate on the host designated as the storage primary. The swfsMount mount module itself remains role-agnostic; host placement is determined by which host runs the services.
MinIO services also live on the storage primary host. The MinIO endpoint presented to mounts is served through the IO Coordinator reverse proxy, which routes to the MinIO instance on whichever host currently holds the storage primary role.
See SeaweedFS Evaluation for details on scope, host gating, proxy behavior, and security material.
Secrets
S3FS_AUTH/<NAME_IN_UPPERCASE>: MinIO backend credentials, containingACCESS_KEY_ID:SECRET_ACCESS_KEY.
Operational Notes / Assumptions
- FUSE Access: The module enables
programs.fuse.userAllowOther = truewhenever mounts are defined so boths3fsandweed mountcan expose shared FUSE mounts safely. - Network Dependency: Generated mount services depend on
network-online.targetbefore attempting either backend. - Recovery Behavior: The health-check timer uses
mountpointplus a boundedstatprobe. On failure it lazily unmounts the path, restarts the generated mount service, and can restart configured dependent services. - SeaweedFS Scope: The SeaweedFS evaluation deployment remains separate from this abstraction. The new SeaweedFS backend only reuses
weed mountfor workload mounts and does not replace the evaluation stack.
References
-
IO Coordinator(../../../hosts/server/nixio.md)
SeaweedFS Evaluation — Evaluation-Only Storage Service
Purpose
SeaweedFS is deployed here as an evaluation-only storage service alongside MinIO. It exists to validate SeaweedFS as a possible replacement candidate without changing existing MinIO-backed workloads or the repository’s migration posture. The evaluation deployment provides an all-in-one SeaweedFS stack on the storage primary host so the repository can test endpoint shape, proxy integration, and service behavior in a realistic environment while keeping the current MinIO setup intact.
Entry Point
- Main file:
modules/nixos/server/storage/seaweedfs.nix - Supporting file:
modules/nixos/server/storage/default.nix
Architecture / Services / Scope
- Evaluation only: this deployment does not replace MinIO and does not perform any migration.
- Storage primary only: the module is gated by
config.server.storagePrimaryHost == config.networking.hostName. - All-in-one topology: the evaluation enables the SeaweedFS master, volume, filer, S3 endpoint, admin UI, and worker components on the coordinator host.
- Proxy surface: endpoints are exposed through the existing
server.proxy.virtualHostsintegration instead of host-local Caddy configuration. Current surface includes the master, filer, S3-compatible, volume, and admin endpoints under theseaweedfs.<domain>subtree. Client-facing TLS terminates at Caddy; gRPC-backed component endpoints additionally get the backend transport settings required for SeaweedFS communication. - Option surface: the module uses the upstream
services.seaweedfsoption surface rather than introducing a repository-localserver.storage.seaweedfs.*option tree.
Secrets
SeaweedFS SOPS entries are separate from MinIO secrets and are used for:
- mTLS between Caddy and SeaweedFS components
- JWT-based inter-component authentication inside SeaweedFS
These entries live under the SEAWEEDFS secret tree on the storage primary host and include both JWT material and TLS certificates/keys for the SeaweedFS component set.
Operational Notes / Assumptions
- The evaluation deployment is separate from
server.storage.swfsMount. The storage abstraction can useweed mountfor SeaweedFS-backed workload mounts without changing the evaluation topology described here. - This deployment is intended to shake out integration details first; repository-local abstractions can be added later if SeaweedFS proves to be a good fit.
References
Server Proxy — Unified Caddy Reverse Proxy
Purpose
The Proxy submodule provides a unified interface for exposing internal services through Caddy. It handles virtual host configuration, automatic SSL via ACME, OAuth2 authentication with Kanidm, static API key authentication, and public exposure through Cloudflared tunnels. It abstracts the complexity of reverse proxying by allowing services to define their proxy requirements within their own module configuration, automatically coordinating between backend hosts and the primary IO host to ensure ports are open and traffic is correctly routed.
Entry Point
- Main file:
modules/nixos/server/proxy/default.nix - Supporting file:
modules/nixos/server/proxy/options.nix - Supporting file:
modules/nixos/server/proxy/config.nix - Supporting file:
modules/nixos/server/proxy/kanidm.nix - Supporting file:
modules/nixos/server/proxy/extensions.nix - Supporting file:
modules/nixos/server/proxy/extensions/l4.nix
Options
server.proxy.domain
| Type | string |
The base domain for all virtual hosts.
server.proxy.extensions
| Type | attribute set of (submodule) |
| Default | { } |
Registry of proxy extensions. Each extension provides config functions that are injected into vhost Caddy blocks, sorted by priority.
server.proxy.extensions.<name>.config
| Type | function that evaluates to a(n) function that evaluates to a(n) function that evaluates to a(n) string |
Function: vhostName -> vhostAttrSet -> hostConfig -> string. Returns Caddy directives to inject, or ‘’ for no-op. The vhostAttrSet includes the resolved extraConfig (already localhost-replaced) as _resolvedExtraConfig.
server.proxy.extensions.<name>.consumesExtraConfig
| Type | boolean |
| Default | false |
Whether this extension embeds extraConfig inside its output. When true, config.nix skips the post-extension extraConfig append for this vhost.
server.proxy.extensions.<name>.enable
| Type | boolean |
| Default | false |
Whether this extension is globally enabled.
Each extension SHOULD auto-detect whether it has work to do and set this to true via mkDefault in its module config.
User can explicitly override to force-disable (higher merge priority than mkDefault).
server.proxy.extensions.<name>.globalConfig
| Type | function that evaluates to a(n) string |
| Default | <function> |
Function: hostConfig -> string. Returns Caddy directives to inject into the top-level globalConfig block. Only called on the IO primary host. Sorted by priority across extensions.
server.proxy.extensions.<name>.priority
| Type | signed integer |
| Default | 100 |
Lower values = earlier in Caddy config. Priority ranges: 0-49 reserved, 50-99 auth, 100-199 general, 200+ post-processing.
server.proxy.extensions.<name>.vhostModule
| Type | null or module |
| Default | null |
Optional module to inject into each vhost submodule. Use options.<extensionName> (relative to vhost scope) to declare per-vhost options.
server.proxy.kanidmContexts
| Type | attribute set of (submodule) |
| Default | { } |
Shared Kanidm OAuth2 context configurations.
server.proxy.kanidmContexts.<name>.allowGroups
| Type | list of string |
| Default | [ ] |
| Example | [ "idm_all_persons@auth.racci.dev" "admins@auth.racci.dev" ] |
Default list of Kanidm groups allowed to access virtualHosts using this context.
server.proxy.kanidmContexts.<name>.authDomain
| Type | null or string |
| Default | null |
| Example | "auth.example.com" |
The domain where Kanidm is hosted. Defaults to auth.<server.proxy.domain> if not specified.
server.proxy.kanidmContexts.<name>.scopes
| Type | list of string |
| Default | [ "openid" "email" "profile" "groups" ] |
OAuth scopes to request from Kanidm.
server.proxy.kanidmContexts.<name>.tokenLifetime
| Type | signed integer |
| Default | 3600 |
Token lifetime in seconds for the authentication portal.
server.proxy.virtualHosts
| Type | attribute set of (submodule) |
| Default | { } |
Virtual hosts to be handled by the IO server and forwarded to the respective backend.
server.proxy.virtualHosts.<name>.aliases
| Type | list of string |
| Default | [ ] |
A list of virtual host names that should be routed using this configuration. Options added here will inherit the base domain specified in <server.proxy.domain>.
server.proxy.virtualHosts.<name>.baseUrl
| Type | string |
| Default | ${subdomain}.${getIOPrimaryHostAttr "server.proxy.domain"} |
The base url including the configured base domain name.
server.proxy.virtualHosts.<name>.extensions
| Type | null or (list of string) |
| Default | null |
List of extension names to enable for this virtual host. When null (default), all globally enabled extensions apply. When set to a list, only those named extensions apply. Set to [] to disable all extensions for this vhost.
server.proxy.virtualHosts.<name>.extraConfig
| Type | string |
| Default | "" |
Configuration to be placed in the caddy virtualHost extraConfig.
server.proxy.virtualHosts.<name>.kanidm
| Type | null or (submodule) |
| Default | null |
Enable Kanidm OAuth2 authentication for this virtual host.
server.proxy.virtualHosts.<name>.kanidm.allowGroups
| Type | list of string |
| Default | [ ] |
| Example | [ "idm_all_persons@auth.racci.dev" "admins@auth.racci.dev" ] |
Default list of Kanidm groups allowed to access virtualHosts using this context.
server.proxy.virtualHosts.<name>.kanidm.authDomain
| Type | null or string |
| Default | null |
| Example | "auth.example.com" |
The domain where Kanidm is hosted. Defaults to auth.<server.proxy.domain> if not specified.
server.proxy.virtualHosts.<name>.kanidm.bypassPaths
| Type | list of string |
| Default | [ ] |
| Example | [ "/health" "/api/webhooks/*" ] |
List of path patterns that should bypass authentication.
server.proxy.virtualHosts.<name>.kanidm.context
| Type | string |
| Default | "‹name›" |
The OAuth context name for this virtual host.
server.proxy.virtualHosts.<name>.kanidm.scopes
| Type | list of string |
| Default | [ "openid" "email" "profile" "groups" ] |
OAuth scopes to request from Kanidm.
server.proxy.virtualHosts.<name>.kanidm.tokenLifetime
| Type | signed integer |
| Default | 3600 |
Token lifetime in seconds for the authentication portal.
server.proxy.virtualHosts.<name>.l4
| Type | null or (submodule) |
| Default | null |
This option has no description.
server.proxy.virtualHosts.<name>.l4.config
| Type | string |
| Default | "" |
Configuration for the L4 plugin.
server.proxy.virtualHosts.<name>.l4.listenPort
| Type | 16 bit unsigned integer; between 0 and 65535 (both inclusive) |
Port to listen on for L4 traffic.
server.proxy.virtualHosts.<name>.l4.protocol
| Type | one of "tcp", "udp" |
| Default | "tcp" |
Protocol for L4 listener.
server.proxy.virtualHosts.<name>.listenPorts
| Type | non-empty (list of 16 bit unsigned integer; between 0 and 65535 (both inclusive)) |
| Default | [ 443 ] |
Port(s) to listen on for incoming traffic for this virtual host. If multiple ports are specified, the virtual host will be accessible on all of them.
server.proxy.virtualHosts.<name>.ports
| Type | list of 16 bit unsigned integer; between 0 and 65535 (both inclusive) |
| Default | [ ] |
Ports to be opened from the host for IO Hosts to forward traffic to.
server.proxy.virtualHosts.<name>.public
| Type | boolean |
| Default | false |
When enabled this service will be accessible to the public via Cloudflared Tunnels.
server.proxy.virtualHosts.<name>.requireApiKey
| Type | null or (submodule) |
| Default | null |
This option has no description.
server.proxy.virtualHosts.<name>.requireApiKey.bypassPaths
| Type | list of string |
| Default | [ ] |
| Example | [ "/health" "/api/webhooks/*" ] |
List of path patterns that bypass API key authentication.
server.proxy.virtualHosts.<name>.requireApiKey.enable
| Type | boolean |
| Default | false |
Enable API key authentication for this virtual host.
server.proxy.virtualHosts.<name>.useAcmeCerts
| Type | boolean |
| Default | true |
Whether to generate and use ACME certificates for this virtual host. If false, you must provide your own TLS configuration in extraConfig via the caddy tls directive.
Architecture / Services / Scope
File Layout
default.nix— Logic and helpers: resolving OAuth contexts and mapping local addresses to backend hostnames.options.nix— Option definitions for virtual hosts and shared contexts.config.nix— Caddy integration: generation ofservices.caddy.virtualHostsand ACME certificate requests. L4 (TCP/UDP) forwarding is handled by thel4extension, not byconfig.nix.kanidm.nix— Authentication security: generates the Caddysecurityblock, including identity providers, portals, and authorization policies.extensions.nix— System integration: connects the proxy to the dashboard, Cloudflared tunnels, and automates Kanidm client provisioning.
Extension System
The proxy module supports a registry-based extension system. Extensions are self-contained modules that inject Caddy directives into virtual host configurations — without modifying proxy internals.
Extension Registry
Extensions register themselves via server.proxy.extensions.<name>, an attribute set of submodules. Each extension has:
| Field | Type | Default | Description |
|---|---|---|---|
priority | int | 100 | Lower values = earlier Caddy config placement. Ranges: 0-49 reserved, 50-99 auth, 100-199 general, 200+ post-processing |
enable | bool | false | Globally enabled. Set via mkDefault based on detected config |
consumesExtraConfig | bool | false | When true, the extension embeds vh._resolvedExtraConfig in its output. config.nix skips appending raw extraConfig |
config | vhostName -> vhostAttrSet -> hostConfig -> str | required | Per-vhost Caddy directive generator |
globalConfig | hostConfig -> str | _ → "" | Top-level Caddy globalConfig directives |
vhostModule | nullOr deferredModule | null | Per-vhost option declarations |
Per-Vhost Extension Selection
Each vhost has server.proxy.virtualHosts.<name>.extensions (default null = all enabled extensions). Set to a list of extension names for selective enablement, or [] to disable all extensions.
Config Function Signature
config :: vhostName -> vhostAttrSet -> hostConfig -> string
Arguments:
vhostName(str): The vhost’s attribute name (e.g.,"grafana").vhostAttrSet: The full vhost attribute set, including_resolvedExtraConfig(user’sextraConfigwithreplaceLocalHostapplied) and_name.hostConfig: Full host-level NixOS config.
GlobalConfig Function Signature
globalConfig :: hostConfig -> string
Called once per enabled extension on the IO primary host. Output concatenated into services.caddy.globalConfig, sorted by extension priority.
Auto-Enable Pattern
Extensions auto-detect whether they have work to do using mkDefault; users can force-disable with explicit enable = false.
Priority Ordering
Extensions sort by priority ascending. Equal priorities break alphabetically by extension name. Extensions with lower priority numbers generate config earlier.
Authoring a New Extension
- Create file:
modules/nixos/server/proxy/extensions/<name>.nix - Import in
proxy/default.nix. - Set
server.proxy.extensions.<name>with priority, config function, etc. - Declare per-vhost options via
options.server.proxy.virtualHostswithattrsOf (submodule ...). - Use
proxyLibfor helpers:replaceLocalHost,resolveKanidmContext,hasAnyKanidm.
API Key Auth Extension
The api-key-auth extension provides static API key authentication for virtual hosts. When enabled, requests must include a valid Req-API-Key header matching a securely generated secret. Bypass paths are supported per vhost. Mutual exclusivity with Kanidm on the same vhost is enforced by the existing consumesExtraConfig assertion.
Migrated Extensions
| Extension | Priority | Purpose |
|---|---|---|
l4 | 10 | L4 TCP/UDP forwarding (layer4 Caddy block + firewall ports) |
kanidm | 50 | Kanidm OAuth2 authentication per vhost |
api-key-auth | 50 | Static API key authentication per vhost (with bypass paths) |
dashboard | 200 | Auto-generate dashboard items |
cloudflared | 200 | Cloudflared tunnel ingress |
Secrets
Kanidm OAuth2 Context
Authentication requires specific secrets per context, managed via sops-nix:
KANIDM/OAUTH2/<UPPER_CONTEXT>_SECRET: Provisioning secret for Kanidm systems.OAUTH_<PREFIX>_CLIENT_SECRET: The OAuth2 client secret for Caddy.<PREFIX>_SHARED_KEY: A shared key used by Caddy to sign and verify authentication tokens.
These are automatically managed if Kanidm provisioning is enabled on the same host.
API Key Auth
Secrets are auto-generated via sops at PROXY_AUTH/<VHOST_NAME>_API_KEY, injected via systemd LoadCredential.
Operational Notes / Assumptions
- Caddy Integration: The module assumes the existence of a
defaultCaddy snippet for common headers and security settings. Whenpublicis enabled, it also expects apublicsnippet. - Dashboard Integration: Services defined in
server.proxy.virtualHostsare automatically added to the server dashboard with default titles and icons derived from the host name. - Layer 4 Forwarding: L4 forwarding uses the
caddy.layer4plugin for non-HTTP traffic like database connections or SSH. Managed by thel4extension (modules/nixos/server/proxy/extensions/l4.nix), which auto-enables when any vhost hasl4 != null.
References
Server SSH — Root Login Development Shell
Purpose
The SSH submodule enhances administrative access by providing a session-only environment tailored for server management. It automatically transitions interactive root SSH sessions into a dedicated development shell, ensuring consistent tooling and a powerful shell experience across server environments, and removes the need for manual setup of common tools and aliases.
Entry Point
- Main file:
modules/nixos/server/ssh-shell/default.nix - Supporting file:
modules/nixos/server/ssh-shell/shell.nix
Options
server.sshShell.enable
| Type | boolean |
| Default | true |
| Example | true |
Whether to enable Auto-enter a session-only devShell for root on interactive SSH logins..
server.sshShell.shellFile
| Type | absolute path |
| Default | /nix/store/jq8636fkq2anq8f33kfqa816d0nrqw4m-source/modules/nixos/server/ssh-shell/shell.nix |
Path to a single-file that defines a session-only environment.
This file is evaluated by nix-shell and should import
Architecture / Services / Scope
The module consists of two parts:
Auto-entry Logic (ssh-shell/default.nix)
- Creates an indirect GC root for the SSH shell at login time by instantiating the shell expression to a derivation, then realizing it with
nix-store --add-root --indirect --realise. This keeps the realized shell alive across upgrades without referencingconfig.system.build.toplevelduring system evaluation. - Modifies
/etc/bashrcto detect interactive root logins via SSH. It evaluates several conditions before launching the session shell:- User must be root (
EUID=0). - Session must be via SSH (
SSH_CONNECTIONpresent). - Session must be interactive (
stdinis a TTY). - No active session shell detected (
SSH_NIX_SHELLunset). - User has not opted out via
NIX_SKIP_SHELL.
- User must be root (
- Configures OpenSSH to accept the
NIX_SKIP_SHELLenvironment variable from clients, allowing remote users to bypass the auto-shell entry when necessary.
Session Environment (ssh-shell/shell.nix)
- The default session shell is a
nix-shellenvironment containing:- Modern Shells: Fish shell with Starship prompt, Zoxide navigation, and Carapace completions.
- Enhanced Tooling: Replacements for standard utilities such as
bat(cat),fd(find),ripgrep(grep), andprocs(ps). - System Diagnostics: Tools like
btop,doggo,gping,inxi, andhyfetch.
- The
shellHookinshell.nixstarts an interactive Fish session and immediately exits thenix-shellwrapper once the Fish session concludes.
Operational Notes / Assumptions
Opt-Out Behavior
Set the NIX_SKIP_SHELL environment variable on your local machine before connecting to log in as root without entering the specialized shell:
NIX_SKIP_SHELL=1 ssh root@your-server
This is particularly useful for automated scripts or troubleshooting scenarios where the standard Bash environment is preferred.
Guard Mechanism
The auto-entry script uses the SSH_NIX_SHELL environment variable to prevent recursive shell entries. It runs nix-shell --add-root --indirect to build and enter the environment in a single call (pinning a GC root under /nix/var/nix/gcroots/per-user/root/ssh-shell-result), which triggers the shellHook and exec’s Fish. If that fails, the system falls back to the default shell, clears the guard, and prints a message to stderr.
References
Identity
Image
The image.nix module provides image and VM-related configuration for server hosts.
SSH Private Key Activation Script
On first boot of a freshly installed host, the SSH private key must be provisioned.
The public key is baked into the image at /etc/ssh/ssh_host_ed25519_key.pub, but the private key is deliberately not packaged.
Instead, the activation script prompts the operator interactively to input the private key,
validates it, and stores it at core.openssh.hostPrivateKeyPath, where OpenSSH and sops age decryption both read the same canonical file.
Before reading input, the script saves the current /dev/console tty state, switches the console into canonical line mode for reliable multiline paste handling, and restores the original tty state on exit.
The script is enabled unconditionally on all server hosts as a no-op unless both of the following hold:
/dev/consoleis available for I/O (it is underbuild-vm -nographic, where QEMU wires it to the host terminal)- A canonical host key does not already exist at
core.openssh.hostPrivateKeyPath
Input handling and validation
The prompt ignores any lines before -----BEGIN OPENSSH PRIVATE KEY-----, then captures the key line-by-line until -----END OPENSSH PRIVATE KEY-----.
If the final END line does not submit automatically, pressing Ctrl+D flushes that last line.
Pressing Ctrl+D before the BEGIN line restarts the prompt with No key provided, and pressing Ctrl+D after BEGIN but before END discards the partial key and restarts the prompt.
The captured key is checked in two stages:
- The key file is parsed and validated as a valid Ed25519 private key.
- The derived public key must match the baked-in public key the image was built with.
On failure the key file is removed and the prompt repeats.
VM Variant Overrides
All servers import the proxmox-lxc module which inherently breaks nixos-rebuild build-vm
because the Proxmox LXC image variant sets boot.isContainer = true and therefore disables the initrd.
We override this behavior for build-vm so that a runnable QEMU VM can be built from such hosts with the following options:
boot.isContainer = false: restores the initrd and normal VM boot.boot.loader.initScript.enable = false: avoids a unique-option conflict onsystem.build.installBootLoaderbetween the grub and init-script loaders.virtualisation.qemu.consoles = [ "tty0" "ttyS0,115200n8" ]: routes boot logs and/dev/consoleto the serial port so they are visible with-nographic(the lastconsole=becomes/dev/console).systemd.services."serial-getty@ttyS0".enable = true: enables root autologin on the serial console so the VM is usable with-nographic.
Building and running a VM
nixos-rebuild build-vm --flake .#<hostname>
./result/bin/run-<hostname>-vm -nographic
Building a Proxmox LXC image
nixos-rebuild build-image --image-variant proxmox-lxc --flake .#<hostname>
Flake Allocations — Cross-Host Cluster Configuration
Purpose
The flake allocations module declares cluster-wide configuration options at the flake level. Rather than configuring each NixOS system independently, allocations let you declare concerns like which machines have accelerators, which server acts as the IO Coordinator, and which servers act as distributed builders — in a single place — and then propagate those values into every host configuration.
Entry Point
- Main file:
modules/flake/allocations.nix - Supporting files:
- Supporting file:
modules/flake/apply/system.nix— maps allocations onto NixOS options per system - Supporting file:
modules/flake/apply/home-manager.nix— Home-Manager apply (placeholder) - Supporting file:
flake/nixos/flake-module.nix— sets the actual allocation values - Supporting file:
lib/builders/default.nix— builder that consumes allocations
- Supporting file:
Architecture / Services / Scope
The allocation system has three layers:
- Option Definitions (
modules/flake/allocations.nix) — declares the available allocation options. - Configuration (
flake/nixos/flake-module.nix) — sets the actual values for those options. - Apply Modules (
modules/flake/apply/) — propagate allocation values into each NixOS or Home-Manager configuration viaspecialArgs.
Data Flow
allocations.nix flake-module.nix apply/system.nix
┌──────────────┐ ┌──────────────────────┐ ┌───────────────────────┐
│ Define opts │──▶│ Set values │──▶│ Map to NixOS options │
│ (types, │ │ (which host has what)│ │ per system via │
│ defaults) │ │ │ │ specialArgs │
└──────────────┘ └──────────────────────┘ └───────────────────────┘
When a NixOS configuration is built, it receives the allocations attribute set and passes it as a specialArgs argument. The apply module then conditionally maps those allocations to NixOS module options based on the host’s device type.
Allocation Options
allocations.accelerators
| Type | attribute set of list of (one of "cuda", "rocm") |
| Default | { } |
| Example | { cudaAndRocmHost = [ "cuda" "rocm" ]; nothing = [ ]; onlyRocm = [ "rocm" ]; } |
Define hardware accelerators allocated to a machine by hostname.
The attribute names are hostnames, and the values are lists of accelerator types assigned to that host.
allocations.hostTypes
| Type | attribute set of list of string |
| Default | { desktop = [ "nixmi" ]; server = [ "nixai" "nixarr" "nixcloud" "nixdev" "nixio" "nixmon" "nixserv" ]; } |
| Example | { desktop = [ "workstation1" ]; server = [ "nixbuild1" "nixbuild2" ]; } |
An Attribute set defining hostnames by their device type. The attribute names are device types, and the values are lists of hostnames assigned to that device type.
allocations.server.distributedBuilders
| Type | list of (one of "nixai", "nixarr", "nixcloud", "nixdev", "nixio", "nixmon", "nixserv") |
| Default | [ ] |
| Example | [ "nixbuild1" "nixbuild2" ] |
List of servers that will act as remote builders for server-side distributed builds.
allocations.server.ioPrimaryCoordinator
| Type | one of "nixai", "nixarr", "nixcloud", "nixdev", "nixio", "nixmon", "nixserv" |
Designate a server to act as the Primary I/O coordinator
allocations.server.monitoringPrimaryHost
| Type | one of "nixai", "nixarr", "nixcloud", "nixdev", "nixio", "nixmon", "nixserv" |
Designate a server to act as the primary monitoring collector.
This host will run Prometheus, Loki, Grafana, and Alertmanager for centralized observability of the entire server cluster.
allocations.accelerators
Maps hostnames to their available hardware accelerators (cuda, rocm). Used by the builder system to configure nixpkgs with the correct cudaSupport / rocmSupport flags per host. Hosts not listed default to no accelerators. The builder reads allocations.accelerators.${hostname} and sets the corresponding nixpkgs config flags.
allocations.hostTypes
Read-only attribute set mapping device types to their hostnames. Auto-populated from the host-discovery function, which scans the hosts/ directory structure.
Server primary-host allocations
These options each designate a specific server as the coordinator for a cluster role (IO Coordinator, Monitoring Coordinator, Database Coordinator, Storage Coordinator, Identity Coordinator). The type of each is constrained to an enum of server hostnames, automatically derived from hostTypes.server. Values flow through apply/system.nix into the corresponding server.* option on each server configuration:
allocations.server.ioPrimaryCoordinator→server.ioPrimaryHost(IO Coordinator)allocations.server.monitoringPrimaryHost→server.monitoringPrimaryHost(Monitoring Coordinator)allocations.server.databasePrimaryHost→server.databasePrimaryHost(Database Coordinator)allocations.server.storagePrimaryHost→server.storagePrimaryHost(Storage Coordinator)allocations.server.authPrimaryHost→server.authPrimaryHost(Identity Coordinator)
allocations.server.distributedBuilders
List of servers that act as remote builders for distributed builds. Flows into server.distributedBuilds.builders on each server configuration.
Apply Modules
The apply modules bridge flake-level allocations to per-system NixOS options.
apply/system.nix is imported during system construction. It receives allocations and deviceType via specialArgs and maps the server allocations onto the corresponding server.* options. It uses optionalAttrs to only apply server-specific options when deviceType == "server", preventing errors on non-server systems.
apply/home-manager.nix is imported by the Home-Manager builder. It is currently a no-op — a placeholder for future home-manager-level allocations.
References
AI Modules — Overview
Purpose
The modules/nixos/ai/ tree is the canonical home for AI infrastructure services in this NixOS configuration. It provides first-class NixOS modules for AI-related daemons and services that are independent of any specific agent container.
Entry Point
- Main file:
modules/nixos/ai/default.nix
Architecture / Services / Scope
What belongs in ai/ vs services/
| Location | Purpose | Examples |
|---|---|---|
modules/nixos/ai/ | AI infrastructure daemons | Mnemosyne sync server, future: LLM gateways, embedding servers |
modules/nixos/services/ | Monolithic service containers | AI Agent (Hermes), future: agent orchestration |
The ai/ tree manages standalone services that an AI agent might consume, while services/ manages the agent container itself.
Operational Notes / Assumptions
The ai/ module tree is loaded on all device types by mkSystem.
References
- Mnemosyne — Sync server, optional MCP server, and sync client orchestration for the Mnemosyne SQLite-based memory provider
Mnemosyne — SQLite-Backed Memory Provider
Purpose
SQLite-backed memory provider with sync and optional MCP server. Part of the ai/ module tree.
Entry Point
- Main file: mnemosyne.nix
- Upstream: mnemosyne-oss/mnemosyne
Architecture / Services / Scope
graph TB
subgraph "NixAI Host"
HC["Hermes Container<br/>(mnemosyne-hermes plugin)"]
SS["Sync Server<br/>(mnemosyne sync-serve)"]
MS["MCP Server<br/>(mnemosyne mcp)"]
CD["Caddy Proxy"]
CT["systemd Timer<br/>(sync client)"]
end
subgraph "External"
EXT["External MCP Clients<br/>(Cursor, Claude Code)"]
REMOTE["Remote Mnemosyne<br/>(laptop, other host)"]
end
HC -->|"plugin reads/writes"| DB[(mnemosyne.db<br/>in container)]
CT -->|"mnemosyne sync --remote"| SS
SS -->|"serve"| SDB[(mnemosyne.db<br/>/var/lib/mnemosyne)]
MS -->|"mcp"| SDB
CD -->|"reverse_proxy"| SS
CD -->|"reverse_proxy"| MS
EXT -->|"MCP/SSE"| CD
REMOTE -->|"sync protocol"| CD
The module can run three kinds of services, each either natively on the host or inside a Docker container:
- Sync server (
mnemosyne sync-serve) — stdlib HTTP, no extra Python dependencies. - MCP server (
mnemosyne mcp) — adds themcpandanyiodependencies via the package’s optionalmcpgroup. - Sync client — per-profile periodic sync to a remote server, driven by a systemd timer (default interval 10 minutes).
Options
services.mnemosyne.client.sync
| Type | attribute set of (submodule) |
| Default | { } |
Sync client profiles for periodic sync to remote servers.
services.mnemosyne.client.sync.<name>.apiKeyFile
| Type | null or absolute path |
| Default | null |
Runtime path to a file containing the API key for authentication.
services.mnemosyne.client.sync.<name>.container
| Type | null or string |
| Default | null |
Docker container to run the sync client inside. If null, the server runs natively on the host.
Additionally, this will only work if the /nix/store is mounted inside the container.
services.mnemosyne.client.sync.<name>.interval
| Type | string |
| Default | "*:0/10" |
Systemd OnCalendar interval for sync. Default runs every 10 minutes.
services.mnemosyne.client.sync.<name>.remote
| Type | string |
Sync server URL (e.g. http://sync.example.com).
services.mnemosyne.client.sync.<name>.user
| Type | null or string |
| Default | null |
User to run the sync client as inside the container.
services.mnemosyne.dataDir
| Type | string |
| Default | "/var/lib/mnemosyne" |
Data directory for Mnemosyne state.
services.mnemosyne.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable Mnemosyne memory service.
services.mnemosyne.server.mcp.container
| Type | null or string |
| Default | null |
Docker container to run the mcp server inside. If null, the server runs natively on the host.
Additionally, this will only work if the /nix/store is mounted inside the container.
services.mnemosyne.server.mcp.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable Mnemosyne mcp server.
services.mnemosyne.server.mcp.host
| Type | string |
| Default | "127.0.0.1" |
Host address for the mcp server to listen on.
services.mnemosyne.server.mcp.port
| Type | 16 bit unsigned integer; between 0 and 65535 (both inclusive) |
| Default | 8766 |
Port for the mcp server to listen on.
services.mnemosyne.server.mcp.user
| Type | null or string |
| Default | null |
User to run the mcp server as inside the container.
services.mnemosyne.server.sync.apiKeyFile
| Type | null or absolute path |
| Default | null |
Runtime path to a file containing the API key for authentication.
services.mnemosyne.server.sync.container
| Type | null or string |
| Default | null |
Docker container to run the sync server inside. If null, the server runs natively on the host.
Additionally, this will only work if the /nix/store is mounted inside the container.
services.mnemosyne.server.sync.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable Mnemosyne sync server.
services.mnemosyne.server.sync.host
| Type | string |
| Default | "127.0.0.1" |
Host address for the sync server to listen on.
services.mnemosyne.server.sync.port
| Type | 16 bit unsigned integer; between 0 and 65535 (both inclusive) |
| Default | 8765 |
Port for the sync server to listen on.
services.mnemosyne.server.sync.user
| Type | null or string |
| Default | null |
User to run the sync server as inside the container.
Secrets
MNEMOSYNE_SYNC_KEY— API key for sync server authentication (host-level sops secret), provided viaapiKeyFileand loaded into the services through systemd credentials.
Operational Notes / Assumptions
- Sync protocol is plain HTTP with delta-based bidirectional sync.
- Sync interval default is 10 minutes.
Usage Examples
Server-only (central sync)
{
services.mnemosyne = {
enable = true;
server.sync.enable = true;
};
}
Client-only (sync to remote)
{
services.mnemosyne = {
enable = true;
client.sync.hermes = {
enable = true;
remote = "http://sync.example.com:8765";
interval = "*:0/15";
};
};
}
References
DIY & Making — 3D Printing and Maker Tooling
Purpose
The purpose.diy Home-Manager modules provide tooling and configuration for hardware tinkering, 3D printing, and related maker activities.
Entry Point
Architecture / Services / Scope
Printing
The printing module installs 3D-printing software and wires up persistent storage so that settings survive reboots on impermanence-based systems.
Options
purpose.diy.printing.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable Enable 3D printing support.
purpose.diy.printing.gitSync.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable Auto-commit OrcaSlicer settings changes to a local git repository.
purpose.diy.printing.gitSync.remoteUrl
| Type | null or string |
| Default | null |
Optional remote URL to push commits to. If set, the git sync service will attempt to push commits to this remote after creating them.
The remote must be configured with appropriate credentials (e.g. via SSH keys) for non-interactive authentication.
purpose.diy.printing.gitSync.repoPath
| Type | string |
| Default | "${config.home.homeDirectory}/.config/OrcaSlicer/user/default" |
Absolute path to the directory that will be tracked as a git repository. The directory is initialised automatically the first time the watcher service starts, so it does not need to exist at activation time.
Defaults to the standard OrcaSlicer per-user profile directory so that filament, process, and machine profiles are all captured without any additional configuration.
Git Sync
The gitSync sub-module adds a long-running systemd user service backed by the packaged orca-slicer-git-sync helper.
It watches the OrcaSlicer profile directory and automatically creates a git commit every time a profile file is added, changed, or removed. This gives a full revision history of slicer settings with zero manual effort.
Commit Message Convention
Commit messages are generated automatically based on the type of filesystem event and the location of the file within the repository:
| Event | Commit message format |
|---|---|
| File added / created | feat(<type>): added <name> |
| File modified | refactor(<type>): updated <name> |
| File deleted | chore(<type>): removed <name> |
Where:
<type>is the name of the first directory component under the repo root (e.g.filament,process,machine). Files placed directly at the root level use the fallback typeconfig.<name>is the filename stripped of its extension (e.g. a file namedPrusament_PLA.jsonyields the namePrusament_PLA).
Examples:
feat(filament): added Prusament_PLA
refactor(process): updated Standard_0.2mm_Quality
chore(machine): removed Prusa_MK4S
How It Works
- A systemd user service is started at login and kept alive by systemd.
- The service uses
inotifywait(frominotify-tools) in one-shot mode inside a loop to detect any filesystem event under the repo path (excluding the.gitdirectory). - After an event is received the watcher sleeps for a short debounce period to absorb rapid bursts of writes (e.g. when OrcaSlicer rewrites multiple files at once).
- All pending changes are staged and committed once per batch. The commit message is derived from the first changed path in that batch, using the same profile-aware naming convention documented above.
- If the watched directory does not yet exist (e.g. OrcaSlicer has never been run), the service polls until it appears, then initialises the repository and starts watching.
Usage Example
{ ... }: {
purpose.diy.enable = true;
purpose.diy.printing = {
enable = true;
gitSync = {
enable = true;
# Optional: use a custom path outside the OrcaSlicer config directory
# repoPath = "/home/alice/slicer-profiles";
};
};
}
Operational Notes / Assumptions
- The git repository is initialised with
git initand an initial commit the first time the service starts if no.gitdirectory exists. - The service is set to restart on failure so transient errors do not leave settings un-tracked.
- Because the watcher operates on the live OrcaSlicer profile directory, no separate mirroring or rsync step is needed.
AI Editors & Assistants — AI-Assisted Development Tooling
Purpose
The purpose.development.editors.ai Home-Manager module configures editor and agent tooling for AI-assisted development, centered around OpenCode and shared skill directories.
Entry Point
- Main file:
modules/home-manager/purpose/development/editors/ai/default.nix - Supporting files: module-local
skills/directory containing the default skills
Architecture / Services / Scope
When enabled, the module:
- Ensures a shared AI filesystem directory exists at activation time.
- Adds useful global git ignores (e.g. editor workspace and tool-state directories).
- Configures Zed to expose an
OpenCodeagent server. - Enables and configures
programs.opencodewith:- plugins
- Nix formatter integration
- LSP integrations across Nix, config formats, and general-purpose languages
- command permissions policy
- a local MCP server (e.g.
mcp-nixosviauvx)
- Writes OpenCode config files (e.g.
~/.config/opencode/oh-my-opencode.json,opencode-notifier.json). - Registers AI skills under
~/.agents/skills/<name>viahome.file. - Persists OpenCode state directories so they survive reboots on impermanence-based systems.
Options
purpose.development.editors.ai.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable Enable AI Tools & Assistants.
purpose.development.editors.ai.includeDefaults
| Type | boolean |
| Default | true |
Whether to include the default set of agents and skills provided by this module.
This includes the agents and skills defined in the ./agents and ./skills directories of this module.
Disabling this will result in a minimal setup with only the base configuration for OpenCode and no pre-registered agents or skills.
purpose.development.editors.ai.skills
| Type | list of string |
| Default | [ ] |
| Example | '' [ ''${inputs.my-skill-repo}/skills/my-skill ''${self}/skills/another-skill ] '' |
List of additional AI skills to add to the global registry. These should be paths to a skill directory, this could be through a flake input or a path in the flake.
These skills will be installed to ~/.agents/skills and will be available to all agents that support the skill system, such as Claude and OpenCode.
purpose.development.editors.ai.tabby.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable Tabby agent for tab completion.
Usage Example
{ self, inputs, ... }: {
purpose.development.editors.ai = {
enable = true;
includeDefaults = true;
skills = [
"${inputs.my-skill-repo}/skills/my-skill"
"${self}/skills/another-skill"
];
};
}
Operational Notes / Assumptions
- Skill links are generated under
~/.agents/skills/<basename>. - Default skills are discovered automatically from the module’s local
skills/directory whenincludeDefaults = true. - The module currently defines default agent discovery as well, but only skill link materialization is active in
home.fileoutput.
Hyprland Helpers — Typed Nix API for the Home-Manager Hyprland Module
Purpose
The core.hyprland modules extend the upstream Home-Manager wayland.windowManager.hyprland module with a typed Nix API for window rules, permissions, slide-in popups, input defaults, and Lua config generation.
They target the HM-native Lua configuration format (configType = "lua"), which is the repository default.
Entry Point
- Main file:
modules/home-manager/core/hyprland/default.nix - Supporting files:
input.nix,permission.nix,slideIn.nix,workspaces.nix,lua.nix,types.nix,noctalia.nix, and all files underlua/*, in the same directory.
The module structure is:
default.nix # Top-level importer
├── permission.nix # custom-settings.permission
├── slideIn.nix # custom-settings.slideIn
├── input.nix # settings.config defaults (cursor, binds, input, misc)
├── workspaces.nix # custom-settings.workspaces
├── lua.nix # custom-settings.lua (Lua config generation)
│ └── lua/ # Lua source files, with @placeholder@ substitution
│ └── opt/ # Conditionally loaded Lua modules
└── types.nix # Shared type definitions
Options
wayland.windowManager.hyprland.custom-settings.lua.applicationBinds
| Type | attribute set of string |
| Default | { } |
Application binds to generate in Lua config.
wayland.windowManager.hyprland.custom-settings.lua.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable Pure Lua configuration files for Hyprland, with a hint of nix substitution magic..
wayland.windowManager.hyprland.custom-settings.lua.luaExtras
| Type | list of absolute path |
| Default | [ ] |
Extra Lua files to copy to the config directory. These files will be copied to the config directory but not required in init.lua, so you can use them as libraries or for other purposes.
Files defined here will respect parent directories and will be copied to the same relative path in the config directory.
The absolute root of the directory tree will be calculated by finding the closest lua ancestor directory, and copying the entire tree from that root to the config directory.
If a directory is specified, it will be recursively copied to the config directory, preserving the directory structure.
wayland.windowManager.hyprland.custom-settings.lua.luaModules
| Type | list of absolute path |
| Default | [ ] |
Lua modules to load in the main init.lua file. Each module is a path to a Lua file, which will be copied to the config directory and required in init.lua. Each module will have variables substituted according to the “variables” option, so you can use that to inject paths to nix packages or other dynamic values.
wayland.windowManager.hyprland.custom-settings.lua.variables
| Type | attribute set of (null or string) |
| Default | { } |
Variables to substitute in Lua files. Each key “foo” replaces @foo@ in source files with the value.
wayland.windowManager.hyprland.custom-settings.permission.plugin
| Type | list of (package or string) |
| Default | [ ] |
List of plugins that are allowed to run.
wayland.windowManager.hyprland.custom-settings.permission.screenCopy
| Type | list of (package or string) |
| Default | [ ] |
List of applications allowed to copy the screen.
wayland.windowManager.hyprland.custom-settings.slideIn
| Type | list of (submodule) |
| Default | [ ] |
List of slide-in popups that slide in from the edge of the screen.
wayland.windowManager.hyprland.custom-settings.slideIn.*.bind
| Type | string |
Key binding to trigger the slide-in popup.
This is passed through to the lua config without checking for validity, the lua config will throw an error if the binding is invalid.
wayland.windowManager.hyprland.custom-settings.slideIn.*.class
| Type | string |
Window class for the slide-in popup.
wayland.windowManager.hyprland.custom-settings.slideIn.*.exec
| Type | string |
Command to execute for the slide-in popup.
wayland.windowManager.hyprland.custom-settings.slideIn.*.extProp
| Type | attribute set of anything |
| Default | { } |
Extra properties to pass to the scratchpad configuration in pyprland.
wayland.windowManager.hyprland.custom-settings.slideIn.*.position
| Type | one of "left", "right", "top", "bottom" |
| Default | "top" |
Direction from which the popup slides in.
wayland.windowManager.hyprland.custom-settings.slideIn.*.size
| Type | submodule |
| Default | { } |
Size of the slide-in popup. Can specify ‘width’ and/or ‘height’.
wayland.windowManager.hyprland.custom-settings.slideIn.*.size.height
| Type | null or string |
| Default | null |
Height of the slide-in popup. Can be specified as a percentage (e.g., ‘33%’) or in pixels (e.g., ‘300px’).
wayland.windowManager.hyprland.custom-settings.slideIn.*.size.width
| Type | null or string |
| Default | null |
Width of the slide-in popup. Can be specified as a percentage (e.g., ‘20%’) or in pixels (e.g., ‘400px’).
wayland.windowManager.hyprland.custom-settings.workspaces.definitions
| Type | attribute set of (submodule) |
| Default | { } |
Workspace definitions by ID
wayland.windowManager.hyprland.custom-settings.workspaces.definitions.<name>.extraRules
| Type | attribute set of anything |
| Default | { } |
Additional workspace rule properties to pass to workspace_rule().
wayland.windowManager.hyprland.custom-settings.workspaces.definitions.<name>.monitor
| Type | null or string |
| Default | null |
Monitor to assign workspace on startup.
wayland.windowManager.hyprland.custom-settings.workspaces.definitions.<name>.name
| Type | null or string |
| Default | null |
Workspace default name.
wayland.windowManager.hyprland.custom-settings.workspaces.definitions.<name>.startup
| Type | list of (string or attribute set of string) |
| Default | [ ] |
Commands to run when workspace is first created empty.
wayland.windowManager.hyprland.custom-settings.workspaces.enable
| Type | boolean |
| Default | false |
Enable workspace configuration module.
Architecture / Services / Scope
input.nix
Sets sensible default values under settings.config for cursor behavior, input device settings, keyboard binds, and misc Hyprland options.
Default config covers:
cursor: warp behavior, hardware cursors, inactivity timeout, hide-on-key-pressbinds: workspace back-and-forth, allow workspace cycles, focus methodinput: keyboard layout, follow-mouse, touchpad, sensitivity, accel profilemisc: DPMS on key/mouse events
permission.nix
Defines custom-settings.permission for screen copy and plugin permission grants:
custom-settings.permission = {
screenCopy = [ pkgs.firefox pkgs.obs ];
plugin = [ pkgs.hyprlandPlugins.hy3 ];
};
slideIn.nix
Add the option custom-settings.slideIn to define a list of edge-sliding popup windows.
Each entry configures a keybind, executable, window class, position, and optional window rules.
Uses Pyprland scratchpads for dropdown-style window management.
Architecture:
- Nix module: generates
~/.config/pypr/config.tomlwith scratchpad definitions, registerssystemd.user.services.pyprland. - Lua module: registers
hl.bind(...)calls that toggle the scratchpads.
lua.nix
Defines custom-settings.lua — the Lua config generation subsystem:
enable(boolean) — Enable pure Lua configuration files with Nix substitution support.variables(attrs ofnullOr str) — Key-value pairs for@placeholder@substitution in Lua source files. Each keyfooreplaces@foo@only in Lua modules that reference that placeholder. Modules that don’t reference a given placeholder are unaffected, so bundled modules can use disjoint placeholder sets. A placeholder referenced by a module but absent fromvariablesis an error. Some variables are pre-populated automatically (seeapplicationBindsbelow). Common injected values include paths toplayerctl,wpctl,zenity,hyprshutdown, anduwsm-app.luaModules(list of paths) — Lua source files to copy into the Hyprland config directory andrequirefrominit.lua. Each file undergoes@placeholder@substitution using thevariablesattrset. Defaults to the bundledlua/binds.lua.applicationBinds(attrs ofstr) — Application keybinds passed into Lua generation. Each attr key is a bind string and each attr value is a command string. Rendered into@applicationBinds@as Lua table entries consumed bybinds.lua. Generated Lua iterates over those table entries and createshl.bind(..., hl.dsp.exec_cmd(...))calls for each bind/command pair.
Lua bind pattern
In lua/binds.lua, binds use the inline Lua expression pattern via settings.bind with attrsToLuaInlineArgs. The generated Lua calls hl.bind(...) with first-class dispatcher functions:
hl.bind("SUPER + Q", hl.dsp.window.kill())
hl.bind("SUPER + SHIFT + SPACE", hl.dsp.window.float({ action = "toggle" }))
hl.bind("ALT + R", hl.dsp.submap("resize"))
hl.define_submap("resize", function()
hl.bind("ESCAPE", hl.dsp.submap("reset"))
-- ...
end)
This pattern keeps bind and submap definitions inline in Lua. Submaps are defined via hl.define_submap(name, fn) alongside related hl.bind(...) calls.
workspaces.nix
Workspace configuration module at modules/home-manager/core/hyprland/workspaces.nix.
Handles workspace naming, monitor assignments, and startup applications with startup-only monitor assignment behavior:
- Startup-only assignment: Monitor assignments run once via
hl.on("hyprland.start")Lua event hook. Users can move workspaces freely after startup without interference. - Persistent naming: Workspace names and startup commands persist via
hl.workspace_rule(), ensuring defaults are restored if a workspace is recreated. - Conditional Lua module: The
lua/opt/workspaces.luamodule is automatically added to the Lua config only whencustom-settings.workspaces.enable = true. If disabled, no workspace Lua code is loaded. - Configuration source: Workspace data (name, monitor, startup commands) is defined in user configs via the typed
custom-settings.workspaces.definitionsoption and passed to Lua as@workspaceConfig@variable.
Enable and configure workspaces in user home config:
wayland.windowManager.hyprland.custom-settings.workspaces = {
enable = true;
definitions = {
"1" = {
name = "Terminal";
monitor = "DP-6"; # startup-only; workspace can be moved after launch
startup = [ (lib.getExe pkgs.alacritty) ]; # runs when workspace first created
};
"2" = {
name = "Browser";
monitor = "DP-1";
startup = [ (lib.getExe config.programs.firefox.package) ];
};
# ...
};
};
The module generates:
- Lua substitution variable
@workspaceConfig@with workspace definitions as JSON - Conditionally loads
lua/opt/workspaces.luato register startup hooks and workspace rules
lua/opt/workspaces.lua
Lua module at modules/home-manager/core/hyprland/lua/opt/workspaces.lua that registers workspace configuration. Only loaded when custom-settings.workspaces.enable = true. Receives workspace data via @workspaceConfig@ placeholder substitution and:
- Registers
hl.on("hyprland.start")hook to assign workspaces to monitors on startup only - Calls
hl.workspace_rule()for each workspace to set persistent names and startup commands
| Placeholder | Source | Description |
|---|---|---|
@workspaceConfig@ | custom-settings.workspaces.definitions | Workspace config table as JSON |
lua/binds.lua
The default Lua bind template at modules/home-manager/core/hyprland/lua/binds.lua. Uses @placeholder@ substitution for dynamic injection. Substitution is per-file — only placeholders actually present in this template are replaced; other Lua modules are unaffected by binds.lua’s placeholder set.
| Placeholder | Source | Description |
|---|---|---|
@applicationBinds@ | custom-settings.lua.applicationBinds | Auto-generated Lua table of app keybinds |
@playerctl@ | Auto-injected | Path to playerctl binary |
@wpctl@ | Auto-injected | Path to wpctl binary |
@zenity@ | Auto-injected | Path to zenity binary |
@hyprshutdown@ | Auto-injected | Path to hyprshutdown binary |
@uwsmApp@ | Auto-injected | Path to uwsm-app helper |
@DEFAULT_AUDIO_SINK@ | custom-settings.lua.variables | Audio sink name |
@DEFAULT_AUDIO_SOURCE@ | custom-settings.lua.variables | Audio source name |
Add custom placeholders by extending custom-settings.lua.variables.
noctalia.nix
Integrates the Noctalia desktop shell as a Hyprland companion. Requires the noctalia flake input.
The module:
- Enables
programs.noctaliaandsystemd, pinspackagefrom thenoctaliaflake input’s packages, and applies Hyprland support for Noctalia windows. - Mirrors a full exported Noctalia config as a typed Nix attrset (
noctaliaSettings), covering bar layouts with monitor overrides, shell panel/screen corners/screenshot/session actions, theme, wallpaper, calendar, control-center shortcuts, desktop/lockscreen widgets, notification layer, plugin settings, widget config, brightness, and more. - Does not declare top-level
colorsorpluginsHM options, and does not manage raw JSON files directly. - Persists
~/.local/share/noctaliaviauser.persistence.directories. - Reads
core.profile.avatar.path→shell.avatar_pathandcore.profile.wallpaper.directory→wallpaper.directory. Wallpaper fill mode is hardcoded (not a profile option). - Location is driven by
core.profile.location.secret(a SOPS secret name). Two modes:- Normal (
secret == null): setsprograms.noctalia.settings. No location block. - Secret (
secret != null): base TOML generated at build time; activation copies it to~/.config/noctalia/config.tomland appends[location] addressfrom the decrypted SOPS secret.
- Normal (
The user-side Hyprland config pairs with this module via Noctalia IPC keybinds (fullscreen, special workspace toggles, settings, audio/brightness dispatchers). Workspace rules in the user config set persistent = true for defined workspaces so they are always available regardless of Noctalia lifecycle.
types.nix
Shared type definitions used across the modules:
monitorSelector— typed Nix attrs for monitor matching (bynameorindex)workspaceSelector— typed Nix attrs for workspace matching (byid,relativeId,name, orspecial)rule— all typed window rule properties (float, fullscreen, opacity, size, move, center, monitor, workspace, and dozens more)windowMatch— match condition types (class, title, initialClass, initialTitle, tag, xwayland, float, fullscreen, pin, focus, group, modal, fullscreenstate, workspace, content, xdg_tag)
Usage Example
{
wayland.windowManager.hyprland = {
enable = true;
configType = "lua";
custom-settings = {
permission = {
screenCopy = [ pkgs.firefox ];
};
lua = {
enable = true;
luaModules = [ ./lua/window_rules.lua ];
applicationBinds = {
"SUPER + Return" = "${pkgs.kitty}/bin/kitty";
"SUPER + E" = "${pkgs.nautilus}/bin/nautilus";
};
};
};
};
}
Operational Notes / Assumptions
- All options live under
custom-settingsto avoid collision with upstream HM Hyprland options. lua.nixauto-injectsapplicationBinds,playerctl,wpctl,zenity,hyprshutdown, anduwsmAppas substitution variables — no need to set those manually.- Variable substitution is per-file: each Lua module only receives replacements for
@placeholder@tokens it actually contains. A variable defined but unused by a given module is silently ignored for that module. A placeholder referenced by a module but missing fromvariablesis an error. - Unknown dispatchers in Lua raise a runtime error from Hyprland’s Lua parser, not a build-time error.
- CamelCase naming in Nix (e.g.
fullscreenState,idleInhibit,keepAspectRatio,noCloseFor,forceRgbx,syncFullscreen) is translated to snake_case in the Lua output.
Core Profile — Shared User Profile Options
Purpose
The core.profile module declares a set of shared user-profile options that act as a single source of truth for values consumed by other modules — desktop environments, shell companions, and display managers — rather than hardcoded literals.
Entry Point
- Main file:
modules/home-manager/core/profile.nix - Supporting file:
modules/home-manager/core/default.nix— imports the module automatically
Architecture / Services / Scope
The module only declares options; it does nothing on its own. Consumers use config.core.profile.* to read the values.
Options
core.profile.avatar.path
| Type | string |
| Default | ${config.home.homeDirectory}/.face |
Path to user avatar image.
core.profile.location.secret
| Type | null or string |
| Default | null |
SOPS secret name used for Noctalia location address.
core.profile.wallpaper.directory
| Type | string |
| Default | ${config.home.homeDirectory}/Pictures/Wallpapers |
Path to wallpaper image directory.
core.profile.avatar.path
Path to the user avatar image. Used by the Noctalia desktop shell (via programs.noctalia.settings.shell.avatar_path) to display the user picture in the bar and session UI.
core.profile.wallpaper.directory
Directory containing wallpaper images.
Consumers:
| Consumer | How it uses the value |
|---|---|
| Noctalia desktop shell | Sets programs.noctalia.settings.wallpaper.directory |
| GNOME azwallpaper extension | Sets org/gnome/shell/extensions/azwallpaper slideshow-directory |
core.profile.location.secret
Name of a SOPS secret whose decrypted value becomes the Noctalia location.address. When unset, the location block is omitted from the Noctalia config entirely. At activation time, the decrypted secret is appended to a base Noctalia config TOML (generated by the module) and written to ~/.config/noctalia/config.toml — the clear-text address never lands in the repo or Nix store.
Secrets
core.profile.location.secret— name of a SOPS secret (not a literal value) whose decrypted content supplies the Noctalia location address at activation time.
Usage Example
{ ... }: {
core.profile = {
avatar.path = "/home/user/.face";
wallpaper.directory = "/home/user/Pictures/Backgrounds";
location.secret = "noctalia-location";
};
}
Operational Notes / Assumptions
- The GNOME azwallpaper extension consumes
core.profile.wallpaper.directoryviadconf-extensions.nix(home/shared/desktop/gnome/).
list-ephemeral — Ephemeral Path Discovery and Persistence Snippets
Purpose
list-ephemeral is a shell utility that helps discover ephemeral paths and generate Nix snippets for persistence. It integrates with Home-Manager to supply defaults, persisted paths, and program context.
Entry Point
- Main file:
modules/home-manager/programs/list-ephemeral.nix - Package:
pkgs/list-ephemeral
Architecture / Services / Scope
Options
programs.list-ephemeral.enable
| Type | boolean |
| Default | hostPersistEnabled || config.user.persistence.enable |
| Example | true |
Whether to enable list-ephemeral helper.
programs.list-ephemeral.extraExcludes
| Type | list of string |
| Default | [ ] |
| Example | [ "home/*/.local/share/Trash" ] |
Additional exclude patterns for list-ephemeral.
programs.list-ephemeral.extraIncludes
| Type | list of string |
| Default | [ ] |
| Example | [ ".config/my-app" "/var/lib/my-app" ] |
Additional paths to always include as candidates.
The module writes a generated config file consumed by the TUI:
- Excludes default ephemeral paths (caches, logs, Electron app state, etc.) so they never appear as persistence candidates.
- Supplies the currently persisted files and directories from
user.persistence/host.persistence, which are marked as already persisted in the UI. - Supplies installed program names so the TUI can filter candidates by program context.
Usage
Default TUI (fzf-based with keybindings):
list-ephemeral
TUI Keybindings
| Key | Action |
|---|---|
/ | Enable search mode (type to fuzzy filter) |
Escape | Disable search and clear query |
Ctrl-P | Open program filter (gum picker) |
Ctrl-X | Clear program filter |
Space | Toggle selection and move down |
Ctrl-A | Select all |
Ctrl-D | Deselect all |
Ctrl-C | Quit (standard fzf behavior) |
Enter | Confirm selection |
Note: In browse mode (default), typing text will appear in the prompt but won’t filter results. Press / to enable search filtering.
List mode:
list-ephemeral list
Trace mode (runs a command and then opens TUI with traced ephemeral paths):
list-ephemeral trace -- <cmd> [args...]
Snippet Generation
The TUI generates Nix snippets based on path location:
- Paths under
$HOMEare emitted asuser.persistence.filesoruser.persistence.directorieswith paths relative to$HOME. - Paths outside
$HOMEare emitted ashost.persistence.filesorhost.persistence.directorieswith absolute paths.
If the selection includes both kinds, the snippet contains both blocks.
Packages Overview
Purpose
This section documents the custom packages defined in this repository. These are packages that are either not available in nixpkgs or require custom builds.
Architecture / Services / Scope
Entry Points
pkgs/: Contains the package definitions, typically organized by package name.alvr-bin: Binaries for ALVR that allows nvidia accelerated by using the AppImage.drive-stats: Tool for monitoring and reporting drive statistics.colour-picker: Hyprland color picker wrapper that temporarily lowers pointer sensitivity, launcheshyprpicker, then restores the prior sensitivity.folder-diff: Nushell helper that compares two directories by tracking added, modified, and deleted files in a temporary Git repo, then prints a binary-safe diff plus per-file change summaries.helpers: Collection of helper scripts for configuration management.huntress: Integration for Huntress security agent.hypr-gamemode: Script to optimize Hyprland performance for gaming.io-guardian: Database lifecycle management across hosts.lidarr-plugins: Lidarr plugins branch.list-ephemeral: Utility to identify ephemeral paths, trace file access, and generate persistence snippets.lix-woodpecker: Woodpecker CI runner.mcp-sequential-thinking: MCP server for step-by-step reasoning.mcp-server-amazon: MCP server for Amazon services interaction.proton-mcp: MCP server for ProtonMail.compressor: Python tool that hashes image/video files, detects media from magic headers, caches WebP image conversions and AV1 MP4 video conversions. For videos, it samples 3 random segments to estimate compression ratio and auto-skip expansion cases. Those sample encodes now use live HandBrake JSON progress too, with per-sample ETA plus total sample ETA in Rich progress bars. Full sequential video encodes also parse live JSON progress, update Rich progress bars with per-video ETA plus rolling queue ETA, fall back to elapsed/progress-derived ETA when HandBrake reports zero, and--debugprints periodic runtime progress snapshots. Records applied outputs in its TSV cache and prints aligned Rich tables.ocr-region: Wayland OCR helper that captures selected region, preprocesses image, runs multilingual Tesseract OCR, copies detected text, and notifies result.monocoque: Sim-racing dashboard and telemetry tool.orca-slicer-git-sync: Watches an OrcaSlicer profile repository, commits batched changes with profile-aware commit messages, and optionally pushes to a configured remote.python: Packages for home assistant python components.screenshot: Wayland screenshot helper that captures an area or output, stores timestamped files, and optionally opens or annotates the result.ssh-relay: WSL SSH agent relay helper that bridges a Windows named pipe into a local Unix socket.ssh-to-age-keys: Converts one or more SSH private keys into a deduplicatedkeys.txtage key file.sunshine-tools: Shared Sunshine helpers for socket-proxy startup and Hyprland monitor disable/restore hooks.swfs-mount-hooks: Shared mount helper forserver.storage.swfsMountprepare, stop, and health-recovery actions.take-control-viewer: Remote support viewer for N-able Take Control via Wine.virtualisation-tools: Shared libvirt and VFIO hook helpers for CPU isolation, GPU detach/attach, and guest hook dispatch.wait-for-io-tools: Shared reachability and database-readiness helpers for IO Guardian-managed services.wlprop: Wayland helper that selects a visible Hyprland window region withslurpand prints matching client JSON.
Operational Notes / Assumptions
Key Options/Knobs
Custom packages may expose different build options depending on their derivation definition.
Common Workflows
- Adding a Package: Create a new directory in
pkgs/with adefault.nixfile. - Using a Package: Reference the package via
pkgs.<name>if thepkgsoverlay is active. - Package CI discovery: The package build workflow enumerates package names lazily from
packages.<system>and skips entries whosemeta.brokenevaluation fails or resolves totrue, so one package that cannot be evaluated does not abort the entire build matrix.
Overlays Overview
Purpose
Overlays allow us to extend or modify the standard nixpkgs collection. We use them to add our custom packages, apply patches, or override package versions.
Architecture / Services / Scope
Entry Points
overlays/: Directory containing individual overlay definitions.overlays/default.nix: The main entry point for the overlays. It composes additions (frompkgs/and external inputs) and modifications (overrides for upstream packages).
Operational Notes / Assumptions
Key Options/Knobs
Overlays themselves don’t typically have “knobs,” but they affect the available packages and their versions in the pkgs set.
Notable Overrides
kernelPackages.universal-pidff: Pinned to upstream commit595c65bbfrommain. Provides a newer force-feedback kernel module driver than the version bundled in the current nixpkgs release.discord: Overridden to enable OpenASAR and Vencord.hermes-agent: Local overlay that builds Hermes Agent from upstream source plus patches:hermes-desktop(pkgs/default.nix): Routes topkgs.hermes-agent.hermesDesktop, exposing the patched Hermes Desktop package as a top-levelpkgsentry for use in home-manager configs.fastembed-hermes: Overridespython312Packages.fastembedto strip Python deps already bundled in the Hermes sealed uv2nix environment (e.g.snowballstemmer). Avoids plugin/core package collision checks during Hermes plugin builds likenixai.
Common Workflows
- Adding an Overlay: Create a new
.nixfile in theoverlays/directory. - Applying an Overlay: Overlays are typically applied in the
flake.nixconfiguration for NixOS or Home-Manager.
Hosts Overview
Purpose
This section covers configuration of individual host machines. Repository uses automatic discovery system to manage hosts based on device type.
Architecture / Services / Scope
Entry Points
hosts/: Root directory for all host configurations.hosts/desktop/: Configurations for desktop systems.hosts/laptop/: Configurations for laptop systems.hosts/server/: Configurations for server systems.hosts/shared/: Shared host configuration still used across multiple hosts.hosts/secrets.yaml: Root-level encrypted secrets for host configurations.
Configuration Structure
Host-specific configurations live in hosts/{device-type}/{hostname}/default.nix.
Operational Notes / Assumptions
Common Workflows
- Adding new host: Create directory for host in appropriate device type category and add
default.nix. - Modifying host: Update
default.nix, associated files in host directory, or relevant module undermodules/nixos/core/.
Decky Loader Lifecycle
When jovian.decky-loader.enable = true is set on host with core.gaming.enable = true, Decky Loader is not started automatically at boot. Instead it is managed in lock-step with Steam desktop application:
-
modules/nixos/core/gaming.nix— overrides Jovian-provideddecky-loader.serviceto remove it frommulti-user.target, suppresses noisy CSS_Loader health-check log spam viaLogFilterPatterns, and adds polkit rule that permits only configured Steam user in active local session to start/stop system service without password prompt. All of this is behindlib.mkIf (config.jovian.decky-loader.enable)guard, so it is no-op on machines without Jovian. -
Home-Manager shared module injected by
modules/nixos/core/gaming.nix— definesdecky-loader-steam-watchsystemd user service, active for duration of graphical session. It polls~/.steam/steam.pidevery 3 seconds to detect Steam starting, then startsdecky-loader.service, and usestail --pidto block until Steam exits before stopping it again. Service is only enabled whenosConfig.jovian.decky-loader.enableis true.
Log filtering
CSS_Loader plugin health-checks Steam’s internal web interface (port 8080) every few seconds. When Steam is not running these produce continuous journal noise of form:
[CSS_Loader] [FAIL] [css_browserhook.py:437] [Health Check] Cannot connect to host 127.0.0.1:8080 …
This is suppressed with following LogFilterPatterns entry on service (requires systemd ≥ 255):
LogFilterPatterns = "~\\[CSS_Loader\\].*\\[Health Check\\].*Cannot connect";
References
Server Hosts
Purpose
Documentation of the individual server host machines managed by this repository.
Architecture / Services / Scope
Entry Points
- NixAI: AI agent and inference host
- NixArr: Media management and playback host
- NixCloud: Application host, user-facing cloud services
- NixDev: Development, CI, and registry host
- NixIO: Ingress host, reverse proxy, and network gateway
- NixMon: Monitoring and observability host
- NixStor: Storage host, SeaweedFS evaluation
Configuration Structure
Host-specific configurations live in hosts/server/{hostname}/default.nix.
Services are gated per host via flake allocations (see Flake Allocations) so that each service runs on exactly one host.
Operational Notes / Assumptions
- Servers depend on the Database Coordinator for startup ordering (see IO Guardian).
- Shared secrets for all server hosts are stored in the shared file.
References
nixai — AI Agent Host
Purpose
nixai is the AI infrastructure host.
It runs the AI agent, local inference via Ollama, the Open WebUI chat interface, Mnemosyne memory sync, and voice services.
Entry Point
- Main file:
hosts/server/nixai/default.nix - Supporting files:
ai-agent.nix,backend.nix,mnemosyne.nix,voice.nix,web.nix
Architecture / Services / Scope
AI Agent
- Containerized AI agent with a web dashboard, an OpenAI-compatible API server, and a webhook platform.
- Memory is enabled, backed by Mnemosyne sync.
- Voice is enabled, using the local Wyoming STT server.
- A hardened SSH daemon is provisioned inside the container for agent administration.
Local Inference
- Runs local models on the host’s AMD iGPU (ROCm/Vulkan), loading a small set of chat and embedding models.
- Used by Open WebUI and the agent for local model access.
Open WebUI (web.nix)
- User-facing chat interface with RAG, web search (via SearXNG), and local Whisper STT.
- Backed by a PostgreSQL database on the Database Coordinator and tied into the database availability target so it only starts when the DB is reachable.
Mnemosyne (mnemosyne.nix)
- Mnemosyne memory provider sync server, used by the agent for persistent memory. Served on its own vhost.
Voice (voice.nix)
- Wyoming Piper (TTS) and faster-whisper (STT) servers exposed over TCP for local voice assistants.
Secrets
Declared secrets
| Secret key | Purpose |
|---|---|
AI_AGENT/AZURE_FOUNDRY_API_KEY | Model API key (Azure Foundry) |
AI_AGENT/AZURE_FOUNDRY_BASE_URL | Model API base URL |
AI_AGENT/OPENROUTER_API_KEY | OpenRouter model API key |
AI_AGENT/DISCORD_BOT_TOKEN | Discord bot token |
AI_AGENT/API_SERVER_TOKEN | Agent API server auth |
MCP/N8N_API_KEY | MCP access to n8n |
MCP/API_TOKEN | MCP bridge API token |
MCP/HASSIO_TOKEN | Home Assistant MCP token |
MCP/GITHUB_TOKEN | GitHub MCP token |
MCP/ANILIST_TOKEN | AniList MCP token |
MNEMOSYNE_SYNC_KEY | Mnemosyne sync API key |
Operational Notes / Assumptions
- Ollama relies on the AMD iGPU; ROCm on the iGPU is noted as unreliable upstream, so the config pins an override and waits on a Vulkan fix.
- Open WebUI, the agent dashboard, Mnemosyne, and the voice/whisper endpoints are exposed through the IO Coordinator reverse proxy.
- The agent’s SSH port is opened to the subnet for administration.
References
nixarr — Media Management
Purpose
nixarr is the media management host for the fleet.
All downloading happens through a VPN tunnel so P2P traffic is isolated.
Entry Point
- Main file:
hosts/server/nixarr/default.nix
Architecture / Services / Scope
Playback
- Jellyfin: Media server with hardware-accelerated transcoding (VA-API via
/dev/dri/renderD128), hardware encoding for HEVC and hardware decoding for AV1/H.264/HEVC/VP9.
Media Management (“arr” stack)
| App | Role |
|---|---|
| Radarr | Movie management |
| Sonarr | TV series management |
| Prowlarr | Indexer management for the whole stack |
| Lidarr | Music management |
| Readarr | Book management |
| Bazarr | Subtitle management |
| Transmission | BitTorrent downloader (Flood UI, cross-seed) |
| Sabnzbd | Usenet downloader |
| Seerr | User-facing media request portal |
Networking / VPN
- All downloading apps run inside a WireGuard VPN namespace (
vpnNamespaces.wg) so P2P/usenet traffic egresses through the VPN rather than the host’s normal connection. - The VPN config is provided as a sops binary secret (
wg.conf) that restartswg.servicewhen rotated. - Access to VPN-isolated apps is allowed from the LAN and the configured tailnet; see the Tailscale module documentation for the cluster’s tailnet integration.
Authentication
- An
arr-servicesKanidm OAuth2 context restricts the media apps to thesysadmingroup on the Identity Coordinator.
Secrets
Declared secrets
| Secret key | Purpose |
|---|---|
wireguard | WireGuard VPN config (binary, wg.conf) |
Operational Notes / Assumptions
- VPN download services restart on failure and wait for
wg.serviceto come up before starting, so they don’t fail during early boot when networking isn’t ready. - Hardware transcoding relies on the host exposing a working
/dev/dridevice. - Media apps are exposed through the IO Coordinator reverse proxy.
References
NixAuth
nixcloud - Application Server
Purpose
NixCloud is an application server hosting user-facing cloud services. General purpose application server for replacing cloud services with self-hosted alternatives.
Entry Point
- Main file:
hosts/server/nixcloud/default.nix
Architecture / Services / Scope
Application Workloads
| Workload | Service | File | Domain |
|---|---|---|---|
| Home Assistant | home-assistant | hosts/server/nixcloud/home-assistant/ | hassio.racci.dev |
| Homebox | homebox | hosts/server/nixcloud/homebox.nix | homebox.racci.dev |
| Immich | immich | hosts/server/nixcloud/immich.nix | photos.racci.dev |
| Music | navidrome | hosts/server/nixcloud/music.nix | music.racci.dev |
| Nextcloud | nextcloud | hosts/server/nixcloud/nextcloud.nix | nc.racci.dev |
| Search | searx | hosts/server/nixcloud/search.nix | search.racci.dev |
Operational Notes / Assumptions
- All apps are exposed via the cluster’s reverse proxy on the IO Coordinator.
- Applications using OAuth2/OIDC/SSO use the Identity Coordinator for authentication and authorization.
- Database services for these apps are provided by the cluster Database Coordinator via
server.database.postgres.*. - Media storage for Nextcloud and Immich uses seaweedfs mounts on the Storage Coordinator.
References
- Server Module
- Identity Module
- Proxy Module
- Identity Coordinator
- Database Coordinator
- Storage Coordinator
NixDB
nixdev — Development & CI
Purpose
nixdev is the development and CI host.
It runs the self-hosted development services: continuous integration, Coder workspace platform, workflow automation, a Docker registry, and a forgesync mirror job.
Entry Point
- Main file:
hosts/server/nixdev/default.nix - Supporting files:
automation.nix,ci.nix,coder.nix,forgesync.nix,registry.nix,woodpecker.nix
Architecture / Services / Scope
CI / Automation
- Woodpecker CI: Self-hosted CI server + local Docker-backed agent, supporting GitHub and Codeberg forges. Served on its own vhost with a separate gRPC agent endpoint.
- GitHub Actions runners: A pool of 10 self-hosted
nixos-runner-*runners for thenix-configrepo.
Workspaces
- Coder: Self-hosted development workspaces, backed by the Docker daemon. Coder users are granted Docker access.
Workflow Automation
- n8n: Workflow automation with task runners, backed by PostgreSQL and Redis on the Database Coordinator.
Registry & Mirroring
- Docker Registry: Self-hosted OCI registry storing images on the Storage Coordinator, with htpasswd auth.
- Forgesync: Mirrors repositories between Codeberg and GitHub on a daily schedule.
Secrets
Declared secrets
| Secret key | Purpose |
|---|---|
GITHUB_TOKEN | Token for the self-hosted runners |
POSTGRES/N8N_PASSWORD | n8n database password |
POSTGRES/CODER_PASSWORD | Coder database password |
POSTGRES/WOODPECKER_PASSWORD | Woodpecker database password |
POSTGRES/WINDMILL_PASSWORD | Windmill database password |
REDIS_PASSWORD | n8n Redis password |
N8N/ENCRYPTION_KEY | n8n encryption key |
N8N/RUNNER_AUTH_TOKEN | n8n task runner auth |
WOODPECKER/GRPC_SECRET | Woodpecker gRPC secret |
WOODPECKER/AGENT_SECRET | Woodpecker agent secret |
WOODPECKER/GITHUB_CLIENT / GITHUB_SECRET | GitHub forge OAuth |
WOODPECKER/CODEBERG_CLIENT / CODEBERG_SECRET | Codeberg forge OAuth |
REGISTRY/SECRET | Registry shared secret |
REGISTRY/HTPASSWD | Registry auth htpasswd |
REGISTRY/S3_ACCESS_KEY / S3_SECRET_KEY | S3 storage credentials |
FORGESYNC/SOURCE_TOKEN / TARGET_TOKEN / MIRROR_TOKEN | Forgesync tokens |
Operational Notes / Assumptions
- Runs Docker with auto-pruning for workspaces and CI workloads.
- Woodpecker, n8n, Coder, the registry, and CI are exposed through the IO Coordinator reverse proxy.
- Databases are provided by the Database Coordinator; n8n and Woodpecker depend on the database availability target.
References
nixio - Ingress Host
Purpose
NixIO serves as the cluster’s primary ingress and network gateway. It handles all external traffic routing, VPN tunnelling, DNS filtering, and dashboard aggregation for the server fleet.
Entry Point
- Main file:
hosts/server/nixio/default.nix
Architecture / Services / Scope
Services
| Service | Module / Path | Role |
|---|---|---|
| Caddy | hosts/server/nixio/proxy.nix | Reverse-proxy and TLS termination for all cluster services |
| Tailscale Tunnel | hosts/server/nixio/tunnel/ | Mesh VPN connectivity, subnet routing, and ingress via Tailscale tags |
| Dashy Dashboard | hosts/server/nixio/dashboard.nix | Aggregated service dashboard displayed on the IO Coordinator |
| AdGuard Home | hosts/server/nixio/adguard.nix | Local DNS filtering and ad-blocking for the home network |
| Network Config | default.nix | Subnet declarations, IP forwarding (IPv4 + IPv6) |
Secrets
Declared secrets
| Secret key | Purpose |
|---|---|
CLOUDFLARE/EMAIL | ACME DNS challenge account email |
CLOUDFLARE/ZONE_API_TOKEN | ACME DNS challenge zone token |
CLOUDFLARE/DNS_API_TOKEN | ACME DNS challenge API token |
Operational Notes / Assumptions
- This host is expected to have stable upstream network access plus reachability to the cluster LAN and tailnet, because ingress, DNS, and tunnel traffic all terminate here.
- Caddy terminates public TLS for cluster services, while some backends also use separate internal TLS or mTLS; certificate trust and backend server names must stay aligned with those upstream services.
- Reverse-proxied services remain individually responsible for their own authn/authz. Publishing a route here does not replace service-level access controls.
References
nixmon — Monitoring
Purpose
nixmon is the monitoring primary host for the cluster.
It runs the observability stack of metrics, logs, alerting, and dashboards.
Entry Point
- Main file:
hosts/server/nixmon/default.nix
Architecture / Services / Scope
Uptime Monitoring
- Uptime Kuma: Service status monitoring.
Observability Stack
As the monitoringPrimaryHost, nixmon runs the collector services defined by the Server Monitoring.
Secrets
Declared secrets
| Secret key | Purpose |
|---|---|
MONITORING/OLTP/BEARER_TOKEN | Bearer token for OTLP ingestion |
MONITORING/GRAFANA/SECRET_KEY | Grafana session secret |
MONITORING/GRAFANA/OAUTH_SECRET | Kanidm OAuth2 secret for Grafana |
MONITORING/HOME_ASSISTANT/WEBHOOK_URL | Home Assistant alert webhook |
MONITORING/NEXTCLOUD_TALK/WEBHOOK_URL | Nextcloud Talk alert webhook |
MONITORING/MINIO_PROMETHEUS_TOKEN | Prometheus token for MinIO metrics |
PROXMOX/USER | Proxmox API user |
PROXMOX/TOKEN_ID | Proxmox API token ID |
PROXMOX/TOKEN_SECRET | Proxmox API token secret |
S3FS_AUTH/LOKI | S3 credentials for Loki storage |
Operational Notes / Assumptions
- Grafana requires a matching
KANIDM/OAUTH2/GRAFANA_SECRETin the Identity Coordinator’s provisioning so the OAuth2 client works. - Monitoring web UIs are exposed through the IO Coordinator reverse proxy.
References
nixserv — Nix Build Server
Purpose
nixserv is the dedicated binary cache and distributed build server for the cluster.
It runs Attic backed by PostgreSQL and S3 storage, and acts as a remote build daemon for distributed Nix builds.
Entry Point
- Main file:
hosts/server/nixserv/default.nix
Architecture / Services / Scope
Attic Binary Cache
- Attic: Self-hosted Nix binary cache server, served at
cache.racci.dev. - Storage is backed by the Storage Coordinator with zstd chunked/compressed NAR storage.
- The Attic database uses PostgreSQL on the Database Coordinator.
- Requires proof-of-possession for uploads, with a 14-day default garbage-collection retention period run on a schedule.
Distributed Builds
As a distributedBuilders host, nixserv exposes the builder user over SSH.
Other hosts configure nix.distributedBuilds and connect to it as a remote build machine (ssh-ng) to offload Nix builds.
Secrets
Declared secrets
| Secret key | Purpose |
|---|---|
ATTIC_ENVIRONMENT | Attic environment file |
POSTGRES/ATTIC_PASSWORD | Attic database password |
Operational Notes / Assumptions
- The cache endpoint and the build daemon rely on the Storage Coordinator for object storage and the Database Coordinator for the metadata database.
- The Attic HTTP endpoint is served through the IO Coordinator reverse proxy.
References
- Attic — Nix binary cache server
- Distributed Builds
- Database Coordinator
- Storage Coordinator
- IO Coordinator
NixStor
Lib Overview
Purpose
The lib directory contains custom Nix functions and builders used throughout the repository to simplify configuration and reduce duplication.
Architecture / Services / Scope
Entry Points
lib/: Root directory for lib functions.attrsets.nix: Functions for manipulating and merging attribute sets.default.nix: Main entry point providing themineandbuildersnamespaces.files.nix: Utilities for filesystem operations and path handling.hardware.nix: Detection and configuration helpers for hardware acceleration and drivers.hypr.nix: Specialized helpers for Hyprland window manager configurations.keys.nix: Management of SSH, GPG, and other cryptographic keys.package.nix: Custom package definitions and derivation helpers.persistence.nix: Helpers for managing path persistence in ephemeral (TempFS) environments.strings.nix: String manipulation and formatting utilities.
lib/builders/: Specialized builders for system and home configurations. Builders forward shared module arguments (e.g.importExternalsand repo-level args) into both NixOS and nested Home ManagerextraSpecialArgs, enabling modules that conditionally import external inputs.
Operational Notes / Assumptions
Key Options/Knobs
The functions in lib take various arguments depending on their purpose. Builders typically take parameters for hostnames, user names, and modules.
Common Workflows
- Using a Lib Function: Access functions via
outputs.lib.<functionName>or by importing the relevant file. - Creating a Builder: Add new builder logic to
lib/builders/.