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/443to 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, if you route anything publicly, selfhost.ingress.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.mounts.smb.shares are on-demand CIFS shares behind per-share access groups. Boot does not wait for the
SMB server. First access may wait up to 30 seconds; after a failed mount, a later access retries. A service
requests storage.mounts = [ … ] to start its automount guards before the service can touch their paths. The
service still owns its failure and restart policy. Converting a share that a previous generation
boot-mounted takes one reboot: systemd cannot install autofs over an already-mounted path. 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 — mirroringselfhost.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>, mirroringselfhost.<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.
-
Mint on the server (the WireGuard host). The client name is
<username>-<device>. Pass the short--deviceso 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 QRThe private key lands in
/var/lib/wireguard/clients/(0600, root). The public key and IP are printed. -
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 = truereaches the whole LAN.falsereaches 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
oidcblock, setforwardAuth.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, apps.bazarr) 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 withconfigureAfter.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.
apps.bazarr fetches subtitles for what Radarr/Sonarr already track. The framework seeds the API key and
the localhost bind before Bazarr starts (it rewrites its own config.yaml, so that file can never be a
store symlink), then reconciles the sonarr/radarr links and languageProfiles through Bazarr’s
settings API. Which providers to search, and with whose credentials, is acquisition — it arrives via
the freeform settings seam, with credentials in secretSettings (read from a file at reconcile time,
never through the store). No provider is named anywhere in the framework.
languageProfiles defaults to empty, and Bazarr with no profile downloads nothing — so wanting subtitles
at all is an explicit choice you make, not one the framework makes for you.
Failure visibility
A media stack fails quietly: a dead root folder or an unreachable client stops imports, and because
nothing is imported, nothing is ever removed from the download client. The apps therefore wire two signals
by default — the *arr ntfy connection carries onHealthIssue / onManualInteractionRequired, and
exportarr exposes *_system_health_issues and *_queue_total with alert rules on both. Neither is
optional, because the failure mode they cover is invisible until you notice missing media.
Note that the generated API keys are 32 characters rather than the framework’s usual 64: exportarr rejects
anything outside ^[a-zA-Z0-9]{20,32}$.
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.
Immich
apps.immich runs Immich and reconciles the accounts plus the external libraries that belong to them.
Where photos live stays with the consumer: mediaLocation, transcoding, job concurrency and the
machine-learning subsystem are left at their nixpkgs defaults for you to set on services.immich.
Two consumer patterns
Immich stores uploads under its own media location, and can additionally scan directories you already have. The difference is one option:
selfhost.users.alice.services.immich.enable = true; # upload-only
selfhost.users.bob.services.immich = {
enable = true;
libraries = [ { name = "bob-photos"; importPaths = [ "/mnt/photos/bob" ]; } ];
};
libraries defaults to empty, so an account is upload-only unless you say otherwise, and a deployment
that never uses external libraries declares no paths anywhere. Declared paths become writable to the
service, since Immich writes sidecars and thumbnails beside the originals it imports.
A library’s name is its reconcile identity. Renaming one creates a second library rather than renaming
the first.
Accounts
services.immich.admin defaults to the user’s isAdmin, so fleet-level admin carries into Immich
without restating it. quotaBytes and storageLabel are reconciled on every run, so changing a quota
moves the existing account.
passwordFile is a bootstrap credential: applied when the account is created and never reconciled, so a
password changed in the app survives. An account given one is asked to change it on first login.
Accounts without a passwordFile get a random password, which is the right default when sign-in is
through OIDC. Without an OIDC provider the service’s oidc.enable composes to false, and each account
needs a passwordFile to be reachable.
OIDC only authenticates. autoRegister is off, so an OIDC login lands on an account that
selfhost.users already declared, matched by email, and a login with no declared account is refused.
That keeps one source of truth for who exists.
The admin@immich.local account is separate from all of these. The reconciler authenticates as it to
drive the API, and nothing else uses it.
Nothing is deleted
The reconcile creates and updates, never removes. Dropping a user or a library from your config leaves the Immich side untouched, because deleting either would take photos with it. Accounts created outside the framework are likewise left alone. Removals are a deliberate manual step in the app.
Backups
Uploaded photos live under Immich’s media location, not in your external libraries, so nothing else covers them. They are far too large to copy through a backup hook, so bind them into a target instead, which mounts them read-only with no second copy:
selfhost.backup.targets.<name>.bindings = {
"/immich/library" = "${config.services.immich.mediaLocation}/library";
"/immich/upload" = "${config.services.immich.mediaLocation}/upload";
"/immich/profile" = "${config.services.immich.mediaLocation}/profile";
};
Skip thumbs/ and encoded-video/, which Immich regenerates. If every account is backed by external
libraries, those originals already live in directories you back up separately and there is nothing here
to add.
The app registers no backup hook for the database. Immich writes its own nightly dump to
<mediaLocation>/backups, so binding that path picks it up if you want the albums, people and
favourites that a re-scan cannot rebuild.
Keeping up with Immich
Immich moves API routes across major releases. api-contract.json beside the module declares the request
payloads configure.nu sends, and the vm-immich test asserts them against the server’s own
/api/spec.json. Immich validates with zod, which strips unknown keys rather than rejecting them, so a
renamed field would otherwise stop being applied with nothing to show for it. Bumping nixpkgs and running
the test is what tells you the reconciler still holds.
Jellyfin
apps.jellyfin completes the startup wizard unattended, then reconciles the server name, libraries and
accounts. Transcoding, storage and everything else stays yours to set on services.jellyfin.
selfhost.apps.jellyfin = {
enable = true;
libraries = [
{ name = "Movies"; collectionType = "movies"; locations = [ "/mnt/media/movies" ]; }
];
};
selfhost.users.alice.services.jellyfin.enable = true;
Jellyfin refuses to start with under 2 GiB free in its data directory, and aborts with a core dump rather
than a readable error. Worth knowing before putting dataDir on a small partition.
Everything is merged, never replaced
branding, encoding, trickplay and a user’s policy are all merged onto whatever Jellyfin currently
holds, then written back only if something changed. That matters most for branding: LoginDisclaimer is
where an SSO plugin puts its login button, and replacing the whole object would wipe it on every run.
The same applies to library locations, which are reconciled rather than set once. Moving a directory in
your config moves the library.
A library’s name is its reconcile identity, so renaming one creates a second library.
Encoding takes exactly one writer
Two mechanisms can own encoding.xml, and mixing them is the trap.
apps.jellyfin.encoding merges onto what Jellyfin currently holds, so fields you do not name keep their
value. services.jellyfin.hardwareAcceleration and services.jellyfin.transcoding instead write the
file from a template, and on a server that already has one they do nothing until you also set
forceEncodingConfig. That template carries 18 elements while a populated encoding.xml runs to over
40, so everything outside it resets to Jellyfin’s default, including TonemappingAlgorithm,
PreferSystemNativeHwDecoder and EnableDecodingColorDepth10Vp9, none of which nixpkgs models.
Set both and the file permanently differs from the template, so every restart leaves another
encoding.xml.backup-<timestamp> behind. Prefer the nixpkgs options on a fresh install where their
defaults are acceptable, and this one on a server whose settings are worth keeping.
Jellyfin ignores properties it does not recognise, so a misspelled field is dropped without complaint.
EnableHwEncoding and EnableHwDecoding are the trap worth naming: both are real TrickplayOptions
fields, neither exists on EncodingOptions, where the equivalents are EnableHardwareEncoding and
HardwareDecodingCodecs.
Forward-auth is off
Jellyfin authenticates its own clients, and native apps cannot pass a forward-auth gateway, so
forwardAuth.enable defaults to false here rather than following the gateway. Turn it on only if every
client you use is a browser.
Accounts
passwordFile is a bootstrap credential: applied when the account is created and never reconciled, so a
password changed in the app survives. Accounts without one get a random password. policy is a freeform
passthrough merged into Jellyfin’s UserPolicy, so anything you do not name keeps its current value.
The admin account is separate. The reconciler authenticates as it to drive the API.
Nothing is deleted
The reconcile creates and updates only. Dropping a library or user from your config leaves the Jellyfin side untouched, since removing either is destructive. Removals are a deliberate manual step.
Keeping up with Jellyfin
api-contract.json beside the module declares the request payloads configure.nu sends, and the
vm-jellyfin test asserts them against the server’s own /api-docs/openapi.json. Jellyfin deserialises
with System.Text.Json, which ignores unknown keys rather than rejecting them, so a renamed field would
otherwise stop being applied with nothing to show for it.
POST /Library/VirtualFolders takes its name, collection type and paths as query parameters, so library
creation is the one call the contract cannot cover. It fails loudly with a 4xx instead.
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.
runtimeSecretsare generated withopensslinto/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.
regenerateIfMissinghandles 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) orforwardAuth. The forwardAuth middleware sets the identity headers from the auth response, and an assertion refusesforwardAuthwith no active provider. - Hardened service units. Bundled reconcilers and backups run with
ProtectSystem = strict,NoNewPrivileges, etc. The notify token reaches non-root consumers via systemdLoadCredential. - 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
generateOncekey 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
| Secret | How it rotates |
|---|---|
| OIDC client secrets | Framework-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
generateOncekey (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.
- Rebuild the host and restore the secrets backend, putting the sops/age master key back first.
- Restore generate-once keys from your out-of-band copy into
runtimeSecretsDirbefore their services start, so they decrypt existing data instead of the guard leaving them absent. nixos-rebuild switch. Disposable secrets regenerate, and OIDC users and clients re-provision.- 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.bazarr.enable
Whether to enable the first-party Bazarr app (subtitle automation; wiring + reconcile, no providers).
Type: boolean
Default:
false
Example:
true
Declared by:
selfhost.apps.bazarr.apiKeyFile
Path to Bazarr’s generated API key.
Type: string (read only)
Default: the generated API-key secret path
Declared by:
selfhost.apps.bazarr.configureAfter
Extra units the reconcile must start after and want — Bazarr connection-tests the *arrs it is given.
Type: list of string
Default:
[ ]
Example:
[
"sonarr.service"
]
Declared by:
selfhost.apps.bazarr.dataDir
Bazarr state directory (config.yaml, database, logs).
Type: string
Default:
"/var/lib/bazarr"
Declared by:
selfhost.apps.bazarr.defaultProfile
Profile name applied to newly-added series and movies. Null leaves new media unassigned (nothing is fetched for it).
Type: null or string
Default:
null
Declared by:
selfhost.apps.bazarr.exporterPort
exportarr listen port (localhost) for Bazarr metrics.
Type: 16 bit unsigned integer; between 0 and 65535 (both inclusive)
Default:
9712
Declared by:
selfhost.apps.bazarr.languageProfiles
Language profiles to ensure. Which languages to want is taste, so the framework ships none — with no profile Bazarr downloads nothing. The posted list is authoritative: a profile removed here is removed from Bazarr.
Type: list of (submodule)
Default:
[ ]
Example:
[ { name = "English"; languages = [ "en" ]; cutoff = "en"; } ]
Declared by:
selfhost.apps.bazarr.languageProfiles.*.cutoff
Language that ends the search once found; null keeps searching for all of them.
Type: null or string
Default:
null
Declared by:
selfhost.apps.bazarr.languageProfiles.*.languages
Two-letter language codes to search for, in priority order.
Type: list of string
Default:
[ ]
Declared by:
selfhost.apps.bazarr.languageProfiles.*.name
Profile name.
Type: string
Declared by:
selfhost.apps.bazarr.port
Bazarr listen port (localhost, behind ingress).
Type: 16 bit unsigned integer; between 0 and 65535 (both inclusive)
Default:
6767
Declared by:
selfhost.apps.bazarr.radarr
Radarr to pull the movie library from. Null = Bazarr handles no movies.
Type: null or (submodule)
Default:
null
Declared by:
selfhost.apps.bazarr.radarr.apiKeyFile
Path to Radarr’s API key — typically apps.radarr.apiKeyFile.
Type: string
Declared by:
selfhost.apps.bazarr.radarr.baseUrl
Radarr URL base.
Type: string
Default:
"/"
Declared by:
selfhost.apps.bazarr.radarr.host
Radarr host reachable from Bazarr.
Type: string
Default:
"127.0.0.1"
Declared by:
selfhost.apps.bazarr.radarr.port
Radarr port.
Type: 16 bit unsigned integer; between 0 and 65535 (both inclusive)
Default:
7878
Declared by:
selfhost.apps.bazarr.secretSettings
Settings whose values are read from a file at reconcile time, as "<section>.<key>" = "/path/to/secret". For provider credentials.
Type: attribute set of string
Default:
{ }
Example:
{ "someprovider.password" = "/run/secrets/someprovider-password"; }
Declared by:
selfhost.apps.bazarr.settings
Freeform config.yaml sections merged into the reconcile, as <section>.<key>. This is the seam
for acquisition config: enabled providers and their per-provider options. Values that are secrets
belong in secretSettings, not here — this lands in the world-readable Nix store.
Type: attribute set of attribute set of anything
Default:
{ }
Example:
{ general.enabled_providers = [ "someprovider" ]; }
Declared by:
selfhost.apps.bazarr.sonarr
Sonarr to pull the series library from. Null = Bazarr handles no TV.
Type: null or (submodule)
Default:
null
Declared by:
selfhost.apps.bazarr.sonarr.apiKeyFile
Path to Sonarr’s API key — typically apps.sonarr.apiKeyFile.
Type: string
Declared by:
selfhost.apps.bazarr.sonarr.baseUrl
Sonarr URL base.
Type: string
Default:
"/"
Declared by:
selfhost.apps.bazarr.sonarr.host
Sonarr host reachable from Bazarr.
Type: string
Default:
"127.0.0.1"
Declared by:
selfhost.apps.bazarr.sonarr.port
Sonarr port.
Type: 16 bit unsigned integer; between 0 and 65535 (both inclusive)
Default:
8989
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.immich.enable
Whether to enable the first-party Immich app (photo and video library).
Type: boolean
Default:
false
Example:
true
Declared by:
selfhost.apps.jellyfin.enable
Whether to enable the first-party Jellyfin app (media server).
Type: boolean
Default:
false
Example:
true
Declared by:
selfhost.apps.jellyfin.branding
Fields merged into Jellyfin’s branding configuration. Merged, not replaced, so a login disclaimer owned by an SSO plugin survives.
Type: attribute set of anything
Default:
{ }
Declared by:
selfhost.apps.jellyfin.defaultPolicy
Policy fields applied to every account, under each user’s own policy. The baseline most deployments want the same for everyone.
Type: attribute set of anything
Default:
{ }
Example:
{
EnableSubtitleManagement = true;
}
Declared by:
selfhost.apps.jellyfin.encoding
EncodingOptions fields, merged onto what Jellyfin holds and applied through the API after startup, so anything not named here keeps its value.
services.jellyfin.hardwareAcceleration and services.jellyfin.transcoding cover part of the same
ground by writing encoding.xml directly. Use one or the other, never both: together the file
always differs from the generated template, and each restart leaves another
encoding.xml.backup-<timestamp> behind.
Unknown keys are dropped silently, since Jellyfin ignores properties it does not recognise.
Type: attribute set of anything
Default:
{ }
Example:
{
TonemappingAlgorithm = "bt2390";
}
Declared by:
selfhost.apps.jellyfin.libraries
Libraries to create and keep pointed at their directories.
Type: list of (submodule)
Default:
[ ]
Declared by:
selfhost.apps.jellyfin.libraries.*.collectionType
Jellyfin collection type. Closed set, so a typo fails evaluation rather than the library create.
Type: one of “unknown”, “movies”, “tvshows”, “music”, “musicvideos”, “trailers”, “homevideos”, “boxsets”, “books”, “photos”, “livetv”, “playlists”, “folders”
Example:
"movies"
Declared by:
selfhost.apps.jellyfin.libraries.*.locations
Directories this library scans. Reconciled, so moving a path moves the library.
Type: list of string
Default:
[ ]
Declared by:
selfhost.apps.jellyfin.libraries.*.name
Library name in Jellyfin; also its reconcile identity, so renaming creates a second one.
Type: string
Declared by:
selfhost.apps.jellyfin.libraries.*.options
Fields merged into this library’s Jellyfin LibraryOptions, applied verbatim.
Type: attribute set of anything
Default:
{ }
Example:
{
EnableRealtimeMonitor = false;
}
Declared by:
selfhost.apps.jellyfin.serverName
Server name Jellyfin reports to clients.
Type: string
Default:
the service’s displayName
Declared by:
selfhost.apps.jellyfin.startup.metadataCountryCode
Metadata country code, applied once by the startup wizard.
Type: string
Default:
"US"
Declared by:
selfhost.apps.jellyfin.startup.preferredMetadataLanguage
Preferred metadata language, applied once by the startup wizard.
Type: string
Default:
"en"
Declared by:
selfhost.apps.jellyfin.startup.uiCulture
Interface culture, applied once by the startup wizard.
Type: string
Default:
"en-US"
Declared by:
selfhost.apps.jellyfin.trickplay
Fields merged into Jellyfin’s trickplay options.
Type: attribute set of anything
Default:
{ }
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.exporterPort
exportarr listen port (localhost) for Prowlarr metrics. Each *arr needs its own — exportarr defaults them all to 9708.
Type: 16 bit unsigned integer; between 0 and 65535 (both inclusive)
Default:
9711
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.exporterPort
exportarr listen port (localhost) for Radarr metrics. Each *arr needs its own — exportarr defaults them all to 9708.
Type: 16 bit unsigned integer; between 0 and 65535 (both inclusive)
Default:
9710
Declared by:
selfhost.apps.radarr.notifyOnImport
Publish a notification when an item is imported or upgraded. Turn this off when something downstream already tells people content arrived (a request manager, for example) — otherwise one arrival produces two messages describing the same thing. The failure events (health checks, manual interaction required) are always published and are not affected by this.
Type: boolean
Default:
true
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.exporterPort
exportarr listen port (localhost) for Sonarr metrics. Each *arr needs its own — exportarr defaults them all to 9708.
Type: 16 bit unsigned integer; between 0 and 65535 (both inclusive)
Default:
9709
Declared by:
selfhost.apps.sonarr.notifyOnImport
Publish a notification when an item is imported or upgraded. Turn this off when something downstream already tells people content arrived (a request manager, for example) — otherwise one arrival produces two messages describing the same thing. The failure events (health checks, manual interaction required) are always published and are not affected by this.
Type: boolean
Default:
true
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.active
Whether an OIDC provider is active, set by the active provider. Compose service defaults against this.
Type: boolean
Default:
false
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"
"!#snapshot"
"!#recycle"
"!.zfs"
]
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.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.domain
Base domain for publicly routed services (e.g. ‘home.example.com’). Required once any service enables ingress; a host that routes nothing may leave it null.
Type: null or string
Default:
null
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 read without authentication (grants everyone ro). Publishing always needs a per-service token, so this decides who can see the messages, not who can send them.
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.
The unit that creates this path must be listed in restartUnits, which is what orders it after the
generator. Without that ordering it can populate the path first, and the guard then suppresses the
very first generation, permanently and silently.
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 unitafterthe 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 ingress.domain)
Type: string
Default:
<subdomain>.<ingress.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.mounts
Named selfhost SMB shares this service may access; their automount guards start before the service.
Type: list of impossible (empty enum)
Default:
[ ]
Declared by:
selfhost.services.<name>.storage.systemdServices
Systemd service names to guard instead of the inferred 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.mounts.smb.enable
Whether to enable Home-server storage.
Type: boolean
Default:
false
Example:
true
Declared by:
selfhost.storage.mounts.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.mounts.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.mounts.smb.shares
CIFS shares keyed by remote root folder, each behind a dedicated access group and mounted on demand. Boot does not wait for the SMB server. First access may wait up to 30 seconds; after a failed mount, a later access retries.
Type: attribute set of (submodule)
Default:
{ }
Example:
{
bphenriques = { };
media = { };
}
Declared by:
selfhost.storage.mounts.smb.shares.<name>.gid
GID for the mount group (required for SMB mount options)
Type: signed integer
Declared by:
selfhost.storage.mounts.smb.shares.<name>.group
Name of the group with access to the mount
Type: string
Default:
"homelab-‹name›"
Declared by:
selfhost.storage.mounts.smb.shares.<name>.localMount
Local mount point for the share
Type: string
Default:
"/mnt/homelab-‹name›"
Declared by:
selfhost.storage.mounts.smb.shares.<name>.systemd.dependentServices
Extra systemd service names to order after this share’s automount guard.
Type: list of string
Default:
[ ]
Declared by:
selfhost.storage.mounts.smb.shares.<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 unitafterthe 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.mounts
Named selfhost SMB shares this task may access; their automount guards start before the task.
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:
- modules/nixos/services/wireguard/user.nix
- modules/nixos/services/radicale/user.nix
- modules/nixos/services/miniflux/user.nix
- modules/nixos/services/jellyfin/user.nix
- modules/nixos/services/immich/user.nix
- modules/nixos/services/gitea/user.nix
- modules/nixos/services/filebrowser/selfhost.nix
- modules/nixos/users.nix
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.immich.enable
Whether to enable an Immich account for this user.
Type: boolean
Default:
false
Example:
true
Declared by:
selfhost.users.<name>.services.immich.admin
Grant this account Immich administrator rights.
Type: boolean
Default:
the user’s isAdmin
Declared by:
selfhost.users.<name>.services.immich.libraries
External libraries owned by this user: existing directories Immich scans in place. Empty leaves the account upload-only, which is the common case.
Type: list of (submodule)
Default:
[ ]
Declared by:
selfhost.users.<name>.services.immich.libraries.*.exclusionPatterns
Glob patterns skipped during the scan.
Type: list of string
Default:
[ ]
Declared by:
selfhost.users.<name>.services.immich.libraries.*.importPaths
Directories Immich scans for this library. The app grants the service write access to them.
Type: list of string
Default:
[ ]
Declared by:
selfhost.users.<name>.services.immich.libraries.*.name
Library name as shown in Immich; also its reconcile identity, so renaming creates a new one.
Type: string
Declared by:
selfhost.users.<name>.services.immich.passwordFile
Path to a file holding the initial password, applied at account creation and never reconciled. Null generates a random one, for accounts that sign in through OIDC.
Type: null or string
Default:
null
Declared by:
selfhost.users.<name>.services.immich.quotaBytes
Upload quota for this account in bytes. Null is unlimited. Reconciled, so changing it moves the existing account’s quota.
Type: null or (unsigned integer, meaning >=0)
Default:
null
Example:
107374182400
Declared by:
selfhost.users.<name>.services.immich.storageLabel
Folder name for this account under a {{label}} storage template. Null falls back to the account’s UUID. Reconciled.
Type: null or string
Default:
null
Declared by:
selfhost.users.<name>.services.jellyfin.enable
Whether to enable a Jellyfin account for this user.
Type: boolean
Default:
false
Example:
true
Declared by:
selfhost.users.<name>.services.jellyfin.passwordFile
Path to a file holding the initial password, applied at account creation and never reconciled. Null generates a random one.
Type: null or string
Default:
null
Declared by:
selfhost.users.<name>.services.jellyfin.policy
Fields merged into this account’s Jellyfin UserPolicy on every run, applied verbatim. Anything not named here keeps whatever Jellyfin holds.
Type: attribute set of anything
Default:
{ }
Example:
{
EnableSubtitleManagement = true;
IsHidden = 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: