# 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 outputs - `systems/` - Per-system configurations (e.g., `artemision`, `palatine-hill`) - `users/` - Per-user configurations using home-manager - `modules/` - Reusable Nix modules for common services - `lib/` - Custom Nix library functions - `hydra/` - Hydra CI/CD configuration - `secrets/` - 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//`: - `home.nix` - Home-manager configuration using `home.packages` and imports - `secrets.yaml` - SOPS-encrypted secrets using age encryption - `non-server.nix` - Desktop-specific configurations ### Nix Patterns 1. **Module-based approach**: Uses Nix modules for organizing configuration 1. **Home-manager integration**: User environment managed via home-manager 1. **SOPS secrets**: Secrets managed with SOPS and age encryption 1. **Flake-based**: Uses flakes for reproducible builds and development environments 1. **Multi-system support**: Supports multiple machines with different configurations 1. **Dynamic configuration generation**: Modules in the `modules/` directory are automatically imported into all systems (can be overridden per system). New systems are automatically discovered by `genSystems()` ### Modern Nix Features This repository uses modern Nix features including: - **Flakes**: Enabled via `flake` experimental feature - **Nix Command**: Enabled via `nix-command` experimental feature - **Blake3 Hashes**: Enabled via `blake3-hashes` experimental feature - **Git Hashing**: Enabled via `git-hashing` experimental feature - **Verified Fetches**: Enabled via `verified-fetches` experimental 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 .#` - Build a specific system - `nix run .#` - Run a specific system - `nix flake update` - Update flake inputs ### Development Workflow 1. Make changes to system or user configuration 1. Test with `nh os switch` or `nh home switch` 1. For CI/CD, Hydra automatically builds and tests changes 1. Secrets are managed with SOPS and age keys ### Important Files - `flake.nix` - Main entry point for the flake - `systems/artemision/configuration.nix` - Example system configuration - `users/alice/home.nix` - Example user configuration - `modules/base.nix` - Base module with common settings - `hydra/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 `unstable` channel if the channel is specifiable (e.g., for `pkgs.`) ## 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.nix` calls `genSystems inputs outputs src (src + "/systems")` - `genSystems` scans the `systems/` 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, `genSystems` imports `systems//default.nix` - This file exports parameters for `constructSystem` like: - `users = [ "alice" ]` — which users to create - `home = true` — enable home-manager - `sops = true` — enable secret decryption - `server = true/false` — machine role - `modules = [ ... ]` — additional system-specific modules **Stage 3: Assembly** (`constructSystem` assembles the full config) - Loads essential system files: `hardware.nix`, `configuration.nix` - Auto-imports all `.nix` files from `modules/` directory via `lib.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//` | `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: ```nix { config, pkgs, lib, inputs, outputs, server, system, ... }: ``` - `config` — NixOS configuration options - `pkgs` — Nix packages (nixpkgs) - `lib` — Nix library functions (extended with `lib.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 desktops - `system` — System architecture string (e.g., `"x86_64-linux"`) ## Adding a New NixOS System ### Step 1: Create the Directory Structure ```bash mkdir -p systems/ cd systems/ ``` ### 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:** ```nix { 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: ```bash sudo nixos-generate-config --show-hardware-config > systems//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: ```nix { 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: ```bash # Create and encrypt secrets file sops systems//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: ```text systems// ├── 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`: ```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: ```bash # Build the new system nix build .# # 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 ```nix { 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 boolean ```nix config = lib.mkIf config.myService.enable { ... }; ``` - **`mkDefault`** — Provide a default value that can be overridden ```nix boot.kernelParams = lib.mkDefault [ "quiet" ]; ``` - **`mkForce`** — Force a value, preventing other modules from overriding ```nix services.openssh.enable = lib.mkForce true; ``` - **`mkEnableOption`** — Define an `enable` option with standard description ```nix options.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`: ```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): 1. `inputs.nixos-modules.nixosModule` (SuperSandro2000's convenience functions) 1. `inputs.nix-index-database.nixosModules.nix-index` 1. Hostname attribute from `genHostName` 1. `hardware.nix` (hardware-specific config) 1. `configuration.nix` (main system config) 1. **System-specific modules** from `modules` parameter in `default.nix` (e.g., custom-service.nix) 1. **All `.nix` files from global `modules/` directory** (features enabled across all systems) 1. SOPS module (if `sops = true`) 1. Home-manager module (if `home = true`) 1. User configurations (if `users = [...]` and `home = 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 1. Create `modules/my-feature.nix` with `options.myFeature.enable` 1. Set the feature enabled in `configuration.nix` of systems that need it: ```nix myFeature.enable = true; ``` 1. Or enable globally and disable selectively: ```nix # 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 1. Create user config: `users//default.nix` and `users//home.nix` 1. Update system's `default.nix`: ```nix users = [ "alice" "newuser" ]; ``` 1. Create secrets: `sops users//secrets.yaml` 1. Redeploy: `nh os switch` ### Override a Module's Default Behavior In any system's `configuration.nix`: ```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 ```bash # List all module paths being loaded nix eval .#nixosConfigurations..options --json | jq keys | head -20 # Evaluate a specific config value nix eval .#nixosConfigurations..config.services.openssh.enable ``` ### Validate Configuration Before Deploying ```bash # Check syntax and evaluate nix flake check # Build without switching nix build .# # Preview what would change nix build .# && 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: ```text systems//secrets.yaml # System-wide secrets users//secrets.yaml # User-specific secrets ``` ### Creating and Editing Secrets **Create or edit a secrets file:** ```bash # For a system sops systems//secrets.yaml # For a user sops users//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:** ```yaml # systems/palatine-hill/secrets.yaml acme: email: user@example.com api_token: "secret-token-here" postgresql: password: "db-password" ``` **Example secrets for a user:** ```yaml # 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: ```nix # 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: ```bash # 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//`), you need to register its age encryption key: 1. Generate the key on the target machine (if using existing deployment) or during initial setup 1. Add the public key to `.sops.yaml`: ```yaml keys: - &artemision - &palatine-hill - &new-hostname creation_rules: - path_regex: 'systems/new-hostname/.*' key_groups: - age: *new-hostname ``` 1. Re-encrypt existing secrets with the new key: ```bash 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`:** ```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`:** ```nix { gpu.enable = true; } ``` **Deploy:** ```bash 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`:** ```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`:** ```nix { inputs }: { users = [ "alice" ]; server = true; modules = [ ./postgresql-backup.nix ]; } ``` **Deploy:** ```bash 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`:** ```nix { steam.enable = false; # Override the module option } ``` The module in `modules/steam.nix` should use: ```nix config = lib.mkIf config.steam.enable { # steam configuration only if enabled }; ``` ## Debugging & Validation ### Check Module Evaluation ```bash # 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 ```bash # 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 ```bash # 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 ```