Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

selfhost-nix is a set of opinionated NixOS modules for a single-admin selfhost: declare a service once and get ingress, authentication, secrets, monitoring, a dashboard tile, backups, and notifications from that one definition. Each bundled default sits behind a neutral contract you can swap, and everything is off until you enable it.

⚠️ Built for a private network (LAN or VPN). Nothing here is hardened for the public internet. Exposing a service is yours to design and secure, and it is out of scope.

One person’s fleet, shared as a reference and starting point. Opinionated and still unstable, so you fork to vary the rest.

New here? Getting started installs it, Concepts explains how it fits together, Recipes wire a service end to end, and the Options reference lists every selfhost.* option.

Getting started

⚠️ These services are meant for a private network: LAN or VPN, not the public internet. The defaults harden nothing for internet exposure. Doing it safely is entirely your responsibility and is out of scope. Don’t port-forward 80/443 to this host and assume it’s safe.

Add the flake input

# flake.nix
inputs.selfhost-nix.url = "github:bphenriques/selfhost-nix";
inputs.selfhost-nix.inputs.nixpkgs.follows = "nixpkgs";

Import the module into a host:

imports = [ inputs.selfhost-nix.nixosModules.default ];

Enable it

Set selfhost.enable and selfhost.domain, then turn on the providers you want: ingress.traefik.enable, auth.oidc.pocket-id.enable, notify.ntfy.enable, monitoring.enable. Now register services with selfhost.services.<name>. Registering wires the cross-cutting parts (route, auth, dashboard tile, healthcheck, secrets). It does not run the service. You enable the upstream services.<name> and connect the values it derives. Recipes wires one end to end, and Concepts explains the model.

Prerequisites

  • A flake on nixpkgs unstable, with selfhost-nix.inputs.nixpkgs.follows = "nixpkgs".
  • A secrets backend (sops-nix, agenix, or plain files). The framework is path-based: every secret option takes a file path, never a value, so nothing secret reaches the Nix store. You wire the paths, and the backend is yours.

Concepts

One idea underpins everything: declare a service once, and every cross-cutting concern reads from that single definition. This page is the model. The options reference is the per-option truth.

The service registry

selfhost.services.<name> registers a service: a backend (host/port) and a derived public url (https://<subdomain>.<domain>). Registering isn’t running. You enable the upstream services.<name> and wire the values and secret files it derives (see Recipes). That one entry is what capabilities attach to:

  • ingress.enable: a reverse-proxy route at the public URL (on by default).
  • oidc.enable / forwardAuth.enable: authentication, one or the other (see below).
  • integrations.homepage / .monitoring / .notify: a dashboard tile, health and metrics probes, and failure alerts. These default to their concern. Enable monitoring or a notify provider globally and every service opts in (a tile follows having a route).
  • backup.package: a pre-backup hook a target picks up.

Data the framework doesn’t model goes in extraConfig, a freeform slot on the entry that selfhost-nix never reads. Attach your own per-service metadata there (a landing-page category, say) rather than a separate tree keyed by service name, so it rides the same entry as the service. Read it back at config.selfhost.services.<name>.extraConfig, and a consumer module can give it a type.

selfhost.external.<name> puts things this host doesn’t run (a NAS) on the dashboard, without a route or backend. Public hosts and listening ports are checked across the whole registry by one assertion, so two services can’t silently collide.

Concerns & contracts

A cross-cutting concern is a provider-neutral interface (selfhost.<concern>) that other modules read, filled by at most one implementation (selfhost.<concern>.<impl>.enable). To swap it, disable the bundled one and set the interface yourself. Defaults: Traefik (ingress + TLS), Pocket-ID (OIDC), tinyauth (forward-auth), ntfy (notifications). Some concerns have no interface, because the tool is the contract (Prometheus + Alertmanager, rustic backups, CIFS storage). Disable it and handle the concern yourself.

Authentication

A service is gated one of two ways, never both. OIDC (oidc.enable) makes the app its own client, and users sign in at the provider. Forward-auth (forwardAuth.enable) has the edge authenticate first, for apps with no SSO. access.allowedGroups names who may enter (empty = any authenticated user). Clients, users, and groups are provisioned at boot, and credentials reach a service via LoadCredential or a supplementary group, never the Nix store. Identities and their tiers are the Users model.

First-party apps

A first-party app (selfhost.apps.<name>.enable, default-off) is a bundled application. Most register a selfhost.services.<name> entry and inherit everything above from one toggle. Apps that derive config from the framework also expose enableSelfhostIntegration (default on) to opt out of that wiring while still running. Most are HTTP behind ingress. A few aren’t: WireGuard is an ingress-less UDP server, and deSEC a headless DDNS timer. The catalog and each app’s options are in the reference.

Secrets outside the store

runtimeSecrets generates values at boot into a persistent directory, never the Nix store or your secrets backend. Each takes a missing-file policy: regenerate, leave absent, or generate-once for data-bound keys (see the options). runtimeTemplates render config that must embed a secret into tmpfs via opaque placeholders, so the value never reaches the store. Rotation is deliberate: remove the value and restart its generator (oidc-rotate wraps this for OIDC clients).

Storage & dashboard tiles

storage.smb.mounts are CIFS shares behind per-share access groups. A service requests storage.smb = [ … ] and the mount dependency is wired onto its unit (boot-race-safe mode chosen per share). Dashboard tiles come from services and externals that opt into integrations.homepage, grouped by group. The bundled apps.homepage renders them, or read the read-only dashboards.generatedTiles into your own. The framework supplies the data, you own the visuals.

Exposure

HTTP is opened only on ingress.allowedInterfaces (LAN, VPN), keeping services off the public internet. A single wildcard cert (*.<domain>) comes over ACME DNS-01, so issuance needs no inbound port. Putting services on the public internet is out of scope. There is no bundled hardening, and it is a security-sensitive decision you own.

Users

selfhost-nix models people and service identities as selfhost.users.<name>, across three access tiers via groups: admin, users, guests. At least one admin user is asserted (more is your call). Per-user attributes mirror the framework’s registry, so where an option lives tells you what it touches:

  • A user’s per-service config sits at selfhost.users.<name>.services.<service> — for any service, bundled app or one you registered yourself — mirroring selfhost.services.<service>. (selfhost.apps.<name> is a deploy shortcut with no per-user surface; per-user always belongs to the service.)
  • A cross-cutting concern’s per-user options sit at selfhost.users.<name>.<concern>, mirroring selfhost.<concern>, e.g. auth.oidc.enable.
selfhost.users.alice = {
  groups = [ "admin" ];
  services.filebrowser = { enable = true; storage = { … }; };  # per-user config for the filebrowser service
  services.wireguard.devices = [ … ];                          # per-user config for the wireguard service
  auth.oidc.enable = true;                                      # mirrors selfhost.auth.oidc
};

WireGuard devices

Each entry in services.wireguard.devices is a declarative peer. The server routes its ip to its publicKey. Keys are provisioned in two phases. The private key is minted on the server and never leaves it. The public key (not secret) is declared in config, so peers apply declaratively with no runtime wg set.

  1. Mint on the server (the WireGuard host). The client name is <username>-<device>. Pass the short --device so the generated interface name stays within 15 chars:

    $ sudo wg-manage add alice-phone --device phone
    Client 'alice-phone' provisioned (10.100.0.42)
      publicKey = "kQ…="
    # also prints the config QR
    

    The private key lands in /var/lib/wireguard/clients/ (0600, root). The public key and IP are printed.

  2. Declare it with the printed IP + public key, then rebuild:

    selfhost.users.alice.services.wireguard.devices = [
      { name = "phone"; ip = "10.100.0.42"; fullAccess = true; publicKey = "kQ…="; }
    ];
    

    fullAccess = true reaches the whole LAN. false reaches only the server.

Re-show a QR with sudo wg-manage show alice-phone. wg-manage status lists handshakes. To remove a device, delete it from the registry and rebuild, then sudo wg-manage remove alice-phone to wipe its key.

Extending per-user as a consumer

To give your own registered service a per-user surface, declare it at selfhost.users.<name>.services.<service> — the same place a bundled app declares its own, mirroring the top-level selfhost.services.<service>. Your data rides on the same user object as its identity, so there is no parallel user tree to join and identity stays single-source.

# your module: a typed per-user fragment for your service
options.selfhost.users = lib.mkOption {
  type = lib.types.attrsOf (lib.types.submodule {
    options.services.jellyfin.enable = lib.mkEnableOption "Jellyfin account for this user";
  });
};

Read it back off config.selfhost.users.<name>, which carries both the framework’s username/isAdmin/… and your per-service options:

lib.filterAttrs (_: u: u.services.jellyfin.enable) config.selfhost.users

For per-user data with no service to hang it on, use the never-read passthrough selfhost.users.<name>.extraConfig instead.

Recipes

First-party apps pre-wire a curated few. This is the other side: how you register a service the framework doesn’t bundle, doing by hand the wiring an app does for you. For real, always-current examples see Examples in the wild.

selfhost.services.<name> registers a service: route, OIDC client, dashboard tile, healthcheck, and backup hook. It does not run it. You enable the upstream service and feed it the values the framework derives and the secret files it generates.

Wiring a service (Miniflux + OIDC)

{ config, lib, ... }:
let
  svc = config.selfhost.services.miniflux;
  oidc = config.selfhost.auth.oidc;
in
{
  # 1. Register: route, OIDC client (admins only), tile, healthcheck.
  selfhost.services.miniflux = {
    port = 8081;
    healthcheck.path = "/healthcheck";
    access.allowedGroups = [ config.selfhost.groups.admin ];
    oidc = {
      enable = true;
      systemd.dependentServices = [ "miniflux" ]; # start after its client is provisioned
    };
    integrations.homepage.enable = true;
  };

  # 2. Run it, fed by the derived values + generated secret files.
  services.miniflux = {
    enable = true;
    createDatabaseLocally = true;
    config = {
      LISTEN_ADDR = "127.0.0.1:${toString svc.port}";
      BASE_URL = svc.publicUrl;
      OAUTH2_PROVIDER = "oidc";
      OAUTH2_USER_CREATION = 1;
      OAUTH2_OIDC_DISCOVERY_ENDPOINT = oidc.provider.issuerUrl;
      OAUTH2_OIDC_PROVIDER_NAME = oidc.provider.displayName;
      OAUTH2_REDIRECT_URL = builtins.head svc.oidc.callbackURLs;
      OAUTH2_CLIENT_ID_FILE = svc.oidc.id.file; # provisioned at boot
      OAUTH2_CLIENT_SECRET_FILE = svc.oidc.secret.file; # never in the Nix store
    };
  };

  # 3. Let miniflux read those files (it takes them as *_FILE env vars).
  systemd.services.miniflux.serviceConfig.SupplementaryGroups = svc.oidc.systemd.supplementaryGroups;
}

The framework owns the route, client provisioning, tile, healthcheck, and secrets, and exposes them as derived attributes (svc.publicUrl, svc.oidc.id.file, …). You own the service and the few lines that connect the two.

Variations

Same shape (register, run, wire) with small deltas:

  • Secrets in-settings (e.g. Immich): a service that takes a file path in its own config skips the env vars (settings.oauth = { inherit (oidc.provider) issuerUrl; clientId._secret = svc.oidc.id.file; clientSecret._secret = svc.oidc.secret.file; }) but still needs the supplementary group, since it reads those files as its own user (SupplementaryGroups = svc.oidc.systemd.supplementaryGroups).
  • Forward-auth instead of OIDC (no SSO of its own): drop the oidc block, set forwardAuth.enable = true, and the edge authenticates.
  • Native auth (the app logs users in itself, e.g. Jellyfin): register for the route and tile, enable no framework auth.
  • A container: bind it to 127.0.0.1:<port> and register that port. It’s proxied and monitored like any native service, and its database, volumes, and env stay yours.

Resource limits

Throttling or prioritising a service is host-specific tuning, plain systemd with no framework option. Target the unit by name, using a per-service cap or a shared slice. (Some upstream modules pin their own slice, e.g. Immich, which you override with lib.mkForce.)

systemd.services.jellyfin.serviceConfig.CPUQuota = "150%"; # per-service cap

# or a shared budget across services
systemd.slices.media.sliceConfig = {
  CPUQuota = "300%";
  MemoryHigh = "8G";
};
systemd.services.jellyfin.serviceConfig.Slice = "media.slice";
systemd.services.immich-server.serviceConfig.Slice = lib.mkForce "media.slice";

Examples in the wild

bphenriques/dotfiles is the reference deployment: a real host wiring a couple dozen services (*arr stack, Jellyfin, Immich, containers, tasks, backups), always in sync with this flake. It is the exhaustive catalogue this page deliberately isn’t.

Media automation

The *arr apps (apps.radarr, apps.sonarr, apps.prowlarr) follow one rule that shapes the whole design: the framework wires the plumbing, and never configures a source.

The boundary

A media-automation stack has two halves. One is generic infrastructure: ingress, forward-auth, an API key kept out of the store, notifications, a backup of the library list, and the connections between the tools (root folders, download clients). The other is acquisition: which indexers and trackers to search, which release qualities to prefer, which categories to file under.

selfhost-nix owns the first half and ships nothing of the second. The words indexer, tracker, quality profile, custom format appear nowhere in it. Its plumbing is inert on its own: a download client with no indexers behind it fetches nothing. So what and where you acquire is entirely yours, kept in your own (typically private) config, never in the framework.

That makes the apps neutral, general media tooling with legitimate use, and keeps every acquisition decision and its consequences with the operator.

What the app wires

apps.radarr / apps.sonarr register the service (ingress, forward-auth defaulting to the active provider, admin-group access), generate the API key out of the store and set the app to trust the forward-auth identity, add a library-list backup hook, and run an idempotent reconcile that applies only what you declare:

  • rootFolders: library paths (storage-agnostic, and the path must exist).
  • downloadClients: registered generically via the app’s own schema. You name the implementation and protocol, so it’s never assumed to be torrent (or Transmission). The app connection-tests a client on save, so order the reconcile after the client’s unit with configureAfter.
  • delayProfile: optional, carrying the protocol preference, so it stays your call with no default.

All three default to empty/none: enabling an app configures nothing you didn’t ask for.

apps.prowlarr is wiring-only. An indexer manager talks to APIs, not files, so it has no root folders or download clients. Its indexer list and app-sync are acquisition and live in your config, reading the apps’ apiKeyFile.

What stays yours

  • Indexers / trackers: Prowlarr, from your private config.
  • Quality profiles / custom formats: taste, e.g. a recyclarr unit syncing TRaSH guides. The framework neither bundles nor schedules it (a network-fetching opinion is not plumbing).
  • Download-client ordering: configureAfter = [ "transmission.service" ] (or your usenet client).
  • Where the data lives: root-folder paths and the storage mount, as with any service.

Security

The framework wires the cross-cutting security plumbing. The trust anchors and the host stay yours. Know the split.

What the framework does

  • Secrets off the store. runtimeSecrets are generated with openssl into /var/lib/homelab-secrets (root-owned, tight modes), never in the world-readable Nix store. Templates render on tmpfs, and generation runs sandboxed (ProtectSystem = strict, write scoped to the secrets dir).
  • Durable secret lifecycle. regenerateIfMissing handles disposable random secrets. generateOnce (+ generateOnceGuard) handles data-bound keys, where a key lost while its data survives is left absent and logged, never silently replaced.
  • The edge is the only public surface. Services bind 127.0.0.1, and the reverse proxy fronts them. Access is gated by per-service OIDC clients (group-scoped) or forwardAuth. The forwardAuth middleware sets the identity headers from the auth response, and an assertion refuses forwardAuth with no active provider.
  • Hardened service units. Bundled reconcilers and backups run with ProtectSystem = strict, NoNewPrivileges, etc. The notify token reaches non-root consumers via systemd LoadCredential.
  • WireGuard, not public exposure. That is the way in.

What it will NOT do: your responsibility

  • The root of trust. It generates its own runtime secrets, but any secret you supply (sops/age, …) and the keys that decrypt them are yours to store, back up, and rotate. The framework never sees your master key.
  • Back up generate-once keys. A generateOnce key is safe from silent replacement, but the framework does not back it up. Lose it while its data survives and that data is unrecoverable. Copy these out-of-band. This is the sharpest edge here.
  • Harden the host. SSH, firewall baseline, kernel and account hardening, disk encryption: all the host’s job. The framework hardens its service sandboxes, not your machine.
  • Make the exposure decision. Putting a service on the public internet is your call and your risk, a deliberate out-of-scope choice. Nothing here lightens it.
  • Guard a misconfigured edge. Backends bind localhost so the proxy is the only path in. Keep it that way. Any custom edge or proxy-auth you wire must strip client-supplied identity headers, or they are spoofable.

Key rotation

SecretHow it rotates
OIDC client secretsFramework-managed: oidc-rotate [<client>] (always available) or the opt-in rotation timer. It removes the secret and the provider re-mints it.
Random per-service secrets (regenerateIfMissing)Delete the file. Regenerated on next activation.
Data-bound keys (generateOnce)Manual and deliberate. Rotating means re-keying the data it protects, so the framework refuses to auto-rotate (that would brick the data). Remove the secret together with its data (or its .generated marker) to re-key.
Externally-synced secrets (regenerateIfMissing = false)Rotate in your own store. The framework leaves them untouched.

Restore & disaster recovery

There is deliberately no restore command. In a real disaster the host is gone, and any wrapper it built with it, so recovery is a manual runbook you must be able to run from nothing but your backups and your out-of-band keys. Rehearse it before you need it.

What the backups contain. Each selfhost.backup.targets.<name> is an independent rustic repository. On the live host its profile is written to /etc/rustic/<name>.toml (repository + password-file, plus a <name>-secrets.toml for backend credentials). A snapshot holds only what was staged into that target’s tree: your bindings (paths mounted read-only) and each hook’s output under extras/<hook> (e.g. a Gitea repo copy, a DB dump). A hook output is material to replay, not a live service. Restoring a DB dump means importing it, not dropping it onto a running database.

What is NOT in them. Runtime secrets (/var/lib/homelab-secrets) and OIDC credentials are not backed up unless you explicitly add them as a binding. Two things you must hold out-of-band, or the rest is unrecoverable:

  • your secrets-backend master key (sops/age). Without it none of the secrets you supply decrypt.
  • every generateOnce key (e.g. Pocket-ID’s encryption key). Lose one while its data survives and that data is gone. This is the sharpest edge in the whole system.

Recovery order.

  1. Rebuild the host and restore the secrets backend, putting the sops/age master key back first.
  2. Restore generate-once keys from your out-of-band copy into runtimeSecretsDir before their services start, so they decrypt existing data instead of the guard leaving them absent.
  3. nixos-rebuild switch. Disposable secrets regenerate, and OIDC users and clients re-provision.
  4. Restore data with rustic into each service’s data location, then replay hook outputs (import dumps, drop repos back). List and pull a snapshot with rustic -P <name> snapshots / rustic -P <name> restore <id> --to <dest>. On a bare machine, reconstruct the repo string, password, and backend credentials by hand from your secrets store first.

FileBrowser (multi-user)

services.filebrowser-multiuser adds the per-user access model FileBrowser lacks: proxy-auth users, each scoped to a directory with its own permissions, reconciled into its database. It sits on services.filebrowser (NixOS) and reads root/branding/view from there. This base module is standalone, usable without the selfhost framework (a non-selfhost host drives it directly).

Two entry points. The standalone base is the nixosModules.filebrowser-multiuser output. The selfhost adapter (below) ships inside nixosModules.default.

Access, not storage

A user’s access is one scope, a path under the FileBrowser root the host arranged. The module never creates or mounts directories, only authorizes a name at a path. So it stays backend-agnostic, and a listed scope with no directory fails startup rather than serving an empty view.

Auth is the edge’s job

FileBrowser runs in proxy-auth mode trusting authHeader (default Remote-User): a trusted edge (forwardAuth, a reverse-proxy’s BasicAuth) authenticates, sets the header, and must strip client-supplied values. The module authorizes the name, never authenticates it. An authenticated name not in users is auto-created from unlistedScope/unlistedPermissions. Point unlistedScope at an empty directory unless the edge admits only listed users.

Declarative database

The DB is a derived artifact: a reconciler rebuilds it from the declared config whenever that config changes (a plain reboot keeps it), so removed users drop and nothing drifts. It disables signup and the command runner. Don’t pair it with stateful FileBrowser features.

Selfhost integration

On a selfhost host, selfhost.apps.filebrowser.enable runs the app (and the base above). Its enableSelfhostIntegration (default on) exposes a per-user opt-in at selfhost.users.<name>.services.filebrowser: SMB storage grants (ro/rw) assembled into the user’s scope, with the service registered behind the active forwardAuth. Turn it off to wire users, storage, and auth yourself.

Options reference

selfhost.enable

Whether to enable home-server services.

Type: boolean

Default:

false

Example:

true

Declared by:

selfhost.apps.bentopdf.enable

Whether to enable the first-party BentoPDF app (static, client-side PDF toolkit).

Type: boolean

Default:

false

Example:

true

Declared by:

selfhost.apps.desec.enable

Whether to enable deSEC dynamic DNS updates.

Type: boolean

Default:

false

Example:

true

Declared by:

selfhost.apps.desec.domains

Hostnames to keep pointed at the current public IP.

Type: non-empty (list of string)

Example:

[
  "squirrel-plaza.dedyn.io"
]

Declared by:

selfhost.apps.desec.interval

Refresh period (systemd time span); a boot-time update also runs.

Type: string

Default:

"30min"

Declared by:

selfhost.apps.desec.tokenFile

File holding a deSEC API token authorized for domains.

Type: string

Declared by:

selfhost.apps.filebrowser.enable

Whether to enable the first-party FileBrowser app (per-user proxy-auth file sharing).

Type: boolean

Default:

false

Example:

true

Declared by:

selfhost.apps.filebrowser.enableSelfhostIntegration

Derive FileBrowser users and per-user SMB binds from selfhost.users grants and register behind selfhost forwardAuth. Turn off to run the app but wire users, storage and auth yourself.

Type: boolean

Default:

true

Declared by:

selfhost.apps.gitea.enable

Whether to enable the first-party Gitea app (git server with OIDC login).

Type: boolean

Default:

false

Example:

true

Declared by:

selfhost.apps.gitea.serviceAccounts

Non-human Gitea accounts (CI/bots), provisioned via the gitea CLI.

Type: attribute set of (submodule)

Default:

{ }

Declared by:

selfhost.apps.gitea.serviceAccounts.<name>.enable

Whether to enable this non-UI Gitea account.

Type: boolean

Default:

true

Example:

true

Declared by:

selfhost.apps.gitea.serviceAccounts.<name>.sshKeys

SSH keys for git-over-SSH; registered via the admin API on account creation.

Type: list of (submodule)

Default:

[ ]

Declared by:

selfhost.apps.gitea.serviceAccounts.<name>.sshKeys.*.key

Public key in authorized_keys format.

Type: string

Declared by:

selfhost.apps.gitea.serviceAccounts.<name>.sshKeys.*.readOnly

Register as a read-only (deploy) key.

Type: boolean

Default:

false

Declared by:

selfhost.apps.gitea.ssh.enable

Whether to enable Gitea’s built-in SSH server for git-over-SSH (off by default — exposes a TCP port; HTTPS git works without it).

Type: boolean

Default:

false

Example:

true

Declared by:

selfhost.apps.gitea.ssh.openFirewall

Whether to enable opening the SSH port in the firewall (all interfaces); leave off to scope it yourself.

Type: boolean

Default:

false

Example:

true

Declared by:

selfhost.apps.gitea.ssh.port

Listen port for the built-in SSH server.

Type: 16 bit unsigned integer; between 0 and 65535 (both inclusive)

Default:

2222

Declared by:

selfhost.apps.homepage.enable

Whether to enable the first-party homepage dashboard app (gethomepage).

Type: boolean

Default:

false

Example:

true

Declared by:

selfhost.apps.homepage.port

homepage listen port (localhost, behind ingress).

Type: 16 bit unsigned integer; between 0 and 65535 (both inclusive)

Default:

3001

Declared by:

selfhost.apps.miniflux.enable

Whether to enable the first-party Miniflux app (RSS reader with OIDC login).

Type: boolean

Default:

false

Example:

true

Declared by:

selfhost.apps.prowlarr.enable

Whether to enable the first-party Prowlarr app (indexer manager; wiring only, no indexers).

Type: boolean

Default:

false

Example:

true

Declared by:

selfhost.apps.prowlarr.apiKeyFile

Path to Prowlarr’s generated API key, for the consumer indexer-sync reconciler.

Type: string (read only)

Default: the generated API-key secret path

Declared by:

selfhost.apps.prowlarr.port

Prowlarr listen port (localhost, behind ingress).

Type: 16 bit unsigned integer; between 0 and 65535 (both inclusive)

Default:

9696

Declared by:

selfhost.apps.radarr.enable

Whether to enable the first-party Radarr app (media automation; ingress + auth + secrets wired, zero acquisition config).

Type: boolean

Default:

false

Example:

true

Declared by:

selfhost.apps.radarr.apiKeyFile

Path to Radarr’s generated API key, for consumer reconcilers (e.g. Prowlarr sync, recyclarr).

Type: string (read only)

Default: the generated API-key secret path

Declared by:

selfhost.apps.radarr.configureAfter

Extra units the reconcile must start after and want. Radarr connection-tests a download client on save, so order this after the client’s service (e.g. your torrent/usenet daemon).

Type: list of string

Default:

[ ]

Example:

[
  "transmission.service"
]

Declared by:

selfhost.apps.radarr.delayProfile

Optional default delay profile. Null = leave Radarr’s own default untouched. Carries the protocol preference — acquisition taste, no framework default.

Type: null or (submodule)

Default:

null

Declared by:

selfhost.apps.radarr.delayProfile.enableTorrent

This option has no description.

Type: boolean

Default:

true

Declared by:

selfhost.apps.radarr.delayProfile.enableUsenet

This option has no description.

Type: boolean

Default:

true

Declared by:

selfhost.apps.radarr.delayProfile.bypassIfHighestQuality

This option has no description.

Type: boolean

Default:

true

Declared by:

selfhost.apps.radarr.delayProfile.preferredProtocol

Protocol preference.

Type: one of “torrent”, “usenet”

Declared by:

selfhost.apps.radarr.delayProfile.torrentDelay

This option has no description.

Type: signed integer

Default:

0

Declared by:

selfhost.apps.radarr.delayProfile.usenetDelay

This option has no description.

Type: signed integer

Default:

0

Declared by:

selfhost.apps.radarr.downloadClients

Download clients to register. The framework applies them via the *arr schema; it ships none and assumes no protocol.

Type: list of (submodule)

Default:

[ ]

Declared by:

selfhost.apps.radarr.downloadClients.*.fields

Implementation-specific fields passed through to the client schema (host, port, category, …).

Type: attribute set of anything

Default:

{ }

Declared by:

selfhost.apps.radarr.downloadClients.*.implementation

The *arr download-client implementation (e.g. “Transmission”, “Sabnzbd”). No default — you choose.

Type: string

Declared by:

selfhost.apps.radarr.downloadClients.*.name

Client name in Radarr.

Type: string

Declared by:

selfhost.apps.radarr.downloadClients.*.protocol

Client protocol.

Type: one of “torrent”, “usenet”

Declared by:

selfhost.apps.radarr.port

Radarr listen port (localhost, behind ingress).

Type: 16 bit unsigned integer; between 0 and 65535 (both inclusive)

Default:

7878

Declared by:

selfhost.apps.radarr.rootFolders

Root library folders to ensure. Paths only — storage/protocol-agnostic.

Type: list of (submodule)

Default:

[ ]

Declared by:

selfhost.apps.radarr.rootFolders.*.defaultQualityProfile

Name of a quality profile (consumer/recyclarr-managed) to seed as this folder’s default; null = none.

Type: null or string

Default:

null

Declared by:

selfhost.apps.radarr.rootFolders.*.path

Library path (must exist on disk; typically a selfhost storage mount).

Type: string

Declared by:

selfhost.apps.radicale.enable

Whether to enable the first-party Radicale app (CalDAV/CardDAV server).

Type: boolean

Default:

false

Example:

true

Declared by:

selfhost.apps.radicale.enableSelfhostIntegration

Derive Radicale’s htpasswd users from selfhost.users grants. Turn off to run Radicale but manage its htpasswd file yourself.

Type: boolean

Default:

true

Declared by:

selfhost.apps.sonarr.enable

Whether to enable the first-party Sonarr app (media automation; ingress + auth + secrets wired, zero acquisition config).

Type: boolean

Default:

false

Example:

true

Declared by:

selfhost.apps.sonarr.apiKeyFile

Path to Sonarr’s generated API key, for consumer reconcilers (e.g. Prowlarr sync, recyclarr).

Type: string (read only)

Default: the generated API-key secret path

Declared by:

selfhost.apps.sonarr.configureAfter

Extra units the reconcile must start after and want. Sonarr connection-tests a download client on save, so order this after the client’s service (e.g. your torrent/usenet daemon).

Type: list of string

Default:

[ ]

Example:

[
  "transmission.service"
]

Declared by:

selfhost.apps.sonarr.delayProfile

Optional default delay profile. Null = leave Sonarr’s own default untouched. Carries the protocol preference — acquisition taste, no framework default.

Type: null or (submodule)

Default:

null

Declared by:

selfhost.apps.sonarr.delayProfile.enableTorrent

This option has no description.

Type: boolean

Default:

true

Declared by:

selfhost.apps.sonarr.delayProfile.enableUsenet

This option has no description.

Type: boolean

Default:

true

Declared by:

selfhost.apps.sonarr.delayProfile.bypassIfHighestQuality

This option has no description.

Type: boolean

Default:

true

Declared by:

selfhost.apps.sonarr.delayProfile.preferredProtocol

Protocol preference.

Type: one of “torrent”, “usenet”

Declared by:

selfhost.apps.sonarr.delayProfile.torrentDelay

This option has no description.

Type: signed integer

Default:

0

Declared by:

selfhost.apps.sonarr.delayProfile.usenetDelay

This option has no description.

Type: signed integer

Default:

0

Declared by:

selfhost.apps.sonarr.downloadClients

Download clients to register. The framework applies them via the *arr schema; it ships none and assumes no protocol.

Type: list of (submodule)

Default:

[ ]

Declared by:

selfhost.apps.sonarr.downloadClients.*.fields

Implementation-specific fields passed through to the client schema (host, port, category, …).

Type: attribute set of anything

Default:

{ }

Declared by:

selfhost.apps.sonarr.downloadClients.*.implementation

The *arr download-client implementation (e.g. “Transmission”, “Sabnzbd”). No default — you choose.

Type: string

Declared by:

selfhost.apps.sonarr.downloadClients.*.name

Client name in Sonarr.

Type: string

Declared by:

selfhost.apps.sonarr.downloadClients.*.protocol

Client protocol.

Type: one of “torrent”, “usenet”

Declared by:

selfhost.apps.sonarr.port

Sonarr listen port (localhost, behind ingress).

Type: 16 bit unsigned integer; between 0 and 65535 (both inclusive)

Default:

8989

Declared by:

selfhost.apps.sonarr.rootFolders

Root library folders to ensure. Paths only — storage/protocol-agnostic.

Type: list of (submodule)

Default:

[ ]

Declared by:

selfhost.apps.sonarr.rootFolders.*.defaultQualityProfile

Name of a quality profile (consumer/recyclarr-managed) to seed as this folder’s default; null = none.

Type: null or string

Default:

null

Declared by:

selfhost.apps.sonarr.rootFolders.*.path

Library path (must exist on disk; typically a selfhost storage mount).

Type: string

Declared by:

selfhost.apps.transmission.enable

Whether to enable the first-party Transmission app (torrent client).

Type: boolean

Default:

false

Example:

true

Declared by:

selfhost.apps.wireguard.enable

Whether to enable WireGuard VPN server (interface, keys, user/device registry, client provisioning).

Type: boolean

Default:

false

Example:

true

Declared by:

selfhost.apps.wireguard.address

Server address with CIDR (e.g. 10.100.0.1/24).

Type: string

Declared by:

selfhost.apps.wireguard.clientSubnet

Client address subnet (e.g. 10.100.0.0/24).

Type: string

Declared by:

selfhost.apps.wireguard.dns

DNS server pushed to clients.

Type: string

Declared by:

selfhost.apps.wireguard.endpoint

Public endpoint host/IP that clients dial.

Type: string

Declared by:

selfhost.apps.wireguard.exporterPort

Prometheus wireguard-exporter listen port (localhost).

Type: 16 bit unsigned integer; between 0 and 65535 (both inclusive)

Default:

9586

Declared by:

selfhost.apps.wireguard.interface

WireGuard interface name.

Type: string

Default:

"wg0"

Declared by:

selfhost.apps.wireguard.lanAccess.enable

Whether to enable opt-in nftables forwarding/NAT so clients reach the LAN (else clients reach only the server).

Type: boolean

Default:

false

Example:

true

Declared by:

selfhost.apps.wireguard.lanAccess.masquerade

Whether to enable srcnat masquerade of client traffic into the LAN (enable only if the LAN lacks routes back to the client subnet).

Type: boolean

Default:

false

Example:

true

Declared by:

selfhost.apps.wireguard.lanAccess.subnet

LAN subnet full-access clients may reach; added to their AllowedIPs and used as the masquerade destination. Required when lanAccess.enable.

Type: string

Declared by:

selfhost.apps.wireguard.listenPort

WireGuard UDP listen port (opened in the firewall).

Type: 16 bit unsigned integer; between 0 and 65535 (both inclusive)

Default:

51820

Declared by:

selfhost.apps.wireguard.name

Short identity prefix for client interface/device names (e.g. ‘bphenr’).

Type: string

Declared by:

selfhost.apps.wireguard.openFirewall

Open the WireGuard listen UDP port in the firewall.

Type: boolean

Default:

true

Declared by:

selfhost.apps.wireguard.peers

Derived per-device peers { name, device, ip, fullAccess, publicKey } for consumer firewall/routing rules.

Type: list of attribute set of anything (read only)

Default: derived from users.*.services.wireguard

Declared by:

selfhost.auth.forwardAuth.active

Whether a forward-auth provider is active. Compose service defaults against this.

Type: boolean (read only)

Default: true once a provider sets url

Declared by:

selfhost.auth.forwardAuth.path

Verify path appended to url for the ingress forward-auth middleware (e.g. /api/auth/traefik); set by the active provider.

Type: string

Default:

""

Declared by:

selfhost.auth.forwardAuth.tinyauth.enable

Whether to enable tinyauth forward-auth gateway (federates to the selfhost OIDC provider).

Type: boolean

Default:

false

Example:

true

Declared by:

selfhost.auth.forwardAuth.tinyauth.port

tinyauth listen port (localhost, behind ingress).

Type: 16 bit unsigned integer; between 0 and 65535 (both inclusive)

Default:

3000

Declared by:

selfhost.auth.forwardAuth.url

Base URL of the forward-auth endpoint, set by the active provider and consumed by the ingress provider (null = no provider active).

Type: null or string

Default:

null

Declared by:

selfhost.auth.oidc.clients

Derived OIDC client configs keyed by service name (read-only)

Type: attribute set of raw value (read only)

Default:

{ }

Declared by:

selfhost.auth.oidc.credentials.dir

Base directory for OIDC credentials (persistent; see credentialsBaseDir).

Type: string (read only)

Default:

"/var/lib/homelab-oidc"

Declared by:

selfhost.auth.oidc.credentials.usersFile

JSON file mapping usernames to their OIDC provider user IDs

Type: string (read only)

Default:

"/var/lib/homelab-oidc/oidc-users.json"

Declared by:

selfhost.auth.oidc.pocket-id.enable

Whether to enable Pocket-ID OIDC implementation.

Type: boolean

Default:

false

Example:

true

Declared by:

selfhost.auth.oidc.pocket-id.port

Pocket-ID listen port (localhost, behind ingress).

Type: 16 bit unsigned integer; between 0 and 65535 (both inclusive)

Default:

8094

Declared by:

selfhost.auth.oidc.provider.apiKeyFile

Path to file containing provider API key

Type: string

Declared by:

selfhost.auth.oidc.provider.discoveryUrl

OIDC discovery document URL (derived from issuerUrl); for consumers that need the full well-known URL rather than the bare issuer.

Type: string (read only)

Default: <issuerUrl>/.well-known/openid-configuration

Declared by:

selfhost.auth.oidc.provider.displayName

Display name of the OIDC provider (shown in UI)

Type: string

Declared by:

selfhost.auth.oidc.provider.internalName

Internal name for URLs and identifiers

Type: string

Declared by:

selfhost.auth.oidc.provider.issuerUrl

OIDC issuer URL (e.g. https://auth.example.com)

Type: string

Declared by:

selfhost.auth.oidc.provisionConfig

Provisioning config derived from OIDC-enabled users and services (read-only)

Type: raw value (read only)

Default:

{
  groups = [
    {
      name = "admin";
    }
    {
      name = "guests";
    }
    {
      name = "users";
    }
  ];
  users = [ ];
}

Declared by:

selfhost.auth.oidc.rotation.enable

Whether to enable a timer that rotates all OIDC client secrets on a schedule.

Type: boolean

Default:

false

Example:

true

Declared by:

selfhost.auth.oidc.rotation.notifyTopic

selfhost.notify topic to alert on if a scheduled rotation fails (null = no alert).

Type: null or string

Default:

null

Declared by:

selfhost.auth.oidc.rotation.schedule

systemd OnCalendar expression for the rotation timer (default: weekly, Sunday 03:00).

Type: string

Default:

"Sun *-*-* 03:00:00"

Example:

"monthly"

Declared by:

selfhost.auth.oidc.systemd.baseProvisionUnit

Systemd unit for base OIDC provisioning (users/groups)

Type: null or string

Default:

null

Declared by:

selfhost.auth.oidc.systemd.clientProvisionUnitPrefix

Prefix for per-client provisioning unit names (provider sets this; e.g. ‘<provider>-provision-client-’ yields ‘<provider>-provision-client-<name>.service’).

Type: null or string

Default:

null

Declared by:

selfhost.backup.package

The rustic-manage package to use (shared by all targets).

Type: package

Declared by:

selfhost.backup.targets

Backup destinations. Each is an independent rustic pipeline with its own content, repository, retention, and schedule.

Type: attribute set of (submodule)

Default:

{ }

Declared by:

selfhost.backup.targets.<name>.backendCredentialsFile

Storage backend access credentials as a rustic secrets TOML ([repository.options]); symlinked to /etc/rustic/<name>-secrets.toml. Null for local-path repositories (no backend auth).

Type: null or string

Default:

null

Declared by:

selfhost.backup.targets.<name>.backupSchedule

systemd OnCalendar schedule for the backup timer.

Type: string

Default:

"*-*-* 03:00:00"

Declared by:

selfhost.backup.targets.<name>.bindings

Virtual backup path (key, must start with ‘/’) -> source path (value), mounted read-only into this target’s tree.

Type: attribute set of string

Default:

{ }

Example:

{
  "/nas/notes" = "/mnt/nas/notes";
}

Declared by:

selfhost.backup.targets.<name>.globs

rustic include/exclude globs (‘!’ = exclude). Default excludes common NAS/sync/OS metadata.

Type: list of string

Default:

[
  "!@eaDir"
  "!.stfolder"
  "!.stversions"
  "!.Trash*"
  "!@Recycle"
  "!$RECYCLE.BIN"
  "!.DS_Store"
  "!Thumbs.db"
]

Declared by:

selfhost.backup.targets.<name>.hooks

Standalone pre-backup hooks not tied to a registry service (e.g. GitHub), run for this target.

Type: attribute set of (submodule)

Default:

{ }

Declared by:

selfhost.backup.targets.<name>.hooks.<name>.package

Package providing a backup script. OUTPUT_DIR points to a fresh, empty directory for the hook’s output.

Type: package

Declared by:

selfhost.backup.targets.<name>.hooks.<name>.after

Systemd units this hook must run after (e.g. the service whose data it snapshots).

Type: list of string

Default:

[ ]

Declared by:

selfhost.backup.targets.<name>.passwordFile

rustic repository encryption password file.

Type: string

Declared by:

selfhost.backup.targets.<name>.repository

rustic repository string (e.g. ‘opendal:b2’).

Type: string

Declared by:

selfhost.backup.targets.<name>.retention

Snapshot retention policy (rustic forget keep-within-*).

Type: submodule

Declared by:

selfhost.backup.targets.<name>.retention.daily

How long to keep daily snapshots (rustic keep-within-daily; e.g. 7 days).

Type: string

Declared by:

selfhost.backup.targets.<name>.retention.monthly

How long to keep monthly snapshots (e.g. 1 year).

Type: string

Declared by:

selfhost.backup.targets.<name>.retention.weekly

How long to keep weekly snapshots (e.g. 1 month).

Type: string

Declared by:

selfhost.backup.targets.<name>.retention.yearly

How long to keep yearly snapshots (e.g. 2 years).

Type: string

Declared by:

selfhost.backup.targets.<name>.services

Registry services whose backup hook to run for this target (must declare services.<name>.backup.package).

Type: list of impossible (empty enum)

Default:

[ ]

Declared by:

selfhost.backup.targets.<name>.verifySchedule

systemd OnCalendar schedule for the verification timer.

Type: string

Default:

"Sun *-*-* 05:00:00"

Declared by:

selfhost.dashboards.generatedTiles

Service/external tiles keyed by integrations.homepage.group (read-only). The bundled apps.homepage renders these; otherwise read them into a dashboard you own and decide tabs/layout.

Type: attribute set of list of anything (read only)

Default:

{ }

Declared by:

selfhost.domain

Base domain for all services (e.g. ‘home.example.com’)

Type: string

Declared by:

selfhost.external

External services not managed by this host (shown on homepage dashboard via integrations.homepage)

Type: attribute set of (submodule)

Default:

{ }

Declared by:

selfhost.external.<name>.displayName

Human-readable name (defaults to attribute name)

Type: string

Default:

"‹name›"

Declared by:

selfhost.external.<name>.integrations.homepage

Homepage dashboard integration

Type: submodule

Default:

{ }

Declared by:

selfhost.external.<name>.integrations.homepage.enable

Whether to enable homepage entry for this service.

Type: boolean

Default:

false

Example:

true

Declared by:

selfhost.external.<name>.integrations.homepage.group

Free-form tile group this service belongs to; you map groups to tabs/layout in your dashboard.

Type: string

Default:

"Services"

Declared by:

selfhost.external.<name>.integrations.homepage.icon

Icon name from dashboard-icons (e.g. ‘miniflux.svg’)

Type: null or string

Default:

"‹name›.svg"

Declared by:

selfhost.external.<name>.integrations.homepage.settings

Extra homepage tile settings merged into the generated entry (e.g. a widget).

Type: attribute set

Default:

{ }

Declared by:

selfhost.external.<name>.meta.category

Canonical grouping key; a consumer surface (e.g. a landing page) groups services by this.

Type: null or string

Default:

null

Declared by:

selfhost.external.<name>.meta.description

Short description (nixpkgs meta.description).

Type: string

Declared by:

selfhost.external.<name>.meta.homepage

Upstream project homepage (nixpkgs meta.homepage).

Type: null or string

Default:

null

Declared by:

selfhost.external.<name>.name

Registry identifier (defaults to attribute name)

Type: string

Default:

"‹name›"

Declared by:

selfhost.external.<name>.url

Direct URL to the external service

Type: string

Declared by:

selfhost.groups.admin

Name of the admin group

Type: string

Default:

"admin"

Declared by:

selfhost.groups.guests

Name of the guests group

Type: string

Default:

"guests"

Declared by:

selfhost.groups.users

Name of the users group

Type: string

Default:

"users"

Declared by:

selfhost.ingress.acme.credentialsEnvFile

Path to an env file with the DNS provider’s credentials (e.g. CF_DNS_API_TOKEN). Provided by the host, e.g. via sops-nix.

Type: string

Declared by:

selfhost.ingress.acme.dnsProvider

DNS-01 challenge provider name for the ACME client (e.g. ‘cloudflare’)

Type: string

Declared by:

selfhost.ingress.acme.email

ACME account email for certificate registration

Type: string

Declared by:

selfhost.ingress.allowedInterfaces

Network interfaces to allow HTTP/HTTPS traffic on. If empty, allows on all interfaces (not recommended).

Type: list of string

Default:

[ ]

Declared by:

selfhost.ingress.traefik.enable

Whether to enable Traefik reverse-proxy ingress implementation.

Type: boolean

Default:

false

Example:

true

Declared by:

selfhost.ingress.traefik.metricsPort

Port for Traefik’s Prometheus metrics endpoint (localhost only)

Type: 16 bit unsigned integer; between 0 and 65535 (both inclusive)

Default:

8082

Declared by:

selfhost.internal.listeningPorts.*.host

Listen address.

Type: string

Default:

"127.0.0.1"

Declared by:

selfhost.internal.listeningPorts.*.name

Owner identifier, shown in collision messages.

Type: string

Declared by:

selfhost.internal.listeningPorts.*.port

Listen port.

Type: 16 bit unsigned integer; between 0 and 65535 (both inclusive)

Declared by:

selfhost.inventory

Registered services as use-case-agnostic facts (name, displayName, description, normalized access model, ingress, publicUrl, meta.homepage). Read-only; consumers decide presentation.

Type: list of (attribute set) (read only)

Default:

[ ]

Declared by:

selfhost.mail.from

Sender email address

Type: string

Declared by:

selfhost.mail.host

SMTP server hostname

Type: string

Declared by:

selfhost.mail.passwordFile

Path to file containing the SMTP password (typically a sops secret path)

Type: string

Declared by:

selfhost.mail.port

SMTP server port

Type: 16 bit unsigned integer; between 0 and 65535 (both inclusive)

Default:

587

Declared by:

selfhost.mail.tls

TLS mode for SMTP connection

Type: one of “none”, “starttls”, “tls”

Default:

"starttls"

Declared by:

selfhost.mail.user

SMTP authentication username

Type: string

Declared by:

selfhost.monitoring.enable

Whether to enable Prometheus monitoring (metrics, healthchecks, alert rules).

Type: boolean

Default:

false

Example:

true

Declared by:

selfhost.monitoring.alertmanager.enable

Whether to enable Alertmanager alert delivery (routes fired alerts to notify).

Type: boolean

Default:

false

Example:

true

Declared by:

selfhost.monitoring.alertmanager.port

Alertmanager listen port (localhost, behind ingress).

Type: 16 bit unsigned integer; between 0 and 65535 (both inclusive)

Default:

9093

Declared by:

selfhost.monitoring.blackboxPort

Blackbox-exporter listen port (localhost) for service healthchecks.

Type: 16 bit unsigned integer; between 0 and 65535 (both inclusive)

Default:

9116

Declared by:

selfhost.monitoring.prometheusPort

Prometheus listen port (localhost, behind ingress).

Type: 16 bit unsigned integer; between 0 and 65535 (both inclusive)

Default:

9090

Declared by:

selfhost.monitoring.retentionSize

Prometheus metrics retention size cap (the effective bound).

Type: string

Default:

"5GB"

Declared by:

selfhost.monitoring.retentionTime

Prometheus metrics retention time.

Type: string

Default:

"365d"

Declared by:

selfhost.monitoring.scopes

Monitoring scopes: infrastructure, hardware, and other non-service metric sources

Type: attribute set of (submodule)

Default:

{ }

Declared by:

selfhost.monitoring.scopes.<name>.enable

Whether to enable this monitoring scope.

Type: boolean

Default:

true

Example:

true

Declared by:

selfhost.monitoring.scopes.<name>.exporters

Prometheus exporter definitions merged into services.prometheus.exporters

Type: attribute set of anything

Default:

{ }

Declared by:

selfhost.monitoring.scopes.<name>.rules

Prometheus alert rule groups

Type: list of (attribute set)

Default:

[ ]

Declared by:

selfhost.monitoring.scopes.<name>.scrapeConfigs

Prometheus scrape configurations

Type: list of (attribute set)

Default:

[ ]

Declared by:

selfhost.monitoring.scopes.<name>.systemdOverrides

Systemd service overrides for monitoring-related units

Type: attribute set of anything

Default:

{ }

Declared by:

selfhost.monitoring.scrapeInterval

Global Prometheus scrape and evaluation interval.

Type: string

Default:

"60s"

Declared by:

selfhost.notify.package

send-notification implementation. Contract: send-notification --topic <t> --message <m> [--title <T>] [--priority <p>] [--tags <x>], reading NOTIFY_URL and NOTIFY_TOKEN_FILE from the env.

Type: package

Default:

pkgs.selfhost.send-notification

Declared by:

selfhost.notify.active

Whether a notify provider is active. Compose service defaults against this.

Type: boolean (read only)

Default: true once a provider sets url

Declared by:

selfhost.notify.ntfy.enable

Whether to enable ntfy notification implementation (server + provisioning).

Type: boolean

Default:

false

Example:

true

Declared by:

selfhost.notify.ntfy.port

ntfy listen port (localhost, behind ingress).

Type: 16 bit unsigned integer; between 0 and 65535 (both inclusive)

Default:

2586

Declared by:

selfhost.notify.provisioningUnit

Systemd unit of the active provider that provisions publisher tokens; consumers that read a token via LoadCredential order after it. null = no provider, or a provider with no provisioning step.

Type: null or string

Default:

null

Declared by:

selfhost.notify.topics

Notification topics and their visibility (framework subsystems self-register their own homelab-* topics).

Type: attribute set of (submodule)

Default:

{ }

Declared by:

selfhost.notify.topics.<name>.public

Whether the topic can be published without authentication

Type: boolean

Default:

false

Declared by:

selfhost.notify.url

Base URL of the notification endpoint, set by the active notify provider and consumed by send-notification (NOTIFY_URL); null = no provider active.

Type: null or string

Default:

null

Declared by:

selfhost.oidcPlaceholder

Opaque placeholder pair per OIDC client.

Type: attribute set of (submodule) (read only)

Default:

{ }

Declared by:

selfhost.oidcPlaceholder.<name>.id

This option has no description.

Type: string

Declared by:

selfhost.oidcPlaceholder.<name>.secret

This option has no description.

Type: string

Declared by:

selfhost.runtimePlaceholder

Opaque placeholder string per declared runtime secret.

Type: attribute set of string (read only)

Default:

{ }

Declared by:

selfhost.runtimeSecrets

Runtime-generated secret files (one-shot openssl rand).

Type: attribute set of (submodule)

Default:

{ }

Declared by:

selfhost.runtimeSecrets.<name>.bytes

Random bytes (hex-encoded; file is 2x chars).

Type: signed integer

Default:

32

Declared by:

selfhost.runtimeSecrets.<name>.generateOnce

Generate once, then never regenerate (supersedes regenerateIfMissing): a later loss is left absent and logged, not silently replaced — for data-bound secrets (e.g. an encryption key). To rotate deliberately, remove the secret together with the protected data.

Type: boolean

Default:

false

Declared by:

selfhost.runtimeSecrets.<name>.generateOnceGuard

Path to the data a generate-once secret protects (e.g. a service’s data dir). While it exists and is non-empty, a missing secret is left absent rather than regenerated. Required when generateOnce = true.

Type: null or string

Default:

null

Declared by:

selfhost.runtimeSecrets.<name>.group

Unix group; defaults to owner’s primary group.

Type: null or string

Default:

null

Declared by:

selfhost.runtimeSecrets.<name>.mode

File mode (octal) of the secret file.

Type: string

Default:

"0400"

Declared by:

selfhost.runtimeSecrets.<name>.owner

Unix owner of the file.

Type: string

Default:

"root"

Declared by:

selfhost.runtimeSecrets.<name>.path

This option has no description.

Type: string (read only)

Default:

"/var/lib/homelab-secrets/‹name›"

Declared by:

selfhost.runtimeSecrets.<name>.regenerateIfMissing

Generate a new random value if the file is missing. When false (externally-synced secrets), the file is left absent and logged rather than aborting secret generation; consumers fail until it is restored.

Type: boolean

Default:

true

Declared by:

selfhost.runtimeSecrets.<name>.restartUnits

Units consuming this secret; wired requires+after on the generator (ordering only; values are persistent).

Type: list of string

Default:

[ ]

Declared by:

selfhost.runtimeSecretsDir

Persistent directory containing runtime-generated secret files. Include in backups.

Type: string (read only)

Default:

"/var/lib/homelab-secrets"

Declared by:

selfhost.runtimeTemplates

Templates rendered from runtime secrets and OIDC credentials.

Type: attribute set of (submodule)

Default:

{ }

Declared by:

selfhost.runtimeTemplates.<name>.content

Template body; reference secrets via runtimePlaceholder.<name> and OIDC creds via oidcPlaceholder.<client>.{id,secret}.

Type: strings concatenated with “\n”

Declared by:

selfhost.runtimeTemplates.<name>.group

Unix group; defaults to owner’s primary group.

Type: null or string

Default:

null

Declared by:

selfhost.runtimeTemplates.<name>.mode

File mode (octal) of the rendered file.

Type: string

Default:

"0400"

Declared by:

selfhost.runtimeTemplates.<name>.owner

Unix owner of the rendered file.

Type: string

Default:

"root"

Declared by:

selfhost.runtimeTemplates.<name>.path

Rendered output path (tmpfs; regenerated each boot).

Type: string

Default:

"/run/homelab-secrets/templates/‹name›"

Declared by:

selfhost.runtimeTemplates.<name>.restartUnits

Units restarted when the template body changes between deploys.

Type: list of string

Default:

[ ]

Declared by:

selfhost.services

Registry of selfhost services: routing, metadata, and integrations (HTTP ingress optional via ingress.enable).

Type: attribute set of (submodule)

Default:

{ }

Declared by:

selfhost.services.<name>.access.allowedGroups

Groups authorized to access this service (canonical groups or any a user is in). Empty means unrestricted (any authenticated user).

Type: list of (one of “admin”, “guests”, “users”)

Default:

[ ]

Declared by:

selfhost.services.<name>.backup.package

Package providing backup script. Use writeShellApplication with runtimeInputs for dependencies. OUTPUT_DIR is provided as an environment variable pointing to a fresh, empty directory for the hook’s output.

Type: null or package

Default:

null

Declared by:

selfhost.services.<name>.backup.after

Systemd services this backup hook requires and orders after.

Type: list of string

Default:

[ ]

Declared by:

selfhost.services.<name>.displayName

Human-readable name (defaults to attribute name)

Type: string

Default:

"‹name›"

Declared by:

selfhost.services.<name>.extraConfig

Consumer-owned per-service data with no first-class option (e.g. a landing-page tag); selfhost-nix never reads it. Co-located on the service entry instead of a parallel tree keyed by name. Read back at config.selfhost.services.<name>.extraConfig.

Type: open submodule of attribute set of anything

Default:

{ }

Declared by:

selfhost.services.<name>.forwardAuth.enable

Whether to enable ingress-level access control via the forward-auth gateway.

Type: boolean

Default:

false

Example:

true

Declared by:

selfhost.services.<name>.healthcheck.path

Path for health checks (used by monitoring and homepage)

Type: string

Default:

"/"

Declared by:

selfhost.services.<name>.healthcheck.probeModule

Blackbox exporter module for health probes. Use http_any for services that require authentication on all endpoints.

Type: one of “http_2xx”, “http_any”

Default:

"http_2xx"

Declared by:

selfhost.services.<name>.healthcheck.url

Full health check URL (derived from url and healthcheck path)

Type: string (read only)

Default: <url><healthcheck.path>

Declared by:

selfhost.services.<name>.host

Hostname or IP where the service listens (local or remote)

Type: string

Default:

"127.0.0.1"

Declared by:

selfhost.services.<name>.ingress.enable

Whether to enable HTTP ingress route for this service.

Type: boolean

Default:

true

Example:

true

Declared by:

selfhost.services.<name>.integrations.homepage

Homepage dashboard integration

Type: submodule

Default:

{ }

Declared by:

selfhost.services.<name>.integrations.homepage.enable

Whether to enable homepage entry for this service.

Type: boolean

Default:

false

Example:

true

Declared by:

selfhost.services.<name>.integrations.homepage.group

Free-form tile group this service belongs to; you map groups to tabs/layout in your dashboard.

Type: string

Default:

"Services"

Declared by:

selfhost.services.<name>.integrations.homepage.icon

Icon name from dashboard-icons (e.g. ‘miniflux.svg’)

Type: null or string

Default:

"‹name›.svg"

Declared by:

selfhost.services.<name>.integrations.homepage.settings

Extra homepage tile settings merged into the generated entry (e.g. a widget).

Type: attribute set

Default:

{ }

Declared by:

selfhost.services.<name>.integrations.monitoring.enable

Whether to enable monitoring for this service.

Type: boolean

Default: follows selfhost.monitoring.enable

Example:

true

Declared by:

selfhost.services.<name>.integrations.monitoring.exporters

Custom Prometheus exporters for this service

Type: attribute set of anything

Default:

{ }

Declared by:

selfhost.services.<name>.integrations.monitoring.healthcheck

Whether to auto-generate a blackbox healthcheck probe for this service

Type: boolean

Default:

true

Declared by:

selfhost.services.<name>.integrations.monitoring.rules

Custom Prometheus alert rule groups

Type: list of (attribute set)

Default:

[ ]

Declared by:

selfhost.services.<name>.integrations.monitoring.scrapeConfigs

Custom Prometheus scrape configurations

Type: list of (attribute set)

Default:

[ ]

Declared by:

selfhost.services.<name>.integrations.monitoring.systemdOverrides

Systemd service overrides for monitoring-related units

Type: attribute set of anything

Default:

{ }

Declared by:

selfhost.services.<name>.integrations.notify

notification integration

Type: submodule

Default:

{ }

Declared by:

selfhost.services.<name>.integrations.notify.enable

Publish notifications for this service/task.

Type: boolean

Default: on when a notify provider is active and topic is set

Declared by:

selfhost.services.<name>.integrations.notify.tokenFile

Path to this publisher’s access token, provisioned root-owned 0400. How a publisher reads it depends on its user:

  • runs as root (e.g. backup, task failure-hooks): read this path directly, at send time. Best-effort — no dependency on the provider being up, so notify never blocks the publisher.
  • runs as a non-root user (e.g. transmission): receive it via systemd LoadCredential = [ "notify-token:${...tokenFile}" ] and read %d/notify-token ($CREDENTIALS_DIRECTORY/notify-token). Because LoadCredential reads the source at unit start, order the unit after the provider’s provisioning unit (selfhost.notify.provisioningUnit).

Type: string (read only)

Default:

"/var/lib/homelab-secrets/notify-publishers/‹name›"

Declared by:

selfhost.services.<name>.integrations.notify.topic

Notification topic this service/task publishes to (null = none).

Type: null or impossible (empty enum)

Default:

null

Declared by:

selfhost.services.<name>.meta.category

Canonical grouping key; a consumer surface (e.g. a landing page) groups services by this.

Type: null or string

Default:

null

Declared by:

selfhost.services.<name>.meta.description

Short description (nixpkgs meta.description).

Type: string

Declared by:

selfhost.services.<name>.meta.homepage

Upstream project homepage (nixpkgs meta.homepage).

Type: null or string

Default:

null

Declared by:

selfhost.services.<name>.name

Registry identifier (defaults to attribute name)

Type: string

Default:

"‹name›"

Declared by:

selfhost.services.<name>.oidc

OIDC client configuration for this service

Type: submodule

Default:

{ }

Declared by:

selfhost.services.<name>.oidc.enable

Whether to enable OIDC client for this service.

Type: boolean

Default:

false

Example:

true

Declared by:

selfhost.services.<name>.oidc.callbackURLs

Callback URLs for the OIDC client

Type: list of string

Default: [ <publicUrl>/oauth2/oidc/callback ]

Declared by:

selfhost.services.<name>.oidc.gid

Fixed GID for the credentials group (null = auto-assign)

Type: null or signed integer

Default:

null

Declared by:

selfhost.services.<name>.oidc.group

Group name for this client’s credentials

Type: string (read only)

Default:

"homelab-oidc-‹name›"

Declared by:

selfhost.services.<name>.oidc.id.file

Path to the file containing the client ID

Type: string (read only)

Default:

"/var/lib/homelab-oidc/‹name›/id"

Declared by:

selfhost.services.<name>.oidc.id.placeholder

Placeholder for client ID (use in config files, substituted at runtime)

Type: string (read only)

Default:

"@HOMELAB_OIDC_‹name›_ID@"

Declared by:

selfhost.services.<name>.oidc.name

Display name of the OIDC client in the provider

Type: string

Default:

"‹name›"

Declared by:

selfhost.services.<name>.oidc.pkce

Enable PKCE for this client

Type: boolean

Default:

false

Declared by:

selfhost.services.<name>.oidc.secret.file

Path to the file containing the client secret

Type: string (read only)

Default:

"/var/lib/homelab-oidc/‹name›/secret"

Declared by:

selfhost.services.<name>.oidc.secret.placeholder

Placeholder for client secret (use in config files, substituted at runtime)

Type: string (read only)

Default:

"@HOMELAB_OIDC_‹name›_SECRET@"

Declared by:

selfhost.services.<name>.oidc.systemd.dependentServices

Systemd services needing this client’s credentials; auto-wired requires/after/partOf.

Type: list of string

Default:

[ ]

Declared by:

selfhost.services.<name>.oidc.systemd.loadCredentials

Ready-to-use LoadCredential entries for systemd services

Type: list of string (read only)

Default:

[
  "oidc-id:/var/lib/homelab-oidc/‹name›/id"
  "oidc-secret:/var/lib/homelab-oidc/‹name›/secret"
]

Declared by:

selfhost.services.<name>.oidc.systemd.supplementaryGroups

Groups to add for direct credential file access

Type: list of string (read only)

Default:

[
  "homelab-oidc-‹name›"
]

Declared by:

selfhost.services.<name>.port

Port the service listens on

Type: 16 bit unsigned integer; between 0 and 65535 (both inclusive)

Declared by:

selfhost.services.<name>.publicHost

Public hostname (derived from subdomain and domain)

Type: string

Default: <subdomain>.<domain>

Declared by:

selfhost.services.<name>.publicUrl

Full public URL (derived from publicHost)

Type: string

Default: https://<publicHost>

Declared by:

selfhost.services.<name>.scheme

URL scheme for backend connection

Type: one of “http”, “https”

Default:

"http"

Declared by:

selfhost.services.<name>.storage.smb

Named selfhost SMB mounts this service requires.

Type: list of impossible (empty enum)

Default:

[ ]

Declared by:

selfhost.services.<name>.storage.systemdServices

Units needing the declared mounts; if empty, auto-resolved from the service or OCI-container name.

Type: list of string

Default:

[ ]

Declared by:

selfhost.services.<name>.subdomain

Subdomain prefix (combined with domain for publicHost)

Type: string

Default:

"‹name›"

Declared by:

selfhost.services.<name>.traefik.middlewares

Extra Traefik middleware definitions to attach to this service’s router

Type: attribute set of attribute set of unspecified value

Default:

{ }

Declared by:

selfhost.services.<name>.url

Full URL for proxying (derived from scheme, host and port)

Type: string

Default: <scheme>://<host>:<port>

Declared by:

selfhost.storage.smb.enable

Whether to enable Home-server storage.

Type: boolean

Default:

false

Example:

true

Declared by:

selfhost.storage.smb.credentialsPath

Path to the SMB credentials file (must be provided by the host, e.g. via sops-nix)

Type: string

Declared by:

selfhost.storage.smb.hostname

IP or hostname of the SMB server; prefer an IP or /etc/hosts entry for reliable resolution at boot.

Type: string

Declared by:

selfhost.storage.smb.mounts

CIFS shares keyed by remote root folder, each behind a dedicated access group. Mount mode is chosen per share: one with dependents boot-mounts with nofail (services retry), while an independent share uses lazy x-systemd.automount on first access — dodging the boot-time network race that ordering a service after the mount (RequiresMountsFor) would otherwise hit.

Type: attribute set of (submodule)

Default:

{ }

Example:

{
  bphenriques = { };
  media = { };
}

Declared by:

selfhost.storage.smb.mounts.<name>.gid

GID for the mount group (required for SMB mount options)

Type: signed integer

Declared by:

selfhost.storage.smb.mounts.<name>.group

Name of the group with access to the mount

Type: string

Default:

"homelab-‹name›"

Declared by:

selfhost.storage.smb.mounts.<name>.localMount

Local mount point for the share

Type: string

Default:

"/mnt/homelab-‹name›"

Declared by:

selfhost.storage.smb.mounts.<name>.systemd.dependentServices

Extra units needing this mount, for non-registry/dynamic cases (registry services should use storage.smb).

Type: list of string

Default:

[ ]

Declared by:

selfhost.storage.smb.mounts.<name>.uid

File-owner UID on the client (default 0/root → access via group; set per-user for owner-level ops like chmod/git).

Type: signed integer

Default:

0

Declared by:

selfhost.tasks

Registry of externally-defined systemd units that opt into selfhost concerns (notify, storage).

Type: attribute set of (submodule)

Default:

{ }

Declared by:

selfhost.tasks.<name>.integrations.notify

notification integration

Type: submodule

Default:

{ }

Declared by:

selfhost.tasks.<name>.integrations.notify.enable

Publish notifications for this service/task.

Type: boolean

Default: on when a notify provider is active and topic is set

Declared by:

selfhost.tasks.<name>.integrations.notify.tokenFile

Path to this publisher’s access token, provisioned root-owned 0400. How a publisher reads it depends on its user:

  • runs as root (e.g. backup, task failure-hooks): read this path directly, at send time. Best-effort — no dependency on the provider being up, so notify never blocks the publisher.
  • runs as a non-root user (e.g. transmission): receive it via systemd LoadCredential = [ "notify-token:${...tokenFile}" ] and read %d/notify-token ($CREDENTIALS_DIRECTORY/notify-token). Because LoadCredential reads the source at unit start, order the unit after the provider’s provisioning unit (selfhost.notify.provisioningUnit).

Type: string (read only)

Default:

"/var/lib/homelab-secrets/notify-publishers/‹name›"

Declared by:

selfhost.tasks.<name>.integrations.notify.topic

Notification topic this service/task publishes to (null = none).

Type: null or impossible (empty enum)

Default:

null

Declared by:

selfhost.tasks.<name>.name

Task identifier (defaults to the attribute name).

Type: string

Default:

"‹name›"

Declared by:

selfhost.tasks.<name>.storage.smb

Named selfhost SMB mounts this task requires.

Type: list of impossible (empty enum)

Default:

[ ]

Declared by:

selfhost.tasks.<name>.systemdServices

Systemd units this task owns; selfhost concerns attach to them.

Type: (list of string) or string convertible to it

Default:

[ ]

Declared by:

selfhost.users

This option has no description.

Type: attribute set of (submodule)

Default:

{ }

Declared by:

selfhost.users.<name>.auth.oidc.enable

Whether to enable OIDC account for this user.

Type: boolean

Default:

true

Example:

true

Declared by:

selfhost.users.<name>.email

This option has no description.

Type: string

Declared by:

selfhost.users.<name>.extraConfig

Consumer-owned per-user data with no first-class option; selfhost-nix never reads it. Per-service config mirrors the registry at users.<name>.services.<name>, not here. Read back at config.selfhost.users.<name>.extraConfig.

Type: open submodule of attribute set of anything

Default:

{ }

Declared by:

selfhost.users.<name>.firstName

This option has no description.

Type: string

Declared by:

selfhost.users.<name>.groups

Groups assigned to this user. If admin group is included, the user is marked as admin.

Type: list of string

Default:

[ ]

Declared by:

selfhost.users.<name>.isAdmin

This option has no description.

Type: boolean (read only)

Default: true if the user’s groups include the admin group

Declared by:

selfhost.users.<name>.lastName

This option has no description.

Type: string

Declared by:

selfhost.users.<name>.name

This option has no description.

Type: string

Default: <firstName> <lastName>

Declared by:

selfhost.users.<name>.services.filebrowser.enable

Whether to enable a FileBrowser entry for this user (access is gated by the service auth, not this flag).

Type: boolean

Default:

false

Example:

true

Declared by:

selfhost.users.<name>.services.filebrowser.admin

Whether to enable FileBrowser admin.

Type: boolean

Default:

false

Example:

true

Declared by:

selfhost.users.<name>.services.filebrowser.storage

selfhost SMB mounts this user may access, keyed by permission; unioned into their scope (read-write iff any is rw).

Type: attribute set of (one of “ro”, “rw”)

Default:

{ }

Declared by:

selfhost.users.<name>.services.gitea.enable

Whether to enable a Gitea account for this user.

Type: boolean

Default:

false

Example:

true

Declared by:

selfhost.users.<name>.services.gitea.admin

Gitea site-admin (reconciled each run); defaults to the user’s fleet isAdmin.

Type: boolean

Default:

false

Declared by:

selfhost.users.<name>.services.miniflux.settings

Per-user Miniflux preferences, applied verbatim via the user-update API (is_admin is framework-managed and ignored here).

Type: attribute set of anything

Default:

{ }

Example:

{
  display_mode = "fullscreen";
  theme = "dark_serif";
}

Declared by:

selfhost.users.<name>.services.radicale.enable

Whether to enable Radicale CalDAV/CardDAV access for this user.

Type: boolean

Default:

false

Example:

true

Declared by:

selfhost.users.<name>.services.wireguard.enable

Whether to enable WireGuard configuration for this user.

Type: boolean

Default:

false

Example:

true

Declared by:

selfhost.users.<name>.services.wireguard.devices

WireGuard devices for this user.

Type: list of (submodule)

Default:

[ ]

Declared by:

selfhost.users.<name>.services.wireguard.devices.*.fullAccess

If true, device can reach the whole LAN; if false, only the home server.

Type: boolean

Default:

false

Declared by:

selfhost.users.<name>.services.wireguard.devices.*.ip

Static WireGuard client IP (e.g. 10.100.0.42).

Type: string

Declared by:

selfhost.users.<name>.services.wireguard.devices.*.name

Device name (e.g. phone, laptop). Lowercase alphanumeric and dashes only.

Type: string matching the pattern [a-z0-9][a-z0-9-]*

Declared by:

selfhost.users.<name>.services.wireguard.devices.*.publicKey

Device’s WireGuard public key, from wg-manage add (the private key stays on the server).

Type: string

Declared by:

selfhost.users.<name>.username

This option has no description.

Type: string

Default:

"‹name›"

Declared by: