19 KiB
Nix Dotfiles Repository Guide
This repository contains NixOS configurations for personal infrastructure. The setup is organized around a flake-based structure with per-system configurations and user-specific settings.
Project Structure
flake.nix- Main flake definition with inputs and outputssystems/- Per-system configurations (e.g.,artemision,palatine-hill)users/- Per-user configurations using home-managermodules/- Reusable Nix modules for common serviceslib/- Custom Nix library functionshydra/- Hydra CI/CD configurationsecrets/- SOPS encrypted secrets
Key Concepts
System Configuration
Each system has its own directory under systems/ containing:
configuration.nix- Main system configuration- Component modules (audio.nix, desktop.nix, etc.)
- Hardware-specific configurations
User Configuration
User configurations are in users/<username>/:
home.nix- Home-manager configuration usinghome.packagesand importssecrets.yaml- SOPS-encrypted secrets using age encryptionnon-server.nix- Desktop-specific configurations
Nix Patterns
- Module-based approach: Uses Nix modules for organizing configuration
- Home-manager integration: User environment managed via home-manager
- SOPS secrets: Secrets managed with SOPS and age encryption
- Flake-based: Uses flakes for reproducible builds and development environments
- Multi-system support: Supports multiple machines with different configurations
- Dynamic configuration generation: Modules in the
modules/directory are automatically imported into all systems (can be overridden per system). New systems are automatically discovered bygenSystems()
Modern Nix Features
This repository uses modern Nix features including:
- Flakes: Enabled via
flakeexperimental feature - Nix Command: Enabled via
nix-commandexperimental feature - Blake3 Hashes: Enabled via
blake3-hashesexperimental feature - Git Hashing: Enabled via
git-hashingexperimental feature - Verified Fetches: Enabled via
verified-fetchesexperimental feature
Key Commands
nh os switch- Apply system configuration (using nix-community/nh)nh home switch- Apply user configuration (using nix-community/nh)nh os build- Build a specific system (using nix-community/nh)nix build .#<system>- Build a specific systemnix run .#<system>- Run a specific systemnix flake update- Update flake inputs
Development Workflow
- Make changes to system or user configuration
- Test with
nh os switchornh home switch - For CI/CD, Hydra automatically builds and tests changes
- Secrets are managed with SOPS and age keys
Important Files
flake.nix- Main entry point for the flakesystems/artemision/configuration.nix- Example system configurationusers/alice/home.nix- Example user configurationmodules/base.nix- Base module with common settingshydra/jobsets.nix- Hydra CI configuration
External Dependencies
- NixOS unstable channel
- Nixpkgs unstable channel
- SOPS for secrets management
- age for encryption
- home-manager for user environments
- nh (nix-community/nh) for simplified Nix operations
Nix MCP Server
- Use the nix MCP server for looking up package names and options
- Specify
unstablechannel if the channel is specifiable (e.g., forpkgs.<package-name>)
Dynamic Configuration System (lib/systems.nix)
This repository automatically generates NixOS system configurations based on the folder structure. Understanding how constructSystem and genSystems work is essential when adding new systems or global modules.
How Configuration Generation Works
The process happens in three stages:
Stage 1: Discovery (flake.nix → genSystems)
flake.nixcallsgenSystems inputs outputs src (src + "/systems")genSystemsscans thesystems/directory and lists all subdirectories- Each subdirectory name becomes a system hostname (e.g.,
artemision,palatine-hill)
Stage 2: Parameter Loading (genSystems reads default.nix)
- For each discovered system,
genSystemsimportssystems/<hostname>/default.nix - This file exports parameters for
constructSystemlike: users = [ "alice" ]— which users to createhome = true— enable home-managersops = true— enable secret decryptionserver = true/false— machine rolemodules = [ ... ]— additional system-specific modules
Stage 3: Assembly (constructSystem assembles the full config)
- Loads essential system files:
hardware.nix,configuration.nix - Auto-imports all
.nixfiles frommodules/directory vialib.adev.fileList - Conditionally loads home-manager, SOPS, and user configs based on parameters
- Merges everything into a complete NixOS system configuration
Key Functions in lib/systems.nix
| Function | Purpose | Called By |
|---|---|---|
genSystems |
Scans systems/ directory and creates configs for each subdirectory |
flake.nix |
constructSystem |
Assembles a single NixOS system with all modules and configs | genSystems |
genHome |
Imports home-manager configs for specified users | constructSystem |
genSops |
Imports SOPS-encrypted secrets for users | constructSystem |
genUsers |
Imports user account configs from users/<username>/ |
constructSystem |
genHostName |
Creates hostname attribute set | constructSystem |
genWrapper |
Conditionally applies generator functions | constructSystem |
Special Arguments Passed to All Configs
These are available in configuration.nix, hardware.nix, and all modules:
{ config, pkgs, lib, inputs, outputs, server, system, ... }:
config— NixOS configuration optionspkgs— Nix packages (nixpkgs)lib— Nix library functions (extended withlib.adev)inputs— Flake inputs (nixpkgs, home-manager, sops-nix, etc.)outputs— Flake outputs (for Hydra and other tools)server— Boolean: true for servers, false for desktopssystem— System architecture string (e.g.,"x86_64-linux")
Adding a New NixOS System
Step 1: Create the Directory Structure
mkdir -p systems/<new-hostname>
cd systems/<new-hostname>
Step 2: Create default.nix (System Parameters)
This file is automatically discovered and loaded by genSystems. It exports the parameters passed to constructSystem.
Minimal example:
{ inputs }:
{
# Required: List of users to create (must have entries in users/ directory)
users = [ "alice" ];
# Optional: Enable home-manager (default: true)
home = true;
# Optional: Enable SOPS secrets (default: true)
sops = true;
# Optional: Is this a server? Used to conditionally enable server features
server = false;
# Optional: System architecture (default: "x86_64-linux")
system = "x86_64-linux";
# Optional: System-specific modules (in addition to global modules/)
modules = [
# ./custom-service.nix
];
}
See systems/palatine-hill/default.nix for a complex example with all options.
Step 3: Create hardware.nix (Hardware Configuration)
Generate this via:
sudo nixos-generate-config --show-hardware-config > systems/<new-hostname>/hardware.nix
This file typically includes:
- Boot configuration and bootloader
- Filesystem mounts and ZFS/LVM settings
- Hardware support (CPU, GPU, network drivers)
- Device-specific kernel modules
Step 4: Create configuration.nix (System Configuration)
This is the main NixOS configuration file. Structure:
{ config, pkgs, lib, inputs, server, system, ... }:
{
# System hostname (usually matches directory name)
networking.hostName = "new-hostname";
# Desktop/desktop specific config
services.xserver.enable = !server;
# System packages
environment.systemPackages = with pkgs; [
# ...
];
# Services to enable
services.openssh.enable = server;
# System-specific settings override global defaults
boot.kernelParams = [ "nomodeset" ];
}
Step 5: Add Optional Secrets
If the system has sensitive data:
# Create and encrypt secrets file
sops systems/<new-hostname>/secrets.yaml
# This will be automatically loaded by genSops if sops = true
Step 6: Add Optional System-Specific Modules
For system-specific functionality that shouldn't be global, create separate .nix files in the system directory:
systems/<new-hostname>/
├── configuration.nix # Main config
├── default.nix
├── hardware.nix
├── secrets.yaml # (optional)
├── custom-service.nix # (optional) System-specific modules
├── networking.nix # (optional)
└── graphics.nix # (optional)
Reference these in default.nix:
{ inputs }:
{
users = [ "alice" ];
modules = [
./custom-service.nix
./networking.nix
./graphics.nix
];
}
Step 7: Deploy the New System
The system is now automatically registered! Deploy with:
# Build the new system
nix build .#<new-hostname>
# Or if you want to switch immediately
nh os switch
Adding a Global Module to modules/
Global modules are automatically imported into all systems. No registration needed.
Create a Module File
Add a new .nix file to the modules/ directory. Example: modules/my-service.nix
Module Structure
{ config, pkgs, lib, inputs, server, ... }:
{
# Define configuration options for this module
options.myService = {
enable = lib.mkEnableOption "my service";
port = lib.mkOption {
type = lib.types.int;
default = 3000;
description = "Port for the service";
};
};
# Actual configuration (conditional on enable option)
config = lib.mkIf config.myService.enable {
environment.systemPackages = [ pkgs.my-service ];
systemd.services.my-service = {
description = "My Service";
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = "${pkgs.my-service}/bin/my-service";
Restart = "always";
};
};
};
}
Using mkIf, mkDefault, and mkForce
-
mkIf— Conditionally apply config based on a booleanconfig = lib.mkIf config.myService.enable { ... }; -
mkDefault— Provide a default value that can be overriddenboot.kernelParams = lib.mkDefault [ "quiet" ]; -
mkForce— Force a value, preventing other modules from overridingservices.openssh.enable = lib.mkForce true; -
mkEnableOption— Define anenableoption with standard descriptionoptions.myService.enable = lib.mkEnableOption "my service";
Disable a Global Module for a Specific System
To disable a module for one system, override it in that system's configuration.nix:
{ config, lib, ... }:
{
# Disable the module entirely
myService.enable = false;
# Or override specific options
services.openssh.port = 2222;
}
Module Loading Order in constructSystem
Modules are applied in this order (later modules override earlier ones):
inputs.nixos-modules.nixosModule(SuperSandro2000's convenience functions)inputs.nix-index-database.nixosModules.nix-index- Hostname attribute from
genHostName hardware.nix(hardware-specific config)configuration.nix(main system config)- System-specific modules from
modulesparameter indefault.nix(e.g., custom-service.nix) - All
.nixfiles from globalmodules/directory (features enabled across all systems) - SOPS module (if
sops = true) - Home-manager module (if
home = true) - User configurations (if
users = [...]andhome = true)
Important: Global modules (step 7) are applied after system-specific configs, so they can't override those values unless using mkForce. System-specific modules take precedence over global ones.
Common Tasks
Enable a Feature Across All Systems
-
Create
modules/my-feature.nixwithoptions.myFeature.enable -
Set the feature enabled in
configuration.nixof systems that need it:myFeature.enable = true; -
Or enable globally and disable selectively:
# In modules/my-feature.nix config = lib.mkIf config.myFeature.enable { # ...enabled by default }; # In a system's configuration.nix myFeature.enable = false; # Disable just for this system
Add a New User to the System
-
Create user config:
users/<username>/default.nixandusers/<username>/home.nix -
Update system's
default.nix:users = [ "alice" "newuser" ]; -
Create secrets:
sops users/<username>/secrets.yaml -
Redeploy:
nh os switch
Override a Module's Default Behavior
In any system's configuration.nix:
{
# Disable a service that's enabled by default in a module
services.openssh.enable = false;
# Override module options
boot.kernelParams = [ "nomodeset" ];
# Add to existing lists
environment.systemPackages = [ pkgs.custom-tool ];
}
Check Which Modules Are Loaded
# List all module paths being loaded
nix eval .#nixosConfigurations.<hostname>.options --json | jq keys | head -20
# Evaluate a specific config value
nix eval .#nixosConfigurations.<hostname>.config.services.openssh.enable
Validate Configuration Before Deploying
# Check syntax and evaluate
nix flake check
# Build without switching
nix build .#<hostname>
# Preview what would change
nix build .#<hostname> && nix-diff /run/current-system ./result
Secrets Management
SOPS (Secrets Operations) manages sensitive data like passwords and API keys. This repository uses age encryption with SOPS to encrypt secrets per system and per user.
Directory Structure
Secrets are stored alongside their respective configs:
systems/<hostname>/secrets.yaml # System-wide secrets
users/<username>/secrets.yaml # User-specific secrets
Creating and Editing Secrets
Create or edit a secrets file:
# For a system
sops systems/<hostname>/secrets.yaml
# For a user
sops users/<username>/secrets.yaml
SOPS will open your $EDITOR with decrypted content. When you save and exit, it automatically re-encrypts the file.
Example secrets structure for a system:
# systems/palatine-hill/secrets.yaml
acme:
email: user@example.com
api_token: "secret-token-here"
postgresql:
password: "db-password"
Example secrets for a user:
# users/alice/secrets.yaml
# The user password is required
user-password: "hashed-password-here"
Accessing Secrets in Configuration
Secrets are made available via config.sops.secrets in modules and configurations:
# In a module or configuration.nix
{ config, lib, ... }:
{
# Reference a secret
services.postgresql.initialScript = ''
CREATE USER app WITH PASSWORD '${config.sops.secrets."postgresql/password".path}';
'';
# Or use the secret directly if it supports content
systemd.services.my-app.serviceConfig = {
EnvironmentFiles = [ config.sops.secrets."api-token".path ];
};
}
Merging Secrets Files
When multiple systems or users modify secrets, use the sops-mergetool to resolve conflicts:
# Set up mergetool
git config merge.sopsmergetool.command "sops-mergetool-wrapper $BASE $CURRENT $OTHER $MERGED"
# Then during a merge conflict
git merge branch-name
# Git will use sops-mergetool to intelligently merge encrypted files
The repository includes helper scripts: utils/sops-mergetool.sh and utils/sops-mergetool-new.sh
Adding a New Machine's Age Key
When adding a new system (systems/<new-hostname>/), you need to register its age encryption key:
-
Generate the key on the target machine (if using existing deployment) or during initial setup
-
Add the public key to
.sops.yaml:keys: - &artemision <age-key-for-artemision> - &palatine-hill <age-key-for-palatine-hill> - &new-hostname <age-key-for-new-hostname> creation_rules: - path_regex: 'systems/new-hostname/.*' key_groups: - age: *new-hostname -
Re-encrypt existing secrets with the new key:
sops updatekeys systems/new-hostname/secrets.yaml
Real-World Examples
Example 1: Adding a Feature to All Desktop Machines
Using artemision (desktop) as an example:
Create modules/gpu-optimization.nix:
{ config, lib, server, ... }:
{
options.gpu.enable = lib.mkEnableOption "GPU optimization";
config = lib.mkIf (config.gpu.enable && !server) {
# Desktop-only GPU settings
hardware.nvidia.open = true;
services.xserver.videoDrivers = [ "nvidia" ];
};
}
Enable in systems/artemision/configuration.nix:
{
gpu.enable = true;
}
Deploy:
nix build .#artemision
nh os switch
Example 2: Adding a Server Service to One System
Using palatine-hill (server) as an example:
Create systems/palatine-hill/postgresql-backup.nix:
{ config, pkgs, lib, ... }:
{
systemd.timers.postgres-backup = {
description = "PostgreSQL daily backup";
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = "03:00";
Persistent = true;
};
};
systemd.services.postgres-backup = {
description = "Backup PostgreSQL database";
script = ''
${pkgs.postgresql}/bin/pg_dumpall | gzip > /backups/postgres-$(date +%Y%m%d).sql.gz
'';
};
}
Reference in systems/palatine-hill/default.nix:
{ inputs }:
{
users = [ "alice" ];
server = true;
modules = [
./postgresql-backup.nix
];
}
Deploy:
nix build .#palatine-hill
Example 3: Disabling a Global Module for a Specific System
To disable modules/steam.nix on a server (palatine-hill) while it stays enabled on desktops:
In systems/palatine-hill/configuration.nix:
{
steam.enable = false; # Override the module option
}
The module in modules/steam.nix should use:
config = lib.mkIf config.steam.enable {
# steam configuration only if enabled
};
Debugging & Validation
Check Module Evaluation
# See which modules are loaded for a system
nix eval .#nixosConfigurations.artemision.config.environment.systemPackages --no-allocator
# Validate module option exists
nix eval .#nixosConfigurations.artemision.options.myService.enable
Debug SOPS Secrets
# View encrypted secrets (you must have the age key)
sops systems/palatine-hill/secrets.yaml
# Check if SOPS integration is working
nix eval .#nixosConfigurations.palatine-hill.config.sops.secrets --json
Test Configuration Without Deploying
# Evaluate the entire configuration
nix eval .#nixosConfigurations.artemision --no-allocator
# Build (but don't activate)
nix build .#artemision
# Check for errors in the derivation
nix path-info ./result