# Agent Guidelines for nix-podman-stacks Declarative Podman Quadlet management using Home Manager and Nix. ## Build/Lint/Test Commands ### Format Code ```bash nix fmt . ``` ### Validate Flake ```bash nix flake check --keep-going ``` ### Build Configurations ```bash # Full CI config (tests all stacks) nix build .#homeConfigurations.ci.activationPackage # Template config nix build ./template#homeConfigurations.myhost.activationPackage # Documentation nix build .#packages.x86_64-linux.book nix build .#packages.x86_64-linux.search ``` ### Integration Tests Each stack that ships a `modules//vm-test.nix` gets a NixOS VM integration test that activates the stack like a real deployment and verifies that all its systemd user services reach a stable `active (running)` state. ```bash # Requires network access for the VM, hence sandbox is disabled nix build .#integrationTests.x86_64-linux.-integration --no-link --option sandbox false ``` The central test scaffolding lives in `tests/` (`base-config.nix`, `base-home.nix`, `mk-integration-test.nix`, `check.sh`, `driver.py`, `dummy-values.nix`) and is merged with the per-stack `vm-test.nix` home-manager fragment. The scaffolding is also exported as the `lib` flake output (`lib.mkIntegrationTest`, `lib.stackTestModules`) so consumers can test their own configuration. The `integrationTests` output is generated with `self.lib.mkIntegrationTest`, so repo and consumer tests share one code path. When changing anything under `tests/` or `lib/`, update the `integrationTests` call site in `flake.nix` and the options table in `docs/book/testing.md` with it. CI builds the stacks matching changed files and falls back to `it-tools` when the shared scaffolding changed, but it only covers `x86_64-linux`/KVM, so verify locally with: `nix build .#integrationTests.x86_64-linux.it-tools-integration --option sandbox false`. `npsTest.*` options are declared by `tests/base-config.nix`. ## Module Structure Every module (`modules//default.nix`) follows this pattern: ```nix { config, lib, pkgs, ... }: let name = "modulename"; cfg = config.nps.stacks.${name}; storage = "${config.nps.storageBaseDir}/${name}"; # category, description, displayName for Homepage/Glance in { imports = import ../mkAliases.nix config lib name [name]; options.nps.stacks.${name} = { enable = lib.mkEnableOption name; # ... options }; config = lib.mkIf cfg.enable { # Configuration }; } ``` ### Test Files When adding a new module, also add a `modules//vm-test.nix` (a home-manager fragment enabling the stack with dummy secrets) so the stack gets a NixOS VM integration test. Available dummy secrets are exposed via `_module.args` (`dummySecretFile`, `dummyClientSecretHash`) from `tests/base-home.nix`, which sources them from `tests/dummy-values.nix`. See `modules/it-tools/vm-test.nix` for a minimal example. A memory hungry stack raises `npsTests.memorySize` in its own `vm-test.nix` (e.g. `immich`). That option is declared by `tests/base-home.nix` and used by `tests/base-config.nix`, so exported stack test modules work standalone. ## Container Configuration Patterns ### Traefik & Network - `traefik.name = name` - **only on routable** (web-facing) containers - `stack = name` - **all containers** in a stack share a Podman network - `dependsOnContainer` / `wantsContainer` for container dependencies ### Dashboard Configs Use the `dashboard` option, it configures Homepage and Glance from a single value: ```nix dashboard = { inherit category description; # let variables, applied to both dashboards name = displayName; icon = "di:jellyfin"; # Glance syntax, translated for Homepage parent = name; # optional, child service, Glance only }; homepage.settings.widget.type = "jellyfin"; # Homepage only glance.icon = "si:jellyfin"; # Glance only ``` - `dashboard` - **all containers** - `homepage` / `glance` - only for what is specific to one dashboard - containers with a `parent` are child services, they are not shown on Homepage ### Volume Abstraction Use `volumeMap` attrset (not `volumes` list) for easier single-entry override: ```nix volumeMap = { data = "${storage}/data:/data"; config = "${storage}/config:/config"; }; ``` ## OIDC Patterns **With claim-based admin mapping** (services support admin role via groups): ```nix oidc = { enable = lib.mkOption { ... }; clientSecretFile = (import ../authelia/options.nix lib).clientSecretFile; clientSecretHash = (import ../authelia/options.nix lib).derivableClientSecretHash ...; adminGroup = lib.mkOption { default = "${name}_admin"; ... }; userGroup = lib.mkOption { default = "${name}_user"; ... }; }; ``` **UserGroup only** (service doesn't support admin role mapping): ```nix oidc = { enable = lib.mkOption { ... }; clientSecretFile = ...; clientSecretHash = ...; userGroup = lib.mkOption { default = "${name}_user"; ... }; # No adminGroup - service only checks user membership }; ``` ## Database Patterns **Simple** (single DB type, e.g., SQLite or external): ```nix db.passwordFile = lib.mkOption { type = lib.types.path; ... }; ``` **Full** (sqlite/postgres choice): ```nix db = { type = lib.mkOption { type = lib.types.enum ["sqlite" "postgres"]; default = "sqlite"; ... }; username = lib.mkOption { default = name; ... }; # postgres only passwordFile = lib.mkOption { type = lib.types.path; ... }; }; ``` ## Naming Conventions - **Options**: camelCase (`enablePrometheusExport`) - **Image tags**: stable versions only, never `latest` - **Image registry**: prefer `ghcr.io` over `docker.io` when both are available ## Option Patterns ### Standard Option Always include descriptions: ```nix lib.mkOption { type = lib.types.str; description = "What this option does."; } ``` ### extraEnv Option For services with many environment variables: ```nix extraEnv = lib.mkOption { type = (import ../types.nix lib).extraEnv; default = {}; description = "Extra environment variables (supports fromFile, fromTemplate, fromCommand)."; }; ``` ### Environment Variable Values When setting environment variables in modules use native Nix types: ```nix # Good - native types environment = { ENABLE_FEATURE = true; # boolean, not "true" MAX_CONNECTIONS = 5; # integer, not "5" DEBUG_MODE = false; # boolean, not "false" PG_PORT = 5432; # integer port number }; # Avoid - unnecessary strings environment = { ENABLE_FEATURE = "true"; MAX_CONNECTIONS = "5"; PG_PORT = "5432"; }; ``` ### Config Files For YAML/JSON configurable services: ```nix yaml = pkgs.formats.yaml {}; settings = lib.mkOption { type = yaml.type; apply = yaml.generate "config.yml"; description = "Service configuration"; }; ``` ### Shared Utilities ```nix (import ../types.nix lib).extraEnv (import ../authelia/options.nix lib).clientSecretFile (import ../socket-proxy/mkSocketProxyOptionModule.nix {stack = name;}) ``` ### Option Exposure Guidelines Keep modules minimal. For details, see: - **Secrets**: Always expose required secrets (see [Secrets Handling](#secrets-handling)) - **Database**: Expose `db.type` only when supporting multiple DB types (see [Database Patterns](#database-patterns)) - **Environment**: Expose `extraEnv` when service has a lot of configurable env vars (see [Option Patterns](#option-patterns)) - **OIDC**: Never expose Authelia URLs or internal auth settings — auto-set when OIDC enabled ## Documentation & Testing - Update `modules//README.md` when adding options with short description, reference to Github and website (if present) and a short usage example - Add new module to root level README.md - Test: add to `module_list.nix` and `ci_config.nix`, then run: ```bash nix flake check --keep-going nix build .#homeConfigurations.ci.activationPackage ``` ## Secrets Handling - Use `config.sops.secrets."path/to/secret".path` for sops-nix examples - Use `dummySecretFile` in `ci_config.nix` for CI testing - Never commit real secrets ### Secret File Options Always use `lib.types.path` for secret options to support reading secrets from files at runtime, e.g. from sops-nix decrypted files: ```nix secretKeyFile = lib.mkOption { type = lib.types.path; description = "Secret key. Generate with `openssl rand -hex 16`"; }; clientSecretFile = (import ../authelia/options.nix lib).clientSecretFile; db.passwordFile = lib.mkOption { type = lib.types.path; description = "Database password file"; }; ``` ## Reference Modules Use these modules as examples for implementing specific patterns: | Module | Demonstrates | | --------------------------------- | -------------------------------------------- | | `modules/example/default.nix` | Complete module structure (the template) | | `modules/streaming/default.nix` | Multi-container stack with dependencies | | `modules/gatus/default.nix` | Full DB pattern (postgres/sqlite) + extraEnv | | `modules/mealie/default.nix` | OIDC with admin/user groups | | `modules/vaultwarden/default.nix` | OIDC userGroup only (no admin mapping) | | `modules/paperless/default.nix` | Full database pattern + secretKeyFile | | `modules/blocky/default.nix` | Config files (YAML settings generation) | | `modules/aiostreams/default.nix` | extraEnv usage patterns | ## Renovate Configuration When a project has multiple related container images (e.g., app + server), add them to the "Multi-Container Projects" group in `renovate.json` so they are updated together in a single PR.