mirror of
https://github.com/SebastianStork/nixos-config.git
synced 2026-03-22 17:49:07 +01:00
7.1 KiB
7.1 KiB
Copilot Instructions — nixos-config
Architecture
This is a NixOS flake managing multiple hosts using flake-parts. The flake output is composed entirely from flake-parts/*.nix — each file is auto-imported via builtins.readDir.
Layers (top → bottom)
- Hosts (
hosts/) — minimal per-machine config: profile import, overlay IP, underlay interface, enabled services. All.nixfiles in a host directory are auto-imported recursively byflake-parts/hosts.nix.- External hosts (
external-hosts/) — non-NixOS devices (e.g., a phone) that participate in the overlay network and syncthing cluster but aren't managed by NixOS. They importnixosModules.defaultdirectly (no profile) and only declarecustom.networkingandcustom.servicesoptions so their config values are discoverable by other hosts. This enables auto-generating nebula certs, DNS records, and syncthing device lists for them. allHosts=nixosConfigurations // externalConfigurations— passed to every module viaspecialArgs, so any module can query the full fleet including external devices.
- External hosts (
- Profiles (
profiles/) — role presets.core.nixis the base for all hosts;server.nixandworkstation.nixextend it. Profile names becomenixosModules.<name>-profile. - Modules (
modules/system/,modules/home/) — reusable NixOS/Home Manager modules auto-imported asnixosModules.default/homeModules.default. Every module is always imported; activation is gated bylib.mkEnableOption+lib.mkIf. - Users (
users/seb/) — Home Manager config. Per-host overrides live in@<hostname>/subdirectories (e.g.,users/seb/@desktop/home.niximports../home.nixand adds host-specific settings).
Networking model
- Underlay (
modules/system/networking/underlay.nix) — physical network via systemd-networkd. - Overlay (
modules/system/networking/overlay.nix,modules/system/services/nebula/) — Nebula mesh VPN (10.254.250.0/24, domainsplitleaf.de). All inter-host communication (DNS, Caddy, SSH) routes over the overlay. - DNS (
modules/system/services/dns.nix) — Unbound on overlay, auto-generates records fromallHosts.
Web services pattern
Each web service module (modules/system/web-services/*.nix) follows a consistent structure:
options.custom.web-services.<name> = { enable; domain; port; doBackups; };
config = lib.mkIf cfg.enable {
# upstream NixOS service config
custom.services.caddy.virtualHosts.${cfg.domain}.port = cfg.port; # reverse proxy
custom.services.restic.backups.<name> = lib.mkIf cfg.doBackups { ... }; # backup
custom.persistence.directories = [ ... ]; # impermanence
};
Hosts enable services declaratively: custom.web-services.forgejo = { enable = true; domain = "git.example.com"; doBackups = true; };
Conventions
- All custom options live under
custom.*— never pollute the top-level NixOS namespace. cfgbinding: alwayslet cfg = config.custom.<path>;at module top.- Pipe operator (
|>): used pervasively instead of nested function calls. - No repeated attrpaths (per
statix): group assignments into a single attrset instead of repeating the path. E.g.custom.networking.overlay = { address = "..."; role = "server"; };— notcustom.networking.overlay.address = "..."; custom.networking.overlay.role = "server";. Setting a single attribute with the full path is fine. Conversely, don't nest single-key attrsets unnecessarily — usecustom.networking.overlay.address = "...";notcustom = { networking = { overlay = { address = "..."; }; }; };. lib.singletoninstead of[ x ]for single-element lists.lib.mkEnableOption "": empty string is intentional — descriptions come from the option path.- Secrets: sops-nix with age keys. Each host/user has
secrets.json+keys/age.pub. The.sops.yamlat repo root is a placeholder — the real config is generated vianix build .#sops-config(seeflake-parts/sops-config.nix). - Impermanence: servers use
custom.persistence.enable = truewith an explicit/persistmount. Modules add their state directories viacustom.persistence.directories. - Formatting:
nix fmtrunsnixfmt+prettier+just --fmtvia treefmt. - Path references: use
./for files in the same directory or a subdirectory. Use${self}/...when the path would require going up a directory (../). Never use../. - Cross-host data: modules receive
allHostsviaspecialArgs(see Hosts layer above). Used by DNS, nebula static host maps, syncthing device lists, and caddy service records.
Developer Workflows
| Task | Command |
|---|---|
| Rebuild & switch locally | just switch |
| Test config without switching | just test |
| Deploy to remote host(s) | just deploy hostname1 hostname2 |
| Format all files | just fmt or nix fmt |
| Run flake checks + tests | just check |
| Check without building | just check-lite |
| Update flake inputs | just update |
| Edit SOPS secrets | just sops-edit hosts/<host>/secrets.json |
| Rotate all secrets | just sops-rotate-all |
| Install a new host | just install <host> root@<ip> |
| Open nix repl for a host | just repl <hostname> |
SOPS commands auto-enter a nix develop .#sops shell if sops isn't available, which handles Bitwarden login and age key retrieval.
Adding a New Module
- Create
modules/system/services/<name>.nix(orweb-services/,programs/, etc.). - Define options under
options.custom.<category>.<name>withlib.mkEnableOption "". - Guard all config with
lib.mkIf cfg.enable { ... }. - For web services: set
custom.services.caddy.virtualHosts, optionallycustom.services.restic.backups, andcustom.persistence.directories. - No imports needed — the file is auto-discovered by
flake-parts/modules.nix.
Adding a New Host
- Create
hosts/<hostname>/withdefault.nix,disko.nix,hardware.nix,secrets.json, andkeys/(containingage.pub,nebula.pub). - In
default.nix, import the appropriate profile (self.nixosModules.server-profileorself.nixosModules.workstation-profile) and setcustom.networking.overlay.address+custom.networking.underlay.*. - The host is auto-discovered by
flake-parts/hosts.nix— no registration needed.
Tests
Integration tests live in tests/ and use NixOS VM testing (pkgs.testers.runNixOSTest). Run via just check. Key details:
- Each test directory contains a
default.nixthat returns a test attrset (withdefaults,nodes,testScript, etc.). - The
defaultsblock importsself.nixosModules.defaultand overridesallHostswith the test's ownnodesvariable:_module.args.allHosts = nodes |> lib.mapAttrs (_: node: { config = node; });. This scopes cross-host lookups (DNS records, nebula static maps, etc.) to only the test's VMs, preventing evaluation of real host configs. - Test nodes define their own overlay addresses, underlay interfaces, and use pre-generated nebula keys from
tests/*/keys/. - The
testScriptis written in Python, using helpers likewait_for_unit,succeed, andfailto assert behavior.