Optional services

Import the example with the default module, then enable only the applications you intend to operate:

{
  imports = [ ./optional-services.nix ];
  homelab.optional.apps = {
    komga.enable = true;
    shelfmark.enable = true;
    unpackerr.enable = true;
  };
}

The example enables nothing by itself. Native services.<name> options remain available for package overrides, application settings and secret-file paths. User-provided files under /run/nix-seal/system/secrets must exist before the corresponding application starts. This project creates no provider credentials. Keep secret values out of Nix expressions, shell arguments and the store.

Optional media readers and acquisition applications wait for the existing media mount checks. Additional output paths must stay under homelab.storage.rootDir. Komga and Kavita receive a read-only library; Unpackerr can write downloads but cannot alter the library. Shelfmark copies completed downloads into its books output, preserving original seed data. Pinchflat can write its videos output. Cross-seed needs a writable media mount to create hardlinks; it therefore has broader media write access than a reader. Keep its API credential private.

Services receive lower CPU and IO weights and Nice 10. These are scheduling preferences, not guaranteed memory or throughput limits. Unpackerr extracts one archive at a time, Shelfmark's example permits one simultaneous download, Pinchflat limits each yt-dlp queue to one worker, and Paperless runs one OCR worker with one thread. FlareSolverr has a 1 GiB memory pressure threshold and 2 GiB hard limit. Adjust native systemd settings against observed workloads.

Karakeep

Enable the native stack with:

homelab.optional.apps.karakeep.enable = true;

The module uses Nixpkgs' source-built Karakeep, Meilisearch and Chromium packages. It creates separate web, worker, browser, search and secret-setup services instead of running the three upstream container images. Karakeep, Meilisearch and the Chrome DevTools endpoint listen only on IPv4 loopback. The module also authenticates Meilisearch and blocks unrelated local users from the DevTools port with a UID-based nftables rule.

The first start creates the Meilisearch master key and NextAuth secret under /var/lib/karakeep with mode 0400. Subsequent starts retain them. Put provider credentials in a runtime file outside the Nix store:

homelab.optional.karakeep = {
  environmentFile = "/run/nix-seal/system/secrets/karakeep.env";
  extraEnvironment = {
    DISABLE_SIGNUPS = "true";
  };
};

Do not put MEILI_MASTER_KEY, NEXTAUTH_SECRET, listener addresses or data paths in extraEnvironment; the module rejects those names. Runtime launchers restore module-owned authentication values after loading environmentFile. The defaults cap the web process at 2 GiB, workers at 4 GiB, Chromium and Meilisearch at 2 GiB each. Chromium keeps its renderer sandbox and runs under a dedicated account. Override the systemd limits only after measuring the actual bookmark and indexing workload.

Quality policy

homelab.optional.quality = {
  enable = true;
  sonarrApiKeyFile = "/run/nix-seal/system/secrets/sonarr-api-key";
  radarrApiKeyFile = "/run/nix-seal/system/secrets/radarr-api-key";
};

Recyclarr creates Homelab 1080p, allowing WEB-DL and WEBRip 1080p, with upgrades disabled. Those two qualities use minimum/preferred/maximum sizes of 5/40/80 MB per minute. This is an explicit starting policy for limited storage, not a measured universal quality recommendation. Size definitions apply to the whole manager instance, including other profiles. Other quality definitions come from Recyclarr's guide resources. The profile does not delete custom formats or reset unmatched scores. It runs weekly on Sunday at 04:10.

Select the new profile in Seerr's manager destinations and for existing managed titles deliberately. Existing titles are not reassigned. Override the generated native services.recyclarr.configuration.<app>.homelab-<app> settings to choose upgrades, 4K or custom formats. Review changes with Recyclarr's preview before applying them. Do not configure another reconciler to own these same profile fields. Recyclarr's guide data is pinned separately from its package. The module writes settings.yml after native secret substitution and replaces both default resource providers with Nix-fetched local copies of immutable commits: TRaSH Guides a9486f6465d4483993dec638131272b397a4338e and config templates 9faf65ff745d74ab906fd73cadaa25f08eb9d981. Nix verifies the archive hashes when building the system. Sync does not fetch guide data over the network or select another revision when a source is unavailable. Settings are module-owned and rewritten before each run.

To update guide data, review upstream changes, update the commit URLs and verified archive hashes in quality-resources.nix, run configuration checks and preview the sync against a disposable manager before applying it. Review package compatibility separately and preserve Recyclarr state with backups. Resource provider settings. Profile behavior, size definitions.

For a separate 4K profile, import quality-4k.nix. It retains Homelab 1080p and adds Homelab 4K WEB, using WEB-DL/WEBRip 2160p. Minimum/preferred/maximum sizes are 10/80/160 MB per minute. A two-hour release can reach about 19.2 GB. Upgrades and score resets remain disabled, and existing titles are not reassigned. Select this profile for chosen titles or a separate Seerr destination. Size definitions affect every 2160p profile in that manager. Check client playback support and storage capacity before selecting it.

The generated 1080p and optional 4K profiles also score the pinned guide's LQ, LQ (Release Title) and BR-DISK custom formats at -10000, with minimum format score 0. This excludes matching releases under these profiles; it does not delete existing media. The guide identifies release-group/title patterns, not measured playback quality. Review those patterns when upgrading the fixed resources. Existing custom formats and undeclared profile scores remain owned by the operator (delete_old_custom_formats = false and reset_unmatched_scores.enabled = false). Manually added positive scores can offset a negative score, so review combined scores when customizing profiles.

Acquisition and indexer tools

Application Supplied configuration Deployment input and workflow check
autobrr Loopback listener, runtime session secret, lower background priority Create an administrator, add the actual announcement network/indexers and downloader credentials, then a filter with explicit categories, size limit and daily download limit. Use a permitted test announcement to verify its action and duplicate rejection.
cross-seed Loopback API, generated package defaults, strict matching, hardlinks, client injection with rechecking, 60-second search delay, daily search and hourly RSS Supply runtime JSON containing API key, torrent client connection and actual per-indexer Torznab URLs. Confirm an injected fixture shares an inode with its original and leaves the original untouched.
Unpackerr One extraction, three retries, one-minute start and five-minute retry delays, shared-group output permissions, runtime Arr credentials and torrent-only polling in the example Enable only the manager entries you use. Verify archive extraction, Arr import and cleanup with a disposable fixture. Original archives are retained.
FlareSolverr Loopback endpoint, closed firewall, bounded browser memory/tasks Assign only indexers that require it; verify browser egress follows the intended VPN route. No authentication is supplied by this endpoint.

Use the autobrr API recipe to reconcile named downloader connections and bounded filters after supplying provider account inputs.

For cross-seed the secret JSON has this shape. Replace every placeholder inside the user-owned secret catalog, never in a tracked Nix file:

{
  "apiKey": "<random cross-seed API key>",
  "torrentClients": [
    "qbittorrent:http://<user>:<url-encoded-password>@<host-accessible-qbittorrent-address>:8081"
  ],
  "torznab": ["http://127.0.0.1:9696/<indexer-id>/api?apikey=<prowlarr-key>"]
}

Use the host-accessible downloader endpoint configured by vpn-confinement; port 8081 is the module default, not an assertion about your namespace forwarding. A Prowlarr base API URL without the indexer ID is not a Torznab endpoint. Keep both original downloads and cross-seed links on the same filesystem. Search rates must also respect each provider's rules. Cross-seed options, Unpackerr preserves permissions stored in an archive. Its file_mode applies only when the archive does not provide a mode. An archive containing owner-only files can therefore require an attended permission repair before an Arr manager can import them. The extraction fixture declares group-readable archive modes; it does not establish automatic normalization of arbitrary archive permissions.

Unpackerr configuration.

The module does not silently route these tools through a VPN. Their outbound requests and local API connections differ. Configure the required namespace or proxy using the VPN contract, then test success and tunnel failure. Do not use Shelfmark's container startup scripts to create a second VPN policy.

Books and channel archives

Choose Komga or Kavita according to your formats and clients. Add a library root at the configured media library's books directory, create a restricted reader account, and verify reading/download access with that account. Disable library modification rights for readers. Kavita requires a runtime token-key file of at least 512 bits. Back up its state directory as well as the books. The module does not invent a supported API for creating either reader's users or libraries.

Shelfmark's example configures local authentication, Prowlarr discovery, qBittorrent's books category and a books output folder. Supply PROWLARR_API_KEY, QBITTORRENT_USERNAME and QBITTORRENT_PASSWORD in shelfmark.env. Finish its local administrator onboarding, select qBittorrent as the torrent client and choose the permitted indexers. Verify a book reaches the reader and that the source torrent remains seedable. The module keeps torrents and copies Usenet results instead of accepting Shelfmark 1.3.15's job-removing move default; it also keeps certificate validation enabled and does not broaden an empty Prowlarr category search automatically. Override those choices only after an explicit retention and trust decision. Shelfmark settings persist in its own database; environment configuration and application UI behavior must be checked after upgrades. Shelfmark 1.3.15 variables.

Pinchflat requires SECRET_KEY_BASE with at least 64 bytes. Include BASIC_AUTH_USERNAME and BASIC_AUTH_PASSWORD in its runtime environment file. Its native package binds all interfaces, so keep the firewall closed and use a private authenticated entry point. Add a media profile with an explicit maximum resolution and download format, then a source with a bounded date range and polling schedule. Keep automatic retention disabled until its deletion behavior has been tested against disposable files. Its media directory defaults to <library>/videos; state and temporary processing stay on the system disk. Pinchflat configuration.

Komga uses private port 25600, leaving port 8080 for SABnzbd. Its library is read-only inside the service; imports belong to the acquisition workflow.

Photos and private documents

Immich and Paperless use their native private state and media directories, separate from the shared media group. They are not added to the normal media profile. Keep their databases on SSD; if moving originals to another mounted filesystem, define the mount and preparation policy in the consuming host. Native application tmpfiles must not create fallback directories on the system disk before an intended mount exists. Verify missing-mount behavior before moving valuable data.

Immich runs its native PostgreSQL/Redis integration and a loopback server. The example retains password login and machine learning, while limiting video encoding to two threads. Configure the first administrator through the private endpoint, create ordinary accounts for mobile uploads, select storage and thumbnail policy, and test a photo upload, download and album permissions. Backup both originals and PostgreSQL, then restore into an isolated instance. A successful database dump alone does not prove photo recovery.

Paperless's runtime environment must include the deployment's PAPERLESS_SECRET_KEY; its separate password file supplies the administrator password. OCR defaults to English, skips existing text and uses one worker and one thread. Configure the document language explicitly if different. Add an ordinary user with limited document permissions, configure ingestion rules and verify that consuming a sample PDF produces searchable text without making it visible to another user. Preserve both its database and document media. Immich requirements, Paperless configuration.

Host services

Syncthing starts with private administration, no discovery or relay service and no shared folders in the example. Its filesystem sandbox preserves native folder creation under the service account; it does not use a read-only root or hide host home directories. Mount dependencies follow declared folders and state. Declare actual mounts in the host and verify missing-mount failure before sharing. Declare device IDs, explicit peer addresses, folder paths and versioning in the host. Open synchronization ports only on the intended interface. Verify two-device synchronization and deletion recovery. Versioning does not replace independent backups.

AdGuard Home starts its web UI on loopback and DNS on loopback port 5353, avoiding an accidental replacement of the host resolver. Configure upstream resolvers, bootstrap addresses, filters and administrative access before directing clients to it. A host deployment must deliberately choose port 53 and the household interface. Do not make an intermittently running desktop the only DNS server.

Scrutiny keeps its web service on loopback port 8083 and bundled InfluxDB on loopback port 8086, avoiding SABnzbd's port 8080. Its collector is disabled until the consuming host explicitly enables it. At this Nixpkgs pin, enabling the native collector runs it as root and also enables smartd with a 600-second polling argument. Review the host's existing SMART configuration before making that choice; enabling the web UI does not select hardware or change SMART policy.

A host that has verified its device paths can configure:

services.scrutiny.collector = {
  enable = true;
  schedule = "daily";
  settings = {
    # Replace with actual paths reported by smartctl --scan.
    allow_listed_devices = [ "/dev/sdX" ];
    # Add only if this disk's USB bridge requires and supports this type.
    devices = [ { device = "/dev/sdX"; type = [ "sat" ]; } ];
  };
};

The allow-list limits which detected devices Scrutiny collects; a devices entry alone is an override, not an allow-list. It does not configure smartd's separate device policy. Omit USB type overrides for devices that do not require them. Confirm the real disk appears and inspect collection failures. For one drive, smartd alone may meet the monitoring need with fewer processes. Collector configuration at the package pin.

Maintainerr

homelab.optional.apps.maintainerr.enable runs the source-built Maintainerr 3.28.0 package from nixpkgs-personal. The build uses Node.js 26, the upstream Yarn lockfile and a fixed offline dependency cache. It compiles native addons against Nixpkgs libraries instead of unpacking the upstream OCI image.

The native systemd service runs under a dedicated account with no capabilities, no new privileges, a read-only system, private devices and temporary files, and restricted kernel and namespace access. It retains the previous limits of 1 GiB memory, one CPU and 256 tasks. No media directory is available to the process. Application state lives under /var/lib/homelab-maintainerr/data; preserve this directory and its ownership in recovery planning. The dedicated UID defaults to 62460; set homelab.optional.maintainerr.uid to an unused value before first activation if that UID is already allocated.

The example requires a nix-seal maintainerr-htpasswd file. Nginx loads it using systemd credentials and serves authenticated access on 127.0.0.1:6246. The native loopback backend on port 6247 is restricted by nftables to nginx, root and the dedicated service account. Other local service users cannot bypass authentication. The proxy strips browser Basic credentials before forwarding and disables buffering for Maintainerr's live event streams. Add TLS in the consuming host before exposing this endpoint beyond loopback, and restart nginx after rotating the runtime htpasswd file.

Configure the actual media-server and Arr/Seerr connections after authenticating through nginx. Existing host.containers.internal URLs keep working through a loopback compatibility alias; new settings can use 127.0.0.1 and the native service port. A service in a VPN namespace still needs its configured host-visible endpoint. Start with reviewed collections and generous grace periods; verify rules against disposable media before authorizing removal. Absence of a media bind prevents filesystem cleanup but does not prevent deletion through supplied Arr or media-server APIs. This module configures no deletion rules and disables telemetry. Systemd waits for /api/health/ready, including its SQLite check, before startup succeeds. Starting the service needs no registry or package network access. Updating the release requires reviewing the source revision, source hash, Yarn dependency hashes, build requirements and migrations. The native package research records the 3.28.0 build and service decisions. Upstream documents the installation and health checks and access limitations.

Jellyfin hardware selection

Hardware selection belongs in the consuming host. For a verified VA-API device:

services.jellyfin = {
  hardwareAcceleration = {
    enable = true;
    type = "vaapi";
    device = "/dev/dri/renderD128";
  };
  forceEncodingConfig = true;
  transcoding = {
    enableHardwareEncoding = true;
    hardwareDecodingCodecs = { h264 = true; hevc = true; };
    threadCount = 2;
    throttleTranscoding = true;
  };
};

Verify the render device, driver and codec support first. The native module configures access to that device. At the inspected pin, maxConcurrentStreams and deleteSegments are declared options but are not written by its encoding XML generator. Do not rely on those options to enforce limits. Configure and verify the required application behavior before claiming a stream or cache cap. forceEncodingConfig overwrites UI encoding changes on restart. Test direct play, a real hardware transcode, unsupported-codec fallback, subtitle handling and HDR behavior. Keep transcode cache on bounded SSD storage, not an unbounded RAM filesystem. Record CPU/GPU use and cache growth before claiming an improvement. Jellyfin acceleration.

Verification scope

The optional evaluation contracts check opt-in behavior, closed firewall ports, media mount dependencies, output path rejection, read-only reader mounts, runtime quality-key requirements and selected worker limits. They do not prove runtime behavior. Separate native checks exercise extraction, cross-seeding, quality reconciliation, book access and photo/document recovery with disposable data. Real provider integration, hardware acceleration and recovery of the deployed host remain deployment checks.