7.0 KiB
7.0 KiB
AGENTS.md - server-config (NixOS server "muffin")
Overview
NixOS flake-based server configuration for host muffin (deployed to root@server-public).
Uses deploy-rs for remote deployment, disko for disk management, impermanence (tmpfs root),
agenix for secrets, lanzaboote for secure boot, and ZFS for data storage.
Target Hardware
- CPU: AMD Ryzen 5 5600X (6C/12T, Zen 3 /
znver3) - RAM: 64 GB DDR4, no swap
- Motherboard: ASRock B550M Pro4
- Boot drive: WD_BLACK SN770 1TB NVMe (f2fs: 20G /persistent, 911G /nix; root is tmpfs)
- SSD pool
tank: 4x 2TB SATA SSDs (raidz2) -- services, backups, music, misc - HDD pool
hdds: 4x 18TB Seagate Exos X18 (raidz1)-- torrents- Connected via esata to external enclosure
- USB: 8GB VFAT drive mounted at /mnt/usb-secrets (agenix identity key)
- GPU: Intel (integrated, xe driver) -- used for Jellyfin hardware transcoding
- NIC: enp4s0 (static 192.168.1.50/24)
Build / Deploy / Test Commands
# Format code (nixfmt-tree)
nix fmt
# Build the system configuration (check for eval errors)
nix build .#nixosConfigurations.muffin.config.system.build.toplevel -L
# Deploy to server
nix run .#deploy -- .#muffin
# Run ALL tests (NixOS VM tests, takes a long time)
nix build .#packages.x86_64-linux.tests -L
# Run a SINGLE test by name (preferred during development)
nix build .#test-zfsTest -L
nix build .#test-testTest -L
nix build .#test-fail2banSshTest -L
nix build .#test-ntfyAlertsTest -L
nix build .#test-filePermsTest -L
# Pattern: nix build .#test-<testName> -L
# Test names are defined in tests/tests.nix (keys of the returned attrset)
# Check flake outputs (list what's available)
nix flake show
# Evaluate without building (fast syntax/eval check)
nix eval .#nixosConfigurations.muffin.config.system.build.toplevel --no-build 2>&1 | head -5
Code Style
Nix Formatting
- Formatter:
nixfmt-tree(declared in flake.nix). Always runnix fmtbefore committing. - Indentation: 2 spaces (enforced by nixfmt-tree).
Module Pattern
Every .nix file is a function taking an attrset with named args and ...:
{
config,
lib,
pkgs,
service_configs,
...
}:
{
# module body
}
- Function args on separate lines, one per line, with trailing comma.
- Opening brace on its own line for multi-line arg lists.
- Use
service_configs(fromservice-configs.nix) for all ports, paths, domains -- never hardcode.
Service File Convention
Each service file in services/ follows this structure:
importsblock withlib.serviceMountWithZpooland optionallylib.serviceFilePerms- Service configuration (
services.<name> = { ... }) - Caddy reverse proxy vhost (
services.caddy.virtualHosts."subdomain.${service_configs.https.domain}") - Firewall rules if needed (
networking.firewall.allowed{TCP,UDP}Ports) - fail2ban jail if the service has authentication (
services.fail2ban.jails.<name>)
Custom Lib Functions (modules/lib.nix)
lib.serviceMountWithZpool serviceName zpoolName [dirs]-- ensures ZFS datasets are mounted before service starts, validates pool membershiplib.serviceFilePerms serviceName [tmpfilesRules]-- sets file permissions via systemd-tmpfiles before service startslib.optimizePackage pkg-- applies-O3 -march=znver3 -mtune=znver3compiler flagslib.vpnNamespaceOpenPort port serviceName-- confines service to WireGuard VPN namespace
Naming Conventions
- Files: lowercase with hyphens (
jellyfin-qbittorrent-monitor.nix) - Test names: camelCase with
Testsuffix intests/tests.nix(fail2banSshTest,zfsTest) - Ports: all declared in
service-configs.nixunderports.*, referenced asservice_configs.ports.<name> - ZFS datasets:
tank/services/<name>for SSD-backed,hdds/services/<name>for HDD-backed - Commit messages: terse, lowercase; prefix with service/module name when scoped (
caddy: add redirect,zfs: remove unneeded options). Generic changes useupdateor short description.
Secrets
- git-crypt:
secrets/directory andusb-secrets/usb-secrets-key*are encrypted (see.gitattributes) - agenix: secrets declared in
modules/age-secrets.nix, decrypted at runtime to/run/agenix/ - Identity: USB drive at
/mnt/usb-secrets/usb-secrets-key - Encrypting new secrets: The agenix identity is an SSH private key at
usb-secrets/usb-secrets-key(git-crypt encrypted). To encrypt a new secret, use the SSH public key directly withage -R:age -R <(ssh-keygen -y -f usb-secrets/usb-secrets-key) -o secrets/<name>.age /path/to/plaintext - DO NOT use
ssh-to-age. Usingssh-to-ageto derive a native age public key and then encrypting withage -r age1...producesX25519recipient stanzas. The SSH private key identity on the server can only decryptssh-ed25519stanzas. This mismatch causesage: error: no identity matched any of the recipientsat deploy time. Always useage -Rwith the SSH public key directly. - Never read or commit plaintext secrets. Never log secret values.
Important Patterns
- Impermanence: Root
/is tmpfs. Only/persistent,/nix, and ZFS mounts survive reboots. Any new persistent state must be declared inmodules/impermanence.nix. - Port uniqueness:
flake.nixhas an assertion that all ports inservice_configs.portsare unique. Always add new ports there. Make sure to put them in the specific "Public" and "Private" sections that are seperated by comments. - Hugepages: Services needing large pages declare their budget in
service-configs.nixunderhugepages_2m.services. The kernel sysctl is set automatically from the total. - Domain: Primary domain is
sigkill.computer. Old domaingardling.comredirects automatically. - Hardened kernel: Uses
_hardenedkernel. Security-sensitive defaults apply. - PostgreSQL as central database: All services that support PostgreSQL MUST use it instead of embedded databases (H2, SQLite, etc.). Connect via Unix socket with peer auth when possible (JDBC services can use junixsocket). The PostgreSQL instance is declared in
services/postgresql.nixwith ZFS-backed storage. UseensureDatabases/ensureUsersto auto-create databases and roles.
Test Pattern
Tests use pkgs.testers.runNixOSTest (NixOS VM tests):
{ config, lib, pkgs, ... }:
pkgs.testers.runNixOSTest {
name = "descriptive-test-name";
nodes.machine = { pkgs, ... }: {
imports = [ /* modules under test */ ];
# VM config
};
testScript = ''
start_all()
machine.wait_for_unit("multi-user.target")
# Python test script using machine.succeed/machine.fail
'';
}
- Register new tests in
tests/tests.nixwithhandleTest ./filename.nix - Tests needing the overlay should use
pkgs.appendOverlays [ (import ../modules/overlays.nix) ] - Test scripts are Python; use
machine.succeed(...),machine.fail(...),assert,subtest
SSH Access
ssh root@server-public # deploy user
ssh primary@server-public # normal user (doas instead of sudo)