Files
nix-dotfiles/.github/copilot-instructions.md

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 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/<username>/:

  • 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
  2. Home-manager integration: User environment managed via home-manager
  3. SOPS secrets: Secrets managed with SOPS and age encryption
  4. Flake-based: Uses flakes for reproducible builds and development environments
  5. Multi-system support: Supports multiple machines with different configurations
  6. 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 .#<system> - Build a specific system
  • nix run .#<system> - Run a specific system
  • nix flake update - Update flake inputs

Development Workflow

  1. Make changes to system or user configuration
  2. Test with nh os switch or nh home switch
  3. For CI/CD, Hydra automatically builds and tests changes
  4. 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.<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.nixgenSystems)

  • 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/<hostname>/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/<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 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

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 boolean

    config = lib.mkIf config.myService.enable { ... };
    
  • mkDefault — Provide a default value that can be overridden

    boot.kernelParams = lib.mkDefault [ "quiet" ];
    
  • mkForce — Force a value, preventing other modules from overriding

    services.openssh.enable = lib.mkForce true;
    
  • mkEnableOption — Define an enable option with standard description

    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:

{ 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)
  2. inputs.nix-index-database.nixosModules.nix-index
  3. Hostname attribute from genHostName
  4. hardware.nix (hardware-specific config)
  5. configuration.nix (main system config)
  6. System-specific modules from modules parameter in default.nix (e.g., custom-service.nix)
  7. All .nix files from global modules/ directory (features enabled across all systems)
  8. SOPS module (if sops = true)
  9. Home-manager module (if home = true)
  10. 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

  2. Set the feature enabled in configuration.nix of systems that need it:

    myFeature.enable = true;
    
  3. 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

  1. Create user config: users/<username>/default.nix and users/<username>/home.nix

  2. Update system's default.nix:

    users = [ "alice" "newuser" ];
    
  3. Create secrets: sops users/<username>/secrets.yaml

  4. 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:

  1. Generate the key on the target machine (if using existing deployment) or during initial setup

  2. 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
    
  3. 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