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
sudo nixos-rebuild switch --flake /etc/nixos#winix
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
The IO Guardian system ensures that services across the infrastructure are aware
of the availability of centralized databases (PostgreSQL and Redis) hosted on config.server.ioPrimaryHost.
It provides graceful startup and shutdown coordination between the database host and dependent services on other servers.
Overview
The system consists of two components:
-
Guardian Server (runs on client servers)
- WebSocket server that listens for commands from the coordinator
- Executes drain/undrain commands by controlling
io-databases.target
-
Guardian Client (runs on the IO Host)
- 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-io-databases.service - This service waits (with retries) until PostgreSQL and Redis on the IO Host are reachable
- Once databases are confirmed available, the service completes
- The
io-databases.targetis now ready to be activated - When the IO Hosts
io-database-coordinator.servicestarts, it sendsundrainto all clients - Clients start
io-databases.target, which starts all dependent services
Database Shutdown (Graceful Drain)
- When
io-database-coordinator.servicestops (before databases stop) - It connects to all guardian servers via WebSocket
- Sends
draincommand to each server - Guardian servers stop
io-databases.target - Dependent services stop gracefully before databases go down
Database Startup (Undrain)
- When databases come online on the IO Host
io-database-coordinator.servicestarts- It sends
undraincommand to all guardian servers - Guardian servers start
io-databases.target - All dependent services start
Security
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.
Generating the PSK
Generate a new PSK using OpenSSL:
openssl rand -base64 32
Adding the Secret
Add the generated PSK to hosts/server/secrets.yaml:
IO_GUARDIAN_PSK: <your-generated-key>
Then encrypt the file:
sops --encrypt --in-place hosts/server/secrets.yaml
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:
{
server.database.dependentServices = [
"my-service"
"another-service"
];
}
Services listed here will:
- Start only when
io-databases.targetis active - Stop when
io-databases.targetstops - Restart when the target restarts
Systemd Units
On Client Servers
| Unit | Type | Description |
|---|---|---|
io-guardian.service | simple | WebSocket server for receiving commands |
io-databases.target | target | Represents “databases are online” |
wait-for-io-databases.service | oneshot | Waits for databases at boot (runs once) |
On nixio
| Unit | Type | Description |
|---|---|---|
io-database-coordinator.service | oneshot | Sends undrain on start, drain on stop |
Troubleshooting
Checking Guardian Status
On client servers:
systemctl status io-guardian.service
systemctl status io-databases.target
systemctl status wait-for-io-databases.service
journalctl -u io-guardian.service -f
On IO Hosts:
systemctl status io-database-coordinator.service
journalctl -u io-database-coordinator.service
Manual Commands
To manually start dependent services on a client:
systemctl start io-databases.target
To manually stop dependent services:
systemctl stop io-databases.target
Common Issues
Guardian server won’t start:
- Check that
IO_GUARDIAN_PSKsecret is properly configured - Verify the sops decryption is working:
cat /run/secrets/IO_GUARDIAN_PSK
Services not starting after boot:
- Check wait service
logs:
journalctl -u wait-for-io-databases.service - Verify network connectivity to an IO Host on ports 5432 (Postgres) and 6379 (Redis)
- Ensure an IO Hosts 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
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"}
// or
{"type": "auth", "status": "error", "message": "Invalid key"}
Commands
// Coordinator sends:
{"type": "command", "action": "drain"}
// or
{"type": "command", "action": "undrain"}
// or
{"type": "command", "action": "ping"}
// Server responds:
{"type": "response", "action": "<action>", "status": "ok", "message": "..."}
// or
{"type": "response", "action": "<action>", "status": "error", "message": "..."}
Server Cluster Monitoring
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.
Overview
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
- Caddy access logs are parsed as JSON at ingest time so
detected_level,logger, andstatusare available in Loki - Ingest-time log parsing for journal
stdoutentries and Caddy access logs to inferdetected_leveland normalize common timestamp formats - Application-specific exporters (Caddy, PostgreSQL, Redis) enabled automatically
- fail2ban exporter available on the IO primary host (when fail2ban is enabled)
-
Collectors (run on the monitoring primary host)
- Prometheus for metrics aggregation with 90-day retention
- 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 primary host)
- Grafana with provisioned datasources and dashboards
- Native Kanidm OAuth2 authentication
Architecture
┌─────────────────────────────────────────────────────┐
│ nixmon (Monitoring Primary) │
│ ┌──────────┐ ┌──────┐ ┌─────────┐ ┌──────────┐ │
│ │Prometheus│ │ Loki │ │ Grafana │ │Alertmgr │ │
│ │ :9090 │ │:3100 │ │ :3000 │ │ :9093 │ │
│ └────┬──┬──┘ └──┬───┘ └─────────┘ └────┬─────┘ │
│ │ │ │ │ │
│ ┌────┘ │ ┌────┘ ┌───────────────┘ │
│ │ scrape│ │ push │ webhooks │
├──┼───────┼───┼─────────────┼────────────────────────┤
│ ▼ ▼ ▼ ▼ │
│ All servers: Home Assistant / Nextcloud │
│ - node_exporter :9100 │
│ - alloy → Loki │
│ - OTLP/HTTP → Alloy :4318 │
│ - caddy metrics :2019 (if proxy configured) │
│ - fail2ban_exporter :9191 (if fail2ban enabled) │
│ - postgres_exporter :9187 (if postgres configured) │
│ - redis_exporter :9121 (if redis configured) │
│ - pve_exporter :9221 (nixmon only, Proxmox API) │
└─────────────────────────────────────────────────────┘
Configuration
Enabling Monitoring
Monitoring is enabled by default on all servers (server.monitoring.enable = true).
The monitoring primary host is configured via the allocations.server.monitoringPrimaryHost
option, currently set to nixmon.
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.
Auto-Detection
The module automatically detects and enables exporters based on host role:
- Caddy exporter: Enabled when
server.proxy.virtualHostsis non-empty - PostgreSQL exporter: Enabled on the IO primary host when postgres databases are configured
- Redis exporter: Enabled on the IO primary host when redis instances are configured
- Caddy access logs: Enabled when Caddy metrics/logs are enabled; each access log file under
/var/log/caddy-access-*is shipped to Loki and parsed as JSON - node_exporter process collector: Enabled on all servers via the
processescollector to expose per-process stats - fail2ban exporter: Enabled on the IO primary host when fail2ban intrusion detection is enabled
- Collector services: Enabled only on the monitoring primary host
Secrets
The monitoring module requires the following secrets in hosts/server/nixmon/secrets.yaml:
MONITORING:
OLTP:
BEARER_TOKEN: <random-secret-key>
GRAFANA:
SECRET_KEY: <random-secret-key>
OAUTH_SECRET: <kanidm-oauth2-secret>
HOME_ASSISTANT:
WEBHOOK_URL: <ha-webhook-url>
NEXTCLOUD_TALK:
WEBHOOK_URL: <nc-talk-webhook-url>
PROXMOX:
USER: <proxmox-user-at-realm>
TOKEN_ID: <proxmox-token-name>
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/nixcloud/secrets.yaml
under KANIDM/OAUTH2/GRAFANA_SECRET (the Kanidm provisioning side).
Caddy Virtual Hosts
The module configures four virtual hosts on nixmon:
| 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
primary host’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
Troubleshooting
Checking Service Status
On the monitoring host (nixmon):
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
GRAFANA_OAUTH_SECRETin nixmon matchesKANIDM/OAUTH2/GRAFANA_SECRETin nixcloud - 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 nixmon and the target host
Proxmox metrics missing:
- Verify
proxmox/token_idandproxmox/token_secretare valid - Check PVE API is accessible from nixmon:
curl -k https://pve.<domain>/api2/json - Review PVE exporter logs:
journalctl -u prometheus-pve-exporter.service
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";
};
# 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
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
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.
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
This section covers all NixOS modules provided by this flake.
NixOS Services
This section documents the custom NixOS service modules available in this configuration. These modules provide specialised integrations and monitoring capabilities.
Nested service modules emit generated fragments such as services-ai-agent-options.md.
AI Agent
AI Agent
Autonomous AI Agent service powered by Hermes, providing intelligent task automation with security controls for code review and development tasks.
- Entry point:
modules/nixos/services/ai-agent.nix - Upstream: Hermes Agent
- Package: The module routes
services.hermes-agent.packagethrough the localpkgs.hermes-agentoverlay, which carries the lazy-deps managed-install fix from PR #48637. This ensures Hermes fails fast withFeatureUnavailableon read-only NixOS installs rather than retryingensurepip.
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.plugins
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable enable extra plugins for the agent.
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" |
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 | "stepfun/step-3.5-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.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.
Secrets Management
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.environmentFile = config.sops.templates."HERMES_ENV".path;
Usage Example
{ ... }: {
services.ai-agent = {
enable = true;
};
}
Voice & STT
Enable voice input and output with services.ai-agent.voice.enable = true;.
Wyoming STT (Reuse Existing Server)
Instead of running a separate Whisper instance inside the Hermes container, you can point Hermes at an existing Wyoming faster-whisper server (e.g. the one already running on nixai at port 10300):
{ ... }: {
services.ai-agent = {
enable = true;
voice = {
enable = true;
wyoming-stt.enable = true;
};
};
}
This sets HERMES_LOCAL_STT_COMMAND to invoke wyoming-transcribe, which sends audio over the Wyoming protocol to the faster-whisper server and returns the transcript. No second Whisper process needed.
Dashboard Service
Enable the web dashboard with services.ai-agent.dashboard.enable = true;.
This adds a separate hermes-dashboard systemd service that runs docker exec into the hermes-agent container to serve the dashboard under the hermes user. Environment files configured via services.hermes-agent.environmentFiles are loaded by systemd’s EnvironmentFile directive (read as root) and passed into the container via docker exec --env-file. The dashboard stays local by default and does not open a browser.
OIDC Authentication
Enable OpenID Connect authentication for the dashboard with services.ai-agent.dashboard.oidc.enable = true;.
The dashboard uses a public PKCE client (no client_secret). The client ID is a public identifier — it does not need to be stored as a secret.
{ ... }: {
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" ];
};
};
};
}
The module generates a HERMES_DASHBOARD_OIDC_ENV environment file with the OIDC settings, which is loaded by the hermes-dashboard service.
Memory (Mnemosyne)
Enable long-term memory with services.ai-agent.memory.enable = true;. This switches the memory provider from the built-in user profile (USER.md injection) to Mnemosyne, a local SQLite-backed memory system with semantic recall.
{ ... }: {
services.ai-agent = {
enable = true;
memory.enable = true;
};
}
Database location: /home/hermes/mnemosyne.db (SQLite with FTS5 hybrid ranking + vector search).
Huntress
Huntress
Managed EDR (Endpoint Detection and Response) platform that protects systems by detecting malicious footholds used by attackers.
- Entry point:
modules/nixos/services/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.
Usage Example
{ config, ... }: {
services.huntress = {
enable = true;
accountKeyFile = config.sops.secrets.huntress_account_key.path;
organisationKeyFile = config.sops.secrets.huntress_org_key.path;
};
}
Operational Notes
The agent configuration is generated at /etc/huntress/agent_config.yaml during the service’s preStart phase. It merges the provided account and organisation keys using yaml-merge. The keys are securely loaded into the service using systemd LoadCredential.
MCPO
MCPO (Model Context Protocol Orchestrator)
Orchestrates Model Context Protocol (MCP) servers, providing a centralized way to manage and expose multiple MCP servers.
- Entry point:
modules/nixos/services/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.
Usage Example
{ config, ... }: {
services.mcpo = {
enable = true;
configuration = {
everything = config.services.mcpo.helpers.npxServer "@modelcontextprotocol/server-everything";
};
};
}
Operational Notes
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.
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.
Metrics
Metrics & Hacompanion
Comprehensive metrics collection and integration with Home Assistant via hacompanion.
- Entry point:
modules/nixos/services/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.
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;
};
};
}
Operational Notes
Hacompanion uses a generated TOML configuration file and securely loads the Home Assistant API token from sops.secrets.HACOMPANION_ENV. The upgradeStatus feature can also integrate with Uptime Kuma to provide heartbeat notifications for successful system upgrades.
Tailscale
Tailscale
Extensions to the standard NixOS Tailscale module, providing easier tag management.
- Entry point:
modules/nixos/services/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.
Usage Example
{ ... }: {
services.tailscale = {
enable = true;
tags = [ "server" "internal" ];
};
}
Operational Notes
This module simplifies the application of Tailscale tags by automatically constructing the --advertise-tags flag. Ensure that the device has the necessary permissions in your Tailscale ACLs to apply the requested tags.
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.generators.enable
| Type | boolean |
| Default | config.core.enable |
| Example | true |
Whether to enable generators configuration.
core.generators.proxmoxLXC.clearPath
| Type | absolute path |
| Default | getExe' pkgs.busybox "clear" |
Clear binary to use for clearing the screen when asking the user for the SSH private key
core.generators.proxmoxLXC.enable
| Type | boolean |
| Default | cfg.enable && config.host.device.isVirtual |
| Example | true |
Whether to enable Proxmox LXC generator configuration.
core.generators.proxmoxLXC.sedPath
| Type | absolute path |
| Default | getExe' pkgs.busybox "sed" |
Sed package to use for validating the SSH private key provided by the user
core.generators.proxmoxLXC.sshKeygenPath
| Type | absolute path |
| Default | getExe' pkgs.openssh "ssh-keygen" |
SSH package to use for validating the SSH private key provided by the user
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.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/dird8mmzmya8m99dcd98bisvxy93rngj-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.
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.
Key Pages
- Activation
- Auto Upgrade
- Containers
- Display Manager
- Gaming
- Generators
- Default Groups
- Locale
- Nix
- OpenSSH
- Printing
- Remote Access
- Security
- SOPS
- Stylix
- Virtualisation
- WSL
Usage Example
{ ... }: {
core = {
enable = true;
audio.enable = true;
bluetooth.enable = true;
network.enable = true;
};
}
Notes
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.
- Entry point: activation.nix
Overview
This module adds activation-time diff reporting with nvd. During activation it compares previous and new system generations and prints package and closure changes.
Options
core.activation.enable
| Type | boolean |
| Default | config.core.enable |
| Example | true |
Whether to enable report diff on activation.
Behaviour
When enabled, module installs system.activationScripts.report-changes that:
- finds previous and newest system profile links under
/nix/var/nix/profiles, - resolves both links to store paths, and
- runs
nvd diffbetween them.
If no previous generation exists yet, script does nothing.
Usage Example
{ ... }: {
core.activation.enable = true;
}
Operational Notes
- Diff output is informational only. Script ends with
|| true, so activation does not fail ifnvd diffreturns non-zero. - Default follows top-level
core.enable, so most hosts get generation diff reporting automatically.
Auto Upgrade
Schedules automatic NixOS upgrades from flake host outputs.
- Entry point: auto-upgrade.nix
Overview
This module configures system.autoUpgrade to rebuild host from github:DaRacci/nix-config#<host>. It also applies resource limits to nixos-upgrade.service so scheduled upgrades run with lower CPU and IO priority.
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
Behaviour
When enabled, module configures:
system.autoUpgrade.dates = "04:00",randomizedDelaySec = "45min"to spread out upgrade times across hosts,- flags
--refresh,--accept-flake-config, and--no-update-lock-file, - service resource controls for
nixos-upgrade.service.
Auto-upgrade itself only turns on when flake has self.rev, meaning repository is in clean revisioned state.
Usage Example
{ ... }: {
core.auto-upgrade = {
enable = true;
hostName = "my-host";
};
}
Operational Notes
- Dirty working trees or non-revisioned evaluations leave
system.autoUpgrade.enable = false. - Module always points upgrades at GitHub flake source, not local checkout.
- Resource limits set
CPUWeight = 20,CPUQuota = 65%, andIOWeight = 20on upgrade service.
Containers
Enables Docker-based container runtime defaults.
- Entry point: containers.nix
Overview
This module turns on Docker as primary container backend and configures OCI containers to use Docker. It also enables weekly image pruning and persists Docker state directories.
Options
core.containers.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable container support.
Behaviour
When enabled, module:
- enables
virtualisation.docker, - sets
virtualisation.docker.package = pkgs.docker, - enables CDI support with
daemon.settings.features.cdi = true, - enables weekly
docker autoPrune, - sets
virtualisation.oci-containers.backend = "docker", - adds
dockertocore.defaultGroups, and - persists Docker state under
/var/lib/docker.
Persisted directories include overlay2, image, volumes, containers, containerd, and buildkit.
Usage Example
{ ... }: {
core.containers.enable = true;
}
Operational Notes
- Module intentionally prefers Docker because current workloads still need features not covered by Podman or
podman-compose. - Users receive Docker access through shared
core.defaultGroupshandling.
Display Manager
Configures display manager for graphical sessions on desktop and laptop hosts.
- Entry point: display-manager.nix
Overview
This module sets up greetd with tuigreet as default display manager. It is automatically enabled on hosts where host.device.isHeadless = false.
When session packages are present in services.displayManager.sessionPackages, module also passes both Wayland and X session directories to tuigreet.
Options
core.display-manager.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable display manager configuration.
Behaviour
When enabled, greetd starts with tuigreet providing terminal-based greeter that:
- shows current time with
--time, - remembers last logged-in user with
--remember, - remembers last selected session with
--remember-session, and - adds
--sessionsand--xsessionsonly whenservices.displayManager.sessionPackagesis non-empty.
Greeter cache is persisted to /var/cache/tuigreet through host.persistence.directories.
Usage Example
{ ... }: {
services.displayManager.sessionPackages = [
pkgs.hyprland
];
core.display-manager.enable = true;
}
Operational Notes
greetdruns asgreeteruser.- Both Wayland (
wayland-sessions) and X11 (xsessions) session paths are built dynamically from installed session packages, so adding new session package is enough to make it appear in greeter.
Gaming
Enables gaming, VR, and Steam-focused desktop features.
- Entry point: gaming.nix
Overview
This module configures desktop gaming stack around Steam, 32-bit graphics support, Android ADB tools, and WiVRn streaming. It also adds firewall rules, udev rules for common gaming devices, and optional Decky Loader lifecycle integration.
Options
purpose.gaming.controllerSupport
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable controller support.
purpose.gaming.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable Gaming support base..
purpose.gaming.minecraft.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable Enable Minecraft support.
purpose.gaming.modding.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable Enable modding support.
purpose.gaming.modding.enableBeatSaber
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable Enable beatsaber modding support.
purpose.gaming.modding.enableSatisfactory
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable Enable satisfactory modding support.
purpose.gaming.modding.enableThunderstore
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable Enable thunderstore support.
purpose.gaming.osu.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable OSU!.
purpose.gaming.osu.lazerPackages
| Type | package |
| Default | <derivation osu-lazer-bin-2026.726.0> |
The package to install for OSU! Lazer
purpose.gaming.roblox.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable Enable Roblox launcher.
purpose.gaming.roblox.vinegarPackage
| Type | package |
| Default | <derivation vinegar-1.9.3> |
The package to use for Vinegar
purpose.gaming.simulator.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable Enable simulator support.
purpose.gaming.simulator.enableRacing
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable Enable Moza Racing.
purpose.gaming.steam.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable Steam.
purpose.gaming.vr.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable Enable VR support.
purpose.gaming.minecraft.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable Enable Minecraft support.
purpose.gaming.modding.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable Enable modding support.
purpose.gaming.modding.enableBeatSaber
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable Enable beatsaber modding support.
purpose.gaming.modding.enableSatisfactory
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable Enable satisfactory modding support.
purpose.gaming.modding.enableThunderstore
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable Enable thunderstore support.
purpose.gaming.osu.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable OSU!.
purpose.gaming.osu.lazerPackages
| Type | package |
| Default | <derivation osu-lazer-bin-2026.726.0> |
The package to install for OSU! Lazer
purpose.gaming.roblox.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable Enable Roblox launcher.
purpose.gaming.roblox.vinegarPackage
| Type | package |
| Default | <derivation vinegar-1.9.3> |
The package to use for Vinegar
purpose.gaming.simulator.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable Enable simulator support.
purpose.gaming.simulator.enableRacing
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable Enable Moza Racing.
purpose.gaming.steam.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable Steam.
purpose.gaming.vr.enable
| Type | boolean |
| Default | false |
| Example | true |
Whether to enable Enable VR support.
Behaviour
When enabled, module:
- adds
adbuserstocore.defaultGroups, - enables
hardware.steam-hardware, - enables 32-bit graphics support,
- installs
pkgs.android-tools, - enables Steam with Steam Deck style launch arguments,
- enables
programs.steam.extest, - adds
pkgs.xwayland-runandpkgs.xwininfoas Steam extra packages, - adds
pkgs.proton-ge-binas compatibility package, - opens Steam Remote Play and local transfer firewall rules,
- enables
services.wivrnwithhighPriority,steam.importOXRRuntimes, firewall access, and JSON config, and - installs udev rules for PlayStation controller, Oculus Quest, and tty ACM devices.
It also overlays gamescope-session to use 4K resolution and wider refresh limits, and opens firewall ports UDP 41492, 9943, 9944 plus TCP 8082, 9943, 9944, and 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.
Usage Example
{ ... }: {
core.gaming.enable = true;
}
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
- Module assumes desktop-class host with graphics stack and Steam support.
- WiVRn config uses NVENC H.265 encoder entries and enables
pkgs.wayvras application. - Some extra behavior only appears when related modules already exist, such as Jovian Decky Loader and Home Manager.
Generators
Configures image and container generator support for shared NixOS hosts.
- Entry point: generators.nix
Overview
This module imports nixos-generators formats and exposes core.generators options for generator-specific setup.
Current focus is Proxmox LXC image generation. When enabled for virtual hosts, module adds activation logic that prompts for SSH host private key on first boot, validates it with ssh-keygen, and stores it under /persist/etc/ssh/ssh_host_ed25519_key so later secret management can install it into /etc/ssh.
Options
core.generators.enable
| Type | boolean |
| Default | config.core.enable |
| Example | true |
Whether to enable generators configuration.
core.generators.proxmoxLXC.clearPath
| Type | absolute path |
| Default | getExe' pkgs.busybox "clear" |
Clear binary to use for clearing the screen when asking the user for the SSH private key
core.generators.proxmoxLXC.enable
| Type | boolean |
| Default | cfg.enable && config.host.device.isVirtual |
| Example | true |
Whether to enable Proxmox LXC generator configuration.
core.generators.proxmoxLXC.sedPath
| Type | absolute path |
| Default | getExe' pkgs.busybox "sed" |
Sed package to use for validating the SSH private key provided by the user
core.generators.proxmoxLXC.sshKeygenPath
| Type | absolute path |
| Default | getExe' pkgs.openssh "ssh-keygen" |
SSH package to use for validating the SSH private key provided by the user
Assertions and Behaviour
When core.generators.proxmoxLXC.enable = true, module asserts that image contains /etc/ssh/ssh_host_ed25519_key.pub.
If activation runs without controlling terminal, prompt is skipped and activation exits cleanly. If terminal exists, module loops until pasted private key:
- contains valid OpenSSH private key block,
- passes
ssh-keygen -y, and - matches public key already present at
/etc/ssh/ssh_host_ed25519_key.pub.
Usage Example
{ ... }: {
core.generators = {
enable = true;
proxmoxLXC = {
enable = true;
};
};
}
Operational Notes
nixos-generatorsformats are imported unconditionally by module, but runtime configuration only applies whencore.generators.enableis on.- Proxmox LXC flow is designed for images where public host key is baked into image but private key must be supplied interactively after boot.
- Stored private key lives in
/persist, so persistence and later secret deployment must be configured for target host.
Groups
Locale
Sets shared timezone and locale defaults.
- Entry point: locale.nix
Overview
This module provides opinionated regional defaults for timezone and locale. It sets Australia/Sydney timezone and enables Australian and US English UTF-8 locales.
Options
core.locale.enable
| Type | boolean |
| Default | true |
| Example | true |
Whether to enable locale configuration.
Usage Example
{ ... }: {
core.locale.enable = true;
}
Operational Notes
- Module is enabled by default.
- Because settings use
mkDefault, this module acts as baseline rather than hard override.
Nix
Defines shared Nix daemon, cache, and registry defaults.
- Entry point: nix.nix
Overview
This module establishes global Nix configuration for hosts in this flake. It sets overlays, system.stateVersion, trusted users, experimental features, binary caches, garbage collection, and registry-derived nixPath.
It also provisions cache push secret and attic-watch-store service for automatic uploads to remote Attic cache.
Options
This module does not define core.* options. It applies shared baseline configuration directly.
Behaviour
Module configures:
- overlays from
inputs.nix4vscode, system.stateVersionfromstate.versionfile in flake root,- trusted Nix users
rootand@wheel, nix.settings.auto-optimise-store = mkForce true,- experimental features
nix-command,flakes, andpipe-operator, - substituters, trusted substituters, and trusted public keys for
cache.nixos.org,nix-community, andcache.racci.dev, - daily automatic Nix GC, and
nix.nixPathderived fromconfig.nix.registry.
It also enables services.angrr to retain recent system profiles and creates systemd.services.attic-watch-store that waits for network-online.target, restarts on failure, logs into Attic with SOPS-managed CACHE_PUSH_KEY, and watches store for uploads.
Operational Notes
- Because module has no enable flag, this is always active and applied to all hosts.
attic-watch-storedepends onsops.secrets.CACHE_PUSH_KEYfromhosts/secrets.yaml.services.angrrkeeps system profiles for 14 days, latest 3 generations, current system, and booted system.
OpenSSH
Configures opinionated SSH server and client defaults.
- Entry point: openssh.nix
Overview
This module enables OpenSSH with ed25519-only host keys, disables password authentication, and generates known-host entries for all configured NixOS hosts in flake outputs.
It also wires SOPS-managed host private key into sshd and publishes matching public key under /etc/ssh/ssh_host_ed25519_key.pub.
Options
core.openssh.enable
| Type | boolean |
| Default | true |
| Example | true |
Whether to enable OpenSSH server and client opinionated configuration.
Behaviour
When enabled, module:
- enables
services.openssh, - disables socket activation (
startWhenNeeded = false) to prevent mid-connection disruptions during system configuration switches, - disables password authentication,
- sets
PermitRootLogin = "prohibit-password", - sets
GatewayPorts = "clientspecified", - configures
services.openssh.hostKeysfromconfig.sops.secrets.SSH_PRIVATE_KEY.path, - publishes current host public key at
/etc/ssh/ssh_host_ed25519_key.pub, - enables
security.pam.sshAgentAuth, - adds current host public host key to
users.users.root.openssh.authorizedKeys.keyFiles, and - generates
programs.ssh.knownHostsentries for every host inoutputs.nixosConfigurations.
Client configuration also restricts host key algorithms and accepted public key types to ssh-ed25519.
Usage Example
{ ... }: {
core.openssh.enable = true;
}
Operational Notes
- Module expects matching host public key file to exist in flake for each host.
- Current host gets
localhostas extra known-host alias in generated SSH client config. - Root authorization here uses host key material from flake, not per-user login keys.
- Private host key comes from SOPS secret
SSH_PRIVATE_KEY, socore.sopsintegration usually pairs with this module. - Socket activation disabled: By default, NixOS uses socket activation for SSH which spawns per-connection service instances (
sshd@...service). When these instances are restarted during a configuration switch, it disconnects active SSH sessions. Disabling socket activation (startWhenNeeded = false) runs SSH as a traditional always-on service, preventing remote disconnection duringnixos-rebuild switchover SSH.
Printing
Enables shared printer support for workstation-class NixOS hosts.
- Entry point: printing.nix
Overview
This module enables CUPS printing support on non-server, non-virtual hosts and installs common printer drivers used in this configuration.
When active, it turns on services.printing, adds HP and Gutenprint drivers, and includes Brother MFC-L3770CDW driver packages.
Options
core.printing.enable
| Type | boolean |
| Default | config.host.device.role != "server" && !config.host.device.isVirtual |
| Example | true |
Whether to enable printing support.
Behaviour
When both config.core.enable and core.printing.enable are true, module:
- enables
services.printing, - installs printer drivers from
pkgs.hplip,pkgs.gutenprint,pkgs.gutenprint-bin,pkgs.cups-filters,pkgs.mfcl3770cdwlpr, andpkgs.mfcl3770cdwcupswrapper, and - adds
lptocore.defaultGroups.
Usage Example
{ ... }: {
core.printing.enable = true;
}
Operational Notes
- 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.
core.defaultGroups = [ "lp" ]ensures standard users can access printer devices through shared default group handling.
Remote Access
Provides optional remote desktop and game-streaming capabilities for desktop hosts.
- Entry point: remote.nix
Overview
This module exposes core.remote with two independent sub-features:
| Sub-feature | Implementation | Purpose |
|---|---|---|
| Remote Desktop | xrdp | Full desktop access over RDP |
| Streaming | Sunshine | Low-latency game or desktop streaming |
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.
Behaviour
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 = "3", - adds two Sunshine application entries named
Shared DesktopandExclusive Desktop, - creates headless output at login via Home Manager, and
- keeps
HEADLESS-2disabled by default until Sunshine prep commands enable it.
Lua Mode Handling
If Hyprland is in Lua mode (programs.hyprland.configType = "lua"), the shared
Home Manager module uses Lua-safe equivalents:
- Startup hook:
settings.onwithhyprland.startevent triggershyprctl output create headless. - Monitor rule:
{ output = "HEADLESS-2"; disabled = true; }(attrset form instead of old string). - Screencopy permission: routed through
wayland.windowManager.hyprland.custom-settings.permission.screenCopy, which handles both Lua and hyprlang modes automatically.
For hyprlang mode, the old string forms (exec-once, monitor = "HEADLESS-2,disable") are used unchanged.
| Application | Behaviour |
|---|---|
| Shared Desktop | Enables HEADLESS-2 at client resolution and leaves physical monitors active. |
| Exclusive Desktop | Enables HEADLESS-2, saves active physical monitor state to $XDG_STATE_HOME/hyprland-disabled-monitors-pre-sunshine.json, disables physical monitors, then restores them on disconnect. |
Usage Examples
Streaming only
{ ... }: {
core.remote = {
enable = true;
streaming.enable = true;
};
}
RDP only
{ ... }: {
core.remote = {
enable = true;
remoteDesktop = {
enable = true;
startCommand = "hyprland";
};
};
}
Operational Notes
- 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.
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 sunshine-proxy-wrapper 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.
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
Applies shared host security defaults.
- Entry point: security.nix
Overview
This module enables baseline security features such as sudo-rs, TPM2 support, Polkit, kernel protection flags, and open-file limits for users.
Options
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.
Usage Example
{ ... }: {
core.security = {
enable = true;
userLimit = 65536;
};
}
Operational Notes
- Module leaves
security.lockKernelModules = falseeven while enabling other hardening defaults. userLimitaffects both PAM sessions and user systemd services, keeping file descriptor limits aligned.
SOPS
Configures shared SOPS and age decryption defaults.
- Entry point: sops.nix
Overview
This module imports sops-nix, sets host default secrets file, derives age SSH key paths from persisted host SSH keys, and declares managed SSH private key secret for OpenSSH.
Options
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/dird8mmzmya8m99dcd98bisvxy93rngj-source/hosts/secrets.yaml" |
Where the SOPS secret file of this host is located in the flake.
Behaviour
When enabled, module:
- imports
inputs.sops-nix.nixosModules.sopsunless function argumentimportExternals = false, - sets
sops.defaultSopsFiletocore.sops.hostSecretsFile, - builds
sops.age.sshKeyPathsfrom persisted host SSH key path first, then appends any configured ed25519 OpenSSH host keys, and - declares
sops.secrets.SSH_PRIVATE_KEYat/etc/ssh/ssh_host_ed25519_keywithsshd.servicerestart hook.
Usage Example
{ ... }: {
core.sops.hostSecretsFile = ./secrets.yaml;
}
Operational Notes
- Default age key path includes
${config.host.persistence.root}/etc/ssh/ssh_host_ed25519_key. - Module filters
config.services.openssh.hostKeysto ed25519 keys before adding them tosops.age.sshKeyPaths. core.opensshtypically consumessops.secrets.SSH_PRIVATE_KEYdeclared here.
Stylix
Applies shared system theme defaults with Stylix.
- Entry point: stylix.nix
Overview
This module imports Stylix and enables dark Tokyo Night theming on non-headless hosts by default.
Options
core.stylix.enable
| Type | boolean |
| Default | !config.host.device.isHeadless |
| Example | true |
Whether to enable Stylix configuration.
Behaviour
When enabled, module:
- imports
inputs.stylix.nixosModules.stylixunless function argumentimportExternals = false, - sets
stylix.enable = true, - sets
stylix.polarity = "dark", and - uses Tokyo Night dark Base16 scheme from
tinted-schemesinput.
Usage Example
{ ... }: {
core.stylix.enable = true;
}
Operational Notes
- Module is intended for graphical hosts.
- Theme source is
${inputs.stylix.inputs.tinted-schemes}/base16/tokyo-night-dark.yaml.
Virtualisation
Configures libvirt, VFIO passthrough, bridge networking, and guest isolation helpers.
- Entry point: virtualisation.nix
Overview
This module enables libvirt with QEMU, VFIO GPU passthrough, Looking Glass shared memory, bridge networking, custom OVMF firmware metadata, and libvirt hook helpers for selected guests.
It also generates helper scripts that change AllowedCPUs on host slices while selected guests run, detach and reattach passthrough GPUs for -single guests, and block host sleep while libvirt domains are active.
Options
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.
Behaviour
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
IOMMUType = "amd",disableEFIfb = true, and configured GPU devices, - configures Looking Glass shared memory file
looking-glassowned byracci:qemu-libvirtd, - adds
virt-manager,virtiofsd,virtio-win, andwin-spiceto system packages, - sets
LIBVIRT_DEFAULT_URI = qemu:///system, - creates bridge networking with DHCP on
bridgeInterfaceandexternalInterfaceenslaved into bridge, - adds
kvmfrkernel module package and modprobe configstatic_size_mb=128, and - installs udev rule for
/dev/kvmfraccess.
Module also 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 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.
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
Usage Example
{ ... }: {
core.virtualisation = {
enable = true;
vmUsers = [ "racci" ];
isolatedGuests = [ "win11-gaming" ];
bridgeInterface = "br0";
externalInterface = "enp6s0";
cpuCores = 24;
gpu = {
video = "10de:1b06";
audio = "10de:1bef";
};
};
}
Operational Notes
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
Adds Windows Subsystem for Linux specific integration and fixes.
- Entry point: wsl.nix
Overview
This module configures WSL-focused defaults such as default user, Windows interop, graphics library paths, nix-ld, and Start Menu launcher syncing.
Options
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.
Behaviour
When enabled, module:
- sets
users.allowNoPasswordLogin = true, - installs
pkgs.wslu, - enables
programs.nix-ldwith C toolchain library for Remote WSL compatibility, - sets session variables for WSL graphics and library paths,
- enables
hardware.graphicsand addsconfig.hardware.graphics.package,config.hardware.graphics.package32, andpkgs.libvdpau-va-gl, and - when NVIDIA graphics are present, appends CUDA and NVIDIA library paths.
If wsl module exists in option tree, module also:
- enables
wsl.enable, - sets
wsl.defaultUser = core.wsl.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 Windows Start Menu.
Usage Example
{ ... }: {
core.wsl = {
enable = true;
user = "racci";
};
}
Operational Notes
core.wsl.useris required when WSL integration is enabled.- Extra binaries
dirname,readlink, andunameare exposed for VS Code Remote WSL compatibility. - Some behavior is conditional on separate WSL module being available in
options.
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 a coordinator node (ioPrimaryHost) and providing helper functions for inter-server communication and attribute collection.
Entry Point
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/dird8mmzmya8m99dcd98bisvxy93rngj-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.
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 coordinator host for the cluster. This host runs primary database instances, the reverse proxy, and storage master nodes. This option is typically set on the coordinator host and used by other servers in the cluster for synchronization.
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
- 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
ioPrimaryHostis a critical component of the cluster, as many services (like Dashy or MinIO) rely on it as the central point of coordination.
Server Dashboard Module
The Server Dashboard module provides an integrated dashboard for monitoring and accessing services within the server cluster.
Purpose
The dashboard module integrates with Dashy and collects dashboard sections from all servers in the cluster to display on the ioPrimaryHost.
Entry Point
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.
Example Usage
Configure the dashboard section for a server:
# hosts/server/nixserv/default.nix
{
server.dashboard = {
name = "Services";
icon = "fas fa-server";
items = {
"Grafana" = {
title = "Grafana Dashboard";
icon = "fas fa-chart-line";
url = "https://grafana.example.com";
};
};
};
}
Operational Notes
- This module uses
getAllAttrsFuncto gatherserver.dashboardconfigurations from all servers in the cluster. - The aggregated configuration is only applied to the
ioPrimaryHost, which runs the primary Dashy instance. - This allows each server to define its own dashboard items, which are then automatically collected and displayed on a single unified dashboard.
Server Network Module
The Server Network module provides a declarative way to manage network configurations and firewall rules across the server cluster.
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
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.
Example Usage
Configure a subnet and open ports on a server:
# hosts/server/nixio/default.nix
{
server.network = {
subnets = [
{
dns = "192.168.1.1";
domain = "lan.example.com";
ipv4.cidr = "192.168.1.0/24";
}
];
openPortsForSubnet = {
tcp = [ 80 443 ];
};
};
}
Operational Notes
- This module uses
getIOPrimaryHostAttrto fetch theserver.network.subnetsconfiguration from theioPrimaryHost. - This ensures that all servers in the cluster are aware of the network structure defined on the coordinator host.
- 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.
Server Distributed Builds Module
The Server Distributed Builds module provides a declarative way to manage distributed builds across the server cluster.
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.
Entry Point
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.
Example Usage
Configure a build server and a host to use it for distributed builds:
# hosts/server/nixserv/default.nix (build server)
{
server.distributedBuilder = {
builders = [ "nixserv" ];
};
}
# hosts/server/nixdev/default.nix (host using build server)
{
server.distributedBuilder = {
builders = [ "nixserv" ];
};
}
Operational Notes
- 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.distributedBuildsand sets up the build machines usingnix.buildMachines. - 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. - For more information on distributed builds in Nix, see the NixOS Manual.
Database Submodule
The database submodule provides a managed interface for PostgreSQL and Redis across the server infrastructure. It centralizes database configuration on the primary database host (config.server.ioPrimaryHost) while allowing client services to declaratively request databases.
The module is implemented across several files in modules/nixos/server/database/:
- default.nix: Core options and connection management.
- postgres.nix: PostgreSQL-specific provisioning and secrets.
- redis.nix: Redis-specific ID mappings and security.
- guardian.nix: Lifecycle synchronization and the IO Guardian.
Purpose
This submodule 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.
- Automated password handling via SOPS secrets.
Entry Points
server.database.postgres: Manage PostgreSQL databases and users (inpostgres.nix).server.database.redis: Manage Redis database instances (inredis.nix).server.database.host: Centralized host address for database connections (indefault.nix).server.database.dependentServices: Lifecycle coordination for dependent services (inguardian.nix).
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.
Key Options and Behaviors
Connection Management
The server.database.host option determines how services connect to databases. On the primary database host (ioPrimaryHost), it defaults to localhost. On all other hosts, it defaults to the value of config.server.ioPrimaryHost.
PostgreSQL Management
When a service defines a database in server.database.postgres:
- Automatic Provisioning: The IO Host 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 IO Host 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 IO Host 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 IO Host to update the mapping file when adding new Redis clients.
Per-Module Examples
Connection Configuration (default.nix)
You can override the default database host (e.g., if using a custom tunnel or local proxy):
{
server.database.host = "10.0.0.50";
}
PostgreSQL Example (postgres.nix)
Requesting a PostgreSQL database for a service:
{
server.database.postgres."my-app" = {
# database and user will be 'my-app'
# Password expected at sops secret: POSTGRES/MY_APP_PASSWORD
};
}
Redis Example (redis.nix)
Requesting a Redis database:
{
server.database.redis.myapp = {
# prefix will be 'myapp'
# database_id is assigned from redis-mappings.json
};
}
Guardian Dependency Example (guardian.nix)
Manually adding services to the database lifecycle coordination:
{
server.database.dependentServices = [
"custom-backend.service"
"worker-node" # .service suffix is added automatically
];
}
Operational Notes
IO Guardian Coordination
Lifecycle management is handled by the IO Guardian.
- 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. - On IO Primary Host: The
io-database-coordinatorservice manages thedrainandundrainsignals sent to clients during system startup and shutdown.
IO Primary Host Behavior
The host designated as the IO Primary Host (config.server.ioPrimaryHost) is responsible for running the actual database engines. It aggregates all database requirements from every host in the flake and applies them locally.
Storage
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.
Purpose
This area provides:
- declarative MinIO-backed and SeaweedFS-backed mounts through
server.storage.swfsMount - a SeaweedFS evaluation deployment on the IO primary host
Key Options and Behaviors
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 generates a timer-driven probe that can lazily unmount stale FUSE mounts, restart the mount service, and optionally restart 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 that runs alongside MinIO on the IO primary host and exposes its endpoint set through the existing Caddy proxy integration.
See SeaweedFS Evaluation for details on scope, host gating, proxy behavior, and security material.
Example
The following example mounts a MinIO-backed media bucket and sets specific ownership.
{
server.storage.swfsMount.media = {
backend = "minio";
uid = 1000;
gid = 1000;
umask = 007;
};
}
Operational Notes
- 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. - MinIO Endpoint: The MinIO backend defaults to
https://minio.racci.devunless a mount overrides the endpoint explicitly. - 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
SeaweedFS Evaluation
SeaweedFS is currently 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.
Purpose
The evaluation deployment provides an all-in-one SeaweedFS stack on the IO 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 Points
modules/nixos/server/storage/seaweedfs.nixmodules/nixos/server/storage/default.nix
Deployment Scope
- Evaluation only: this deployment does not replace MinIO and does not perform any migration.
- IO primary only: the module is gated by
config.server.ioPrimaryHost == 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
The SeaweedFS evaluation endpoints are exposed through the existing server.proxy.virtualHosts integration instead of host-local Caddy configuration.
Current proxy surface includes:
seaweedfs.<domain>for the master endpointfiler.seaweedfs.<domain>for the filer endpoints3.seaweedfs.<domain>for the S3-compatible endpointvolume.seaweedfs.<domain>for the volume endpointadmin.seaweedfs.<domain>for the admin endpoint
Client-facing TLS terminates at Caddy. For gRPC-backed component endpoints, the proxy is additionally configured with the backend transport settings required for SeaweedFS communication.
Security Material
The 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 IO primary host and include both JWT material and TLS certificates/keys for the SeaweedFS component set.
Operational Notes
- The SeaweedFS module uses the upstream
services.seaweedfsoption surface rather than introducing a repository-localserver.storage.seaweedfs.*option tree. - The evaluation deployment is still separate from
server.storage.swfsMount. The new 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
Proxy Submodule
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.
Purpose
This module abstracts the complexity of reverse proxying by allowing services to define their proxy requirements within their own module configuration. It automatically coordinates between backend hosts and the primary IO host to ensure ports are open and traffic is correctly routed.
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.
Per-Module Examples
default.nix - Logic and Helpers
This file contains the internal logic for resolving OAuth contexts and mapping local addresses to backend hostnames.
# Example: How contextToEnvPrefix transforms names for environment variables
contextToEnvPrefix "my-service" # Returns "MY_SERVICE"
options.nix - Option Definitions
Defines the structure of virtual hosts and shared contexts.
server.proxy.kanidmContexts.admin-apps = {
authDomain = "auth.internal.example.com";
allowGroups = [ "admins@auth.example.com" ];
};
server.proxy.virtualHosts.grafana = {
public = true;
kanidm = {
context = "admin-apps";
allowGroups = [ "grafana-users@auth.example.com" ];
bypassPaths = [ "/health" ];
};
extraConfig = "reverse_proxy localhost:3000";
};
config.nix - Caddy Integration
Handles the generation of services.caddy.virtualHosts and ACME certificate requests.
Note: L4 (TCP/UDP) forwarding is handled by the
l4extension, not by config.nix. See Layer 4 Forwarding.
# Generated Caddy block for a vhost with Kanidm
grafana.example.com {
import default
import public
@bypass_auth_grafana path /health
handle @bypass_auth_grafana {
reverse_proxy 10.0.0.5:3000
}
route /auth/* {
authenticate with grafana_portal
}
handle {
authorize with grafana_policy
reverse_proxy 10.0.0.5:3000
}
}
kanidm.nix - Authentication Security
Generates the Caddy security block, including identity providers, portals, and authorization policies.
security {
oauth identity provider admin-apps {
realm admin-apps
client_id "admin-apps"
client_secret {env.OAUTH_ADMIN_APPS_CLIENT_SECRET}
metadata_url https://auth.internal.example.com/oauth2/openid/admin-apps/.well-known/openid-configuration
}
# ... portals and policies
}
extensions.nix - System Integration
Connects the proxy to the dashboard, Cloudflared tunnels, and automates Kanidm client provisioning.
# Automatic Kanidm provisioning based on proxy config
services.kanidm.provision.systems.oauth2.admin-apps = {
displayName = "Admin Apps";
originUrl = [ "https://grafana.example.com/auth/oauth2/admin-apps/authorization-code-callback" ];
# ...
};
Operational Notes
Caddy Integration
The module assumes the existence of a default Caddy snippet for common headers and security settings. When public is enabled, it also expects a public snippet.
Dashboard Integration
Services defined in server.proxy.virtualHosts are automatically added to the server dashboard with default titles and icons derived from the host name.
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.
Layer 4 Forwarding
L4 forwarding uses the caddy.layer4 plugin for non-HTTP traffic like database connections or SSH. Managed by the l4 extension (modules/nixos/server/proxy/extensions/l4.nix), which auto-enables when any vhost has l4 != null.
References
Extension Architecture
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_namehostConfig: Full host-level NixOS config
The vhost attrset contains _resolvedExtraConfig — the user’s extraConfig field with localhost/127.0.0.1 already replaced for non-IO hosts.
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:
server.proxy.extensions.myext.enable = mkDefault (
# check if any vhost uses my extension's features
);
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:(importModule ./extensions/<name>.nix { inherit proxyLib; }) - 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
Example skeleton:
{ proxyLib, ... }:
{ config, lib, ... }:
let
inherit (lib) mkOption types mkDefault;
in
{
options.server.proxy.virtualHosts = mkOption {
type = attrsOf (submodule ({ name, ... }: {
options.mycustom = mkOption {
type = bool;
default = false;
};
}));
};
config = {
server.proxy.extensions.mycustom = {
priority = 75;
config = name: vh: hostCfg:
if !vh.mycustom then "" else "header X-Custom on";
globalConfig = hostCfg: "";
vhostModule = null;
};
};
}
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.
server.proxy.virtualHosts.myservice = {
requireApiKey = {
enable = true;
bypassPaths = [ "/health" "/metrics" ]; # paths that skip auth
};
extraConfig = "reverse_proxy localhost:8080";
};
Generated Caddy config per vhost:
@myservice_apikey_key {
header Req-API-Key {env.API_KEY_MYSERVICE}
}
route /auth/apikey/* {
authorize with myservice_apikey_authorizer
}
handle {
authorize with myservice_apikey_authorizer
reverse_proxy localhost:8080
}
Global caddy-security config:
order authorize before reverse_proxy
authorize with myservice_apikey_authorizer {
with @myservice_apikey_key
}
Secrets auto-generated via sops at PROXY_AUTH/<VHOST_NAME>_API_KEY, injected via systemd LoadCredential. 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 |
Server SSH Module
The Server SSH module provides a rich interactive environment for root users upon login. It automatically transitions interactive root sessions into a dedicated development shell, ensuring consistent tooling and a powerful shell experience across server environments.
Purpose
The SSH submodule enhances administrative access by providing a session-only environment tailored for server management. It removes the need for manual setup of common tools and aliases by automatically entering a pre-configured nix-shell when a root user logs in interactively over SSH.
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/dird8mmzmya8m99dcd98bisvxy93rngj-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
Auto-entry Logic (ssh-shell/default.nix)
The module creates an indirect GC root for the SSH shell at login time by instantiating shell expression to derivation, then realizing it with nix-store --add-root --indirect --realise. This keeps realized shell alive across upgrades without referencing config.system.build.toplevel during system evaluation.
The module modifies /etc/bashrc to 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.
The module also configures OpenSSH to accept the NIX_SKIP_SHELL environment 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-shell environment 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 shellHook in shell.nix starts an interactive Fish session and immediately exits the nix-shell wrapper once the Fish session concludes.
Per-Module Examples
Enabling the SSH Shell
Enable the auto-shell behavior in your host configuration:
{
server.sshShell.enable = true;
}
Customizing the Shell File
Override the shell definition file if you require a different set of tools:
{
server.sshShell.shellFile = ./my-custom-shell.nix;
}
Operational Notes
Opt-Out Behavior
If you need to log in as root without entering the specialized shell, set the NIX_SKIP_SHELL environment variable on your local machine before connecting:
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
Flake Allocations
The flake allocations module defines cross-host configuration options at the flake level. Rather than configuring each NixOS system independently, allocations let you declare cluster-wide concerns — like which machines have GPUs, which server coordinates I/O, and which servers act as distributed builders — in a single place.
How It Works
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/) — Propagates 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 mkSystem builds a NixOS configuration, 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.
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.
allocations.accelerators = {
nixmi = [ "cuda" ];
nixai = [ ];
};
Hosts not listed default to no accelerators. The builder (lib/builders/default.nix) 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 getHostsByType, which scans hosts/ directory structure.
# Automatically resolves to something like:
allocations.hostTypes = {
server = [ "nixio" "nixserv" "nixmon" ];
desktop = [ "nixmi" ];
};
allocations.server.ioPrimaryCoordinator
Designates a server as the primary I/O coordinator for the cluster. This is the host that runs primary database instances, the reverse proxy, and storage master nodes.
The type is constrained to an enum of server hostnames (automatically derived from hostTypes.server).
allocations.server.ioPrimaryCoordinator = "nixio";
This value flows through apply/system.nix into server.ioPrimaryHost on each server configuration.
allocations.server.distributedBuilders
List of servers that act as remote builders for distributed builds.
allocations.server.distributedBuilders = [ "nixserv" ];
Flows into server.distributedBuilder.builders on each server configuration.
Apply Modules
The apply modules (modules/flake/apply/) bridge flake-level allocations to per-system NixOS options.
apply/system.nix
Imported by mkSystem during system construction. Receives allocations and deviceType via specialArgs. For server-type hosts, it maps:
allocations.server.ioPrimaryCoordinator→server.ioPrimaryHostallocations.server.distributedBuilders→server.distributedBuilder.builders
Uses optionalAttrs to only apply server-specific options when deviceType == "server", preventing errors on non-server systems.
apply/home-manager.nix
Imported by the Home-Manager builder. Currently a no-op (mkMerge []) — exists as a placeholder for future home-manager-level allocations.
Source Files
| File | Role |
|---|---|
modules/flake/allocations.nix | Option definitions |
modules/flake/apply/system.nix | NixOS system apply |
modules/flake/apply/home-manager.nix | Home-Manager apply (placeholder) |
flake/nixos/flake-module.nix | Actual configuration values |
lib/builders/default.nix | Builder that consumes allocations |
AI Modules
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.
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.
Current Modules
- Mnemosyne — Sync server, optional MCP server, and sync client orchestration for the Mnemosyne SQLite-based memory provider
Usage
{
services.mnemosyne = {
enable = true;
server.sync.enable = true;
};
}
The ai/ module tree is loaded on all device types by mkSystem.
Mnemosyne
SQLite-backed memory provider with sync and optional MCP server. Part of the ai/ module tree.
Architecture
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 --sse)"]
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
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.
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";
};
};
}
Notes
- Sync server uses
mnemosyne sync servewith stdlib HTTP — no extra Python dependencies. - MCP server adds
mcpandanyiodependencies (viapkgs.mnemosyne-mcp). - Sync protocol is plain HTTP with delta-based bidirectional sync.
- Sync interval default is 10 minutes.
DIY & Making
This section documents the Home-Manager modules under purpose.diy, which provide tooling and configuration for hardware tinkering, 3D printing, and related maker activities.
Printing
The printing module installs 3D-printing software and wires up persistent storage so that settings survive reboots on impermanence-based systems.
- Entry point: printing.nix
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 that watches the OrcaSlicer profile directory and automatically creates a git commit every time a profile file is added, changed, or removed. This gives you a full revision history of your 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 (
orca-slicer-git-sync.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 2 seconds to debounce rapid bursts of writes (e.g. when OrcaSlicer rewrites multiple files at once).
- All pending changes are then committed one file at a time, each with an individually crafted commit message.
- If the watched directory does not yet exist (e.g. OrcaSlicer has never been run), the service polls every 10 seconds 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
- The git repository is initialised with
git initand an initial commit (chore: initial commit) the first time the service starts if no.gitdirectory exists. - The service is set to restart on failure (
Restart=on-failure,RestartSec=10) 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.
Home-Manager: AI Editors & Assistants
This page documents the Home-Manager module at:
modules/home-manager/purpose/development/editors/ai/default.nix
It configures editor/agent tooling for AI-assisted development, centered around OpenCode and shared skill directories.
What this module sets up
When enabled, the module:
- Ensures
~/Projects/AIFSexists at activation time. - Adds useful global git ignores:
.workspace.sisyphus
- Configures Zed to expose an
OpenCodeagent server (opencode acp). - Enables and configures
programs.opencodewith:- plugins
- Nix formatter integration
- LSP integrations:
- Nix:
nixd,nil - Config formats:
marksman(Markdown),yaml-language-server,vscode-json-language-server,taplo(TOML),vscode-css-language-server,vscode-html-language-server - Languages:
rust-analyzer,gopls,pyright,typescript-language-server,bash-language-server,lua-language-server,nushell,powershell-editor-services,dockerfile-language-server
- Nix:
- command permissions policy
- local MCP server (
mcp-nixosviauvx)
- Writes:
~/.config/opencode/oh-my-opencode.json~/.config/opencode/opencode-notifier.json
- Registers AI skills under
~/.agents/skills/<name>viahome.file. - Persists OpenCode state directories:
.local/share/opencode.local/state/opencode
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"
];
};
}
Notes
- 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.
Home-Manager: Hyprland
This page documents the custom Hyprland Home-Manager helper modules at:
modules/home-manager/core/hyprland/
These modules extend wayland.windowManager.hyprland with a typed Nix API for window rules, permissions, slide-in popups, input defaults, and Lua config generation targeting the HM-native Lua configuration format.
The repo default (set in home/shared/desktop/hyprland/default.nix) uses configType = "lua".
In Lua mode, direct settings.* attr names must be Lua-safe identifiers. Use underscore-style names like exec_once, window_rule, and workspace_rule instead of dashed hyprlang names like exec-once.
The module structure is:
default.nix # Top-level importer (imports all submodules)
├── permission.nix # custom-settings.permission
├── slideIn.nix # custom-settings.slideIn
├── windowRule.nix # custom-settings.windowrule
├── input.nix # settings.config defaults (cursor, binds, input, misc)
├── lua.nix # custom-settings.lua (Lua config generation)
│ └── lua/binds.lua # Default Lua bind template with @placeholder@ substitution
└── types.nix # Shared type definitions
Module Files
input.nix
Sets sensible default values under settings.config for cursor behavior, input device settings, keyboard binds, and misc Hyprland options. This module activates automatically when the Hyprland HM module is enabled — no custom-settings option is involved. Override any value via settings.config.input.* etc.
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
windowRule.nix
Defines custom-settings.windowrule: an attribute set of named window rules. Each rule has:
name— rule name (defaults to the attribute key)matcher— list of match conditions (window class, title, workspace, etc.)rule— rule properties (float, fullscreen, size, move, opacity, center, monitor, workspace, pin, group, border, animation, idle inhibit, and many more)
custom-settings.windowrule = {
"kitty-floating" = {
matcher = [ { class = "kitty"; } ];
rule = {
float = true;
center = true;
size = {
width = "40%";
height = "60%";
};
};
};
"firefox-picture-in-picture" = {
matcher = [
{
title = "Picture-in-Picture";
class = "firefox";
}
];
rule = {
pin = true;
opacity.activeopacity = 0.9;
};
};
};
Complex matchers and compound rule types (workspace selectors, monitor selectors, fullscreen state, opacity, center with reserved area, max/min size, move) are fully typed in types.nix.
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
Defines custom-settings.slideIn — a list of edge-sliding popup windows. Each entry configures a keybind, executable, window class, position (left/right/top/bottom/edge/side), and optional window rules. Uses hdrop for dropdown-style window management.
lua.nix
Defines custom-settings.lua — the Lua config generation subsystem. Options:
-
enable(boolean, defaultfalse) — Enable pure Lua configuration files with Nix substitution support. -
variables(attrs ofnullOr str, default{}) — Key-value pairs for@placeholder@substitution in Lua source files. Each keyfooreplaces@foo@in all sourced Lua modules with the given value. Some variables are pre-populated automatically (seeapplicationBindsbelow). Common injected values include paths toplayerctl,wpctl,zenity,hyprshutdown, anduwsm-app. -
luaModules(list of paths, default[ ./lua/binds.lua ]) — Lua source files to copy into the Hyprland config directory andrequirefrominit.lua. Each file undergoes@placeholder@substitution using thevariablesattrset. The default list includeslua/binds.lua. -
applicationBinds(attrs ofstr, default{}) — Application keybinds passed into Lua generation. Each attr key is bind string (for example"SUPER+Return") and each attr value is command string. Rendered into@applicationBinds@as Lua table entries consumed bybinds.lua:custom-settings.lua.applicationBinds = { "SUPER + Return" = "${pkgs.kitty}/bin/kitty"; "SUPER + E" = "${pkgs.nautilus}/bin/nautilus"; };Generated Lua iterates over those table entries and creates
hl.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.
lua/binds.lua
The default Lua bind template at modules/home-manager/core/hyprland/lua/binds.lua. Uses @placeholder@ substitution for dynamic injection:
| 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 null) |
@DEFAULT_AUDIO_SOURCE@ | custom-settings.lua.variables | Audio source name (default null) |
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 (added in flake/home-manager/flake.nix).
The module:
- Enables
programs.noctaliaandsystemd, pinspackagefrominputs.noctalia.packages, and applies a Hyprland layer blur rule for Noctalia windows. - Mirrors a full exported Noctalia v5 config as a typed Nix attrset (
noctaliaSettings), covering bar layouts with monitor overrides, shell panel/screen corners/screenshot/session actions, theme (builtin “Noctalia” with community palette “Tokyo Night Moon”), wallpaper (directory, default/last/monitor paths, automation), 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 tocrop(not a profile option). - Location driven by
core.profile.location.secret(SOPS secret name). Two modes:- Normal (
secret == null): setsprograms.noctalia.settingswith build-time validation. No location block. - Secret (
secret != null): base TOML generated at build time; activation copies it to~/.config/noctalia/config.tomland appends[location] addressfrom decryptedsops.secrets.<name>.path. Clear text never in repo or Nix store.
- Normal (
The user-side Hyprland config (home/racci/features/desktop/hyprland/) pairs with this module via Noctalia IPC keybinds:
| Binding | Action |
|---|---|
SUPER+SPACE → SUPER+SHIFT+F | fullscreen (displaced by Noctalia launcher bind) |
SUPER+S → SUPER+grave | special workspace toggle (displaced by Noctalia control center bind) |
SUPER+SHIFT+S → SUPER+SHIFT+grave | move window to special workspace (displaced by Noctalia settings bind) |
SUPER+comma | Noctalia settings |
| Audio/brightness keys | noctalia msg ... dispatchers |
Workspace rules in the user config now set persistent = true for defined workspaces, ensuring they are always available regardless of Noctalia lifecycle.
Look settings are tuned toward Noctalia documentation recommendations: gaps_in = 5, gaps_out = 10, rounding_power = 2, shadow range/render/color tuned, and blur size/passes/vibrancy adjusted.
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 = {
windowrule."kitty-floating" = {
matcher = [ { class = "kitty"; } ];
rule = {
float = true;
size = { width = "40%"; height = "60%"; };
center = true;
};
};
permission = {
screenCopy = [ pkgs.firefox ];
};
lua = {
enable = true;
applicationBinds = {
"SUPER + Return" = "${pkgs.kitty}/bin/kitty";
"SUPER + E" = "${pkgs.nautilus}/bin/nautilus";
};
};
};
};
}
Notes
- 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.- Unknown dispatchers in Lua raises 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.
Home-Manager: Profile
This page documents the core profile module at:
modules/home-manager/core/profile.nix
It is imported automatically via modules/home-manager/core/default.nix.
The module declares a set of shared user-profile options under core.profile. These values are consumed by other modules — desktop environments, shell companions, and display managers — so they reference a single source of truth rather than hardcoded literals.
Options
core.profile.avatar.path
- Type:
string - Default:
~/.face
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
- Type:
string - Default:
~/Pictures/Wallpapers
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
- Type:
nullOr string - Default:
null
Name of a SOPS secret whose decrypted value becomes the Noctalia location.address. When set to null 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.
Usage Example
{ ... }: {
core.profile = {
avatar.path = "/home/user/.face";
wallpaper.directory = "/home/user/Pictures/Backgrounds";
location.secret = "noctalia-location";
};
}
Notes
- The module only declares options; it does nothing on its own. Consumers use
config.core.profile.*to read the values. - The GNOME azwallpaper extension consumes
core.profile.wallpaper.directoryviadconf-extensions.nix(home/shared/desktop/gnome/).
Home-Manager: list-ephemeral
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.
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.
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.
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.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.image-compressor: Python tool that hashes images, detects image types from file headers, caches WebP conversions, keeps parallel workers, and prints aligned Rich tables.monocoque: Sim-racing dashboard and telemetry tool.orca-slicer-zink: Orca Slicer configured to use the Zink Vulkan driver to resolve nvidia rendering issues.python: Packages for home assistant python components.take-control-viewer: Remote support viewer for N-able Take Control via Wine.
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.
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.
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).
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.hermes-agent: Local overlay that builds Hermes Agent from upstream source plus two patches:overlays/patches/hermes-agent-pr-48637-lazy-deps.patch: Changestools/lazy_deps.pyto raiseFeatureUnavailableon managed/read-only installs (NixOS) instead of attemptingensurepipand failing repeatedly.overlays/patches/hermes-agent-pr-61443-node-headers-hash.patch: Fixes the hardcoded electronnode-headershash in upstreamnix/desktop.nix, allowing thehermesDesktoppassthru to build with the local electron version.
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.home/racci/hm-config.nix: Usespkgs.hermes-desktop(patched via overlay) instead of the unpatched upstreaminputs.hermes-agent.packages.<system>.desktop.
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.
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.
Key Options/Knobs
Host-specific configurations live in hosts/{device-type}/{hostname}/default.nix.
Shared NixOS behavior that used to live under hosts/shared/ is being migrated into reusable modules under modules/nixos/core/. Hosts now typically enable these with top-level core.* options instead of importing host-shared files directly.
Examples:
core.containers.enable = true;core.gaming.enable = true;core.virtualisation.enable = true;core.networking.tailscale.enable = true;
Global options still shared across all hosts remain in hosts/shared/global/.
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/.
References
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";
Lib Overview
Purpose
The lib directory contains custom Nix functions and builders used throughout the repository to simplify configuration and reduce duplication.
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.
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/.