docs: condense comments across the repo
Comments had drifted into multi-paragraph narrative (git commit lineage, debugging stories, restated code) in several hot spots (scripts/deploy, hermes-agent.nix, flake.nix, gitea.nix, headscale.nix). Trim every comment to its load-bearing "why" — gotchas, safety warnings, and non-obvious rationale survive verbatim in substance, just tightened to 1-2 sentences; historical narrative and anything already covered in CLAUDE.md is cut. No code/logic changed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UJqEmY1y3AYX3JoX4Y6b21
This commit is contained in:
@@ -1,11 +1,9 @@
|
||||
{ ... }:
|
||||
|
||||
# Audiobookshelf audiobook/podcast server.
|
||||
# Listens on all interfaces: :8000 stays closed on the LAN (no openFirewall),
|
||||
# but reachable over the trusted tailscale0 interface and via localhost (caddy).
|
||||
# Library/media paths are set in the web UI — point them at /mnt/data/...
|
||||
# Runs as user `audiobookshelf`; added to `users` so it can read group-owned
|
||||
# library dirs on the RAID.
|
||||
# Audiobookshelf audiobook/podcast server, listening on all interfaces but
|
||||
# reachable only via tailscale0 or local caddy (no openFirewall) — library
|
||||
# paths are set in the web UI, pointed at /mnt/data/... In the "users" group
|
||||
# so it can read the RAID's group-owned library dirs.
|
||||
{
|
||||
services.audiobookshelf = {
|
||||
enable = true;
|
||||
|
||||
+39
-64
@@ -1,30 +1,22 @@
|
||||
{ config, pkgs, inputs, ... }:
|
||||
|
||||
# Immich photo/video library. Native nixpkgs module (not the upstream compose
|
||||
# stack) — it owns its own postgres (with the pgvector + vectorchord extensions
|
||||
# it needs for search) and a unix-socket redis, so nothing else is required here.
|
||||
# Immich photo/video library. Native nixpkgs module, not the upstream compose
|
||||
# stack — it owns its own postgres (pgvector + vectorchord) and a unix-socket redis.
|
||||
#
|
||||
# Storage: everything lives under /mnt/data/AppData/immich, which is the media
|
||||
# store MIGRATED from the old ZimaOS/CasaOS install's UPLOAD_LOCATION
|
||||
# (/mnt/data/Immich/upload — same layout: library/ upload/ thumbs/
|
||||
# encoded-video/ profile/ backups/). See scripts/immich-import-legacy-db for the
|
||||
# matching database import. The postgres cluster itself stays on the OS disk.
|
||||
# Storage lives under /mnt/data/AppData/immich, migrated from the old ZimaOS/CasaOS
|
||||
# UPLOAD_LOCATION (same subfolder layout); see scripts/immich-import-legacy-db for
|
||||
# the matching DB import. The postgres cluster itself stays on the OS disk.
|
||||
#
|
||||
# ⚠️ The immich DB is the only copy of albums/faces/dates — the files alone
|
||||
# can't rebuild it. It joins the other unbacked databases on this network.
|
||||
let
|
||||
# The PACKAGE comes from nixpkgs-unstable (3.0.3); the MODULE comes from the
|
||||
# 26.05 pin (which ships 2.7.5). That combination is safe because the two
|
||||
# module files are byte-identical — verified by diffing them at the revisions
|
||||
# in flake.lock. RE-CHECK THAT DIFF on any input bump:
|
||||
# Package pinned to nixpkgs-unstable (3.0.3) while the module stays on the 26.05
|
||||
# pin (2.7.5) — safe only because the two module files are byte-identical
|
||||
# (verified by diff; re-check on any input bump). Needed because immich's
|
||||
# migrations are forward-only and jupiter's imported DB was last written by
|
||||
# 3.0.0, which 2.7.5 refuses to start against; drop once the pin ships >= 3.0.0.
|
||||
# diff <(nixpkgs)/nixos/modules/services/web-apps/immich.nix \
|
||||
# <(unstable)/nixos/modules/services/web-apps/immich.nix
|
||||
#
|
||||
# Why: jupiter's imported database was last written by immich 3.0.0, and
|
||||
# immich runs its migrations forward only — 2.7.5 refuses to start against it
|
||||
# with "corrupted migrations: previously executed migration
|
||||
# 1776217577402-DropAuditTable is missing". Drop this override once nixos-26.11
|
||||
# (or whatever the pin becomes) ships >= 3.0.0.
|
||||
unstable = import inputs.nixpkgs-unstable {
|
||||
inherit (pkgs.stdenv.hostPlatform) system;
|
||||
};
|
||||
@@ -42,27 +34,20 @@ in
|
||||
mediaLocation = "/mnt/data/AppData/immich";
|
||||
machine-learning.enable = true;
|
||||
|
||||
# ⚠️ Setting `settings` at all switches immich to IMMICH_CONFIG_FILE, and
|
||||
# that is ALL-OR-NOTHING (dist/utils/config.js: the config is
|
||||
# `configFile ? loadFromFile(...) : metadataRepo.get(SystemConfig)` — the
|
||||
# database copy is IGNORED, not merged). Two consequences:
|
||||
# 1. Anything not declared here falls back to immich's DEFAULTS, not to
|
||||
# whatever the admin UI had. The old settings stay in the
|
||||
# system_metadata table, so deleting this block restores them.
|
||||
# 2. The admin settings UI goes read-only — saving throws "Cannot update
|
||||
# configuration while IMMICH_CONFIG_FILE is in use". Change settings
|
||||
# HERE and redeploy.
|
||||
# An unknown/misspelled key is a HARD startup failure under a config file
|
||||
# (the same code path only logs a warning without one), so keys below are
|
||||
# taken verbatim from `defaults` in immich's dist/config.js.
|
||||
# ⚠️ Setting `settings` at all switches immich to IMMICH_CONFIG_FILE mode,
|
||||
# which is all-or-nothing: undeclared keys fall back to immich's defaults, not
|
||||
# the admin UI's saved values (which stay in system_metadata and return if
|
||||
# this block is deleted), and the admin settings UI goes read-only. An
|
||||
# unknown/misspelled key is a hard startup failure here (just a warning
|
||||
# without a config file), so keys are copied verbatim from `defaults` in
|
||||
# immich's dist/config.js.
|
||||
settings = {
|
||||
server.externalDomain = "https://immich.mgaction.town";
|
||||
newVersionCheck.enabled = false; # nixpkgs pins the version, not immich
|
||||
|
||||
# OIDC via Authentik on neptun. The Authentik application/provider is
|
||||
# created BY HAND in its UI — same as headscale's and headplane's, which
|
||||
# are also separate apps (hosts/neptun/secrets.nix). Only the client
|
||||
# secret is managed here.
|
||||
# OIDC via Authentik on neptun; the application/provider is created by hand
|
||||
# in its UI (like headscale's and headplane's, separate apps) — only the
|
||||
# client secret is managed here (hosts/neptun/secrets.nix).
|
||||
oauth = {
|
||||
enabled = true;
|
||||
# Authentik's per-application issuer. Trailing slash matters: immich
|
||||
@@ -76,24 +61,18 @@ in
|
||||
clientSecret._secret = config.sops.secrets.immich_oauth_client_secret.path;
|
||||
scope = "openid email profile";
|
||||
buttonText = "Login with Authentik";
|
||||
# Existing accounts (the 2 imported users) keep working: matching is by
|
||||
# email, so an Authentik user with the same address adopts that account
|
||||
# rather than creating a second one.
|
||||
# Matches by email, so the 2 imported users adopt their Authentik account
|
||||
# instead of getting a duplicate.
|
||||
autoRegister = true;
|
||||
# Leave the password form reachable — autoLaunch would bounce straight
|
||||
# to Authentik, locking everyone out if the OIDC app is misconfigured.
|
||||
autoLaunch = false;
|
||||
# Land back on immich's own login page after logout. Without this,
|
||||
# immich falls back to the IdP's discovered end_session_endpoint
|
||||
# (auth.service.js:320-326) and logout dumps you on Authentik's
|
||||
# "you've been logged out" page instead. Must be an ABSOLUTE url —
|
||||
# the config schema rejects a relative path — and mirrors immich's
|
||||
# internal LOGIN_URL, including autoLaunch=0.
|
||||
#
|
||||
# Note this ends the IMMICH session only; the Authentik SSO session
|
||||
# survives, so the next "Login with Authentik" click signs straight
|
||||
# back in without a credential prompt. To end both, drop this line and
|
||||
# let the IdP endpoint take over again.
|
||||
# Without this, immich falls back to the IdP's discovered
|
||||
# end_session_endpoint and logout dumps you on Authentik's own page
|
||||
# instead of back here — must be an absolute url, mirroring immich's
|
||||
# internal LOGIN_URL. This ends the immich session only; the Authentik
|
||||
# SSO session survives, so the next login skips the credential prompt —
|
||||
# drop this line to end both.
|
||||
endSessionEndpoint = "https://auth.mgaction.town/application/o/immich/end-session?post_logout_redirect_url=https://immich.mgaction.town";
|
||||
# The mobile app can't follow a browser redirect back to a custom
|
||||
# scheme through Authentik, so immich bounces it via this endpoint.
|
||||
@@ -101,28 +80,24 @@ in
|
||||
mobileRedirectUri = "https://immich.mgaction.town/api/oauth/mobile-redirect";
|
||||
};
|
||||
};
|
||||
# Hardware transcoding would need the iGPU passed in explicitly, e.g.
|
||||
# accelerationDevices = [ "/dev/dri/renderD128" ]; the default [ ] means
|
||||
# PrivateDevices=yes and CPU-only transcode. The ZimaBlade's Celeron does
|
||||
# this slowly but it only runs on upload.
|
||||
# Hardware transcoding needs accelerationDevices set explicitly (e.g.
|
||||
# "/dev/dri/renderD128"); default CPU-only transcode is slow on the
|
||||
# ZimaBlade's Celeron but only runs on upload.
|
||||
};
|
||||
|
||||
# /mnt/data/AppData is drwx--x--- darman:users — immich needs group "users"
|
||||
# just to TRAVERSE into its own media dir. The dir itself stays 0700
|
||||
# immich:immich (the module's tmpfiles rule re-asserts that every rebuild,
|
||||
# and UMask=0077 keeps new files private), so this grants nothing else.
|
||||
# /mnt/data/AppData is drwx--x--- darman:users; immich only needs group "users"
|
||||
# to traverse into it — the dir itself stays 0700 immich:immich (tmpfiles +
|
||||
# UMask=0077 reassert that), so this grants nothing else.
|
||||
users.users.immich.extraGroups = [ "users" ];
|
||||
|
||||
# mediaLocation is outside /var/lib, so the module won't create it — its own
|
||||
# tmpfiles entry only ADJUSTS an existing dir. Harmless no-op after the
|
||||
# legacy import, which puts the real store here.
|
||||
# mediaLocation is outside /var/lib, so the module won't create it — this rule
|
||||
# only adjusts perms on the dir the legacy import already created.
|
||||
systemd.tmpfiles.rules = [
|
||||
"d /mnt/data/AppData/immich 0700 immich immich -"
|
||||
];
|
||||
|
||||
# The unit's automatic RequiresMountsFor covers /run/immich and /var/lib/immich
|
||||
# only — nothing points it at mediaLocation. Without this immich starts with
|
||||
# the array missing and writes uploaded photos onto the 29G eMMC, into a
|
||||
# directory that becomes invisible the moment /mnt/data mounts over it.
|
||||
# The unit's automatic RequiresMountsFor doesn't cover mediaLocation — without
|
||||
# this, immich starts before /mnt/data mounts and writes uploads onto the 29G
|
||||
# eMMC, invisibly, under the future mountpoint.
|
||||
systemd.services.immich-server.unitConfig.RequiresMountsFor = [ "/mnt/data" ];
|
||||
}
|
||||
|
||||
+10
-13
@@ -6,20 +6,17 @@
|
||||
dataDir = "/mnt/data/AppData/jellyfin";
|
||||
cacheDir = "/mnt/data/AppData/jellyfin/cache";
|
||||
};
|
||||
# "users" so the shared library stays readable (see the UMask note below);
|
||||
# "video"/"render" for the DRI nodes used by hardware transcoding. renderD128
|
||||
# happens to be 0666 so VAAPI alone would work without this, but card1 is
|
||||
# 0660 root:video — and neither mode is guaranteed, so don't rely on it. The
|
||||
# groups are harmless on a host with no GPU: they exist regardless, and this
|
||||
# module stays host-agnostic (the DRIVER is enabled per-host, e.g. jupiter's
|
||||
# hardware.graphics + intel-media-driver).
|
||||
# "users" keeps the shared library readable (see the UMask note below);
|
||||
# "video"/"render" cover the DRI nodes for hardware transcoding — card1 is
|
||||
# 0660 root:video (not guaranteed 0666 like renderD128), so don't rely on
|
||||
# device perms alone. Harmless on a GPU-less host: the driver itself is
|
||||
# enabled per-host (e.g. jupiter's hardware.graphics + intel-media-driver).
|
||||
users.users.jellyfin.extraGroups = [ "users" "video" "render" ];
|
||||
|
||||
# The upstream module hardcodes UMask=0077 — root cause of jellyfin writing
|
||||
# trickplay thumbnails into stray new show folders it invented itself,
|
||||
# owned jellyfin:jellyfin 700, invisible to every other service sharing
|
||||
# the library (cinephage, mediamanager, ...). New files/dirs it creates
|
||||
# from here on inherit group "users" (library roots are setgid, see the
|
||||
# one-time chmod g+s done by hand) and stay group-writable.
|
||||
# The upstream module hardcodes UMask=0077, which made jellyfin write
|
||||
# trickplay thumbnails into new folders owned jellyfin:jellyfin 700 —
|
||||
# invisible to every other service sharing the library (cinephage,
|
||||
# mediamanager). Forcing 0002 makes new files inherit group "users"
|
||||
# (library roots are setgid via a one-time chmod g+s) and stay group-writable.
|
||||
systemd.services.jellyfin.serviceConfig.UMask = lib.mkForce "0002";
|
||||
}
|
||||
|
||||
@@ -15,21 +15,16 @@
|
||||
{
|
||||
services.prowlarr.enable = true;
|
||||
|
||||
# `nofail` is NOT optional here: without it this bind is RequiredBy
|
||||
# local-fs.target, so an unassembled RAID array fails that target and drops
|
||||
# jupiter into emergency mode — which is a dead end, since root is locked and
|
||||
# sulogin has nothing to offer on a headless box. It defeats the `nofail` on
|
||||
# /mnt/data itself (a mount layered on the array is what actually took the
|
||||
# target down). Let this bind fail alone instead.
|
||||
# `nofail` is not optional: without it this bind is RequiredBy local-fs.target,
|
||||
# so an unassembled array drops jupiter into emergency mode — a dead end on a
|
||||
# headless box with root locked. Let this bind fail alone instead.
|
||||
fileSystems."/var/lib/private/prowlarr" = {
|
||||
device = "/mnt/data/AppData/prowlarr/config";
|
||||
fsType = "none";
|
||||
options = [ "bind" "nofail" ];
|
||||
};
|
||||
|
||||
# systemd derives RequiresMountsFor from the unit's own paths, which here is
|
||||
# only /var/lib/prowlarr on the eMMC — so without this prowlarr starts happily
|
||||
# with the array absent and writes its state onto the 29G OS disk. Pin it to
|
||||
# the array so it fails loudly instead.
|
||||
# systemd derives RequiresMountsFor only from /var/lib/prowlarr (eMMC) — pin
|
||||
# it to the array too, or prowlarr starts happily and writes state to the OS disk.
|
||||
systemd.services.prowlarr.unitConfig.RequiresMountsFor = [ "/mnt/data" ];
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
{ ... }:
|
||||
|
||||
# Radarr — movie library manager, feeds off SABnzbd/Prowlarr. dataDir points
|
||||
# at the config migrated from the old ZimaOS docker stack (indexers/download
|
||||
# client/history already set up). Unlike prowlarr, this module uses a static
|
||||
# `radarr` user (no DynamicUser) and only auto-chowns dataDir when it's the
|
||||
# module's own default path — since we point at a pre-existing migrated dir,
|
||||
# chown it by hand once after first deploy:
|
||||
# Radarr — movie library manager, feeds off SABnzbd/Prowlarr; dataDir points
|
||||
# at config migrated from the old ZimaOS docker stack. Unlike prowlarr, this
|
||||
# module uses a static `radarr` user (no DynamicUser) and only auto-chowns
|
||||
# dataDir at its own default path, so the migrated dir needs a manual
|
||||
# one-time chown after first deploy:
|
||||
# chown -R radarr:radarr /mnt/data/AppData/radarr/config
|
||||
{
|
||||
services.radarr = {
|
||||
|
||||
+13
-20
@@ -1,18 +1,13 @@
|
||||
{ config, ... }:
|
||||
|
||||
# SABnzbd — usenet downloader. Migrated off a reused hand-authored ini
|
||||
# (servers/API key/history originally imported from the old ZimaOS docker
|
||||
# stack) onto NixOS-managed `settings`, per the module's own deprecation
|
||||
# notice for `configFile`. Only the values that differ from SABnzbd's own
|
||||
# built-in defaults are declared here — everything else falls back to the
|
||||
# same defaults SABnzbd was already using.
|
||||
# SABnzbd — usenet downloader, migrated off a hand-authored ini (imported from
|
||||
# the old ZimaOS docker stack) onto NixOS-managed `settings`. Only values that
|
||||
# differ from SABnzbd's own defaults are declared here.
|
||||
#
|
||||
# `admin_dir`/`log_dir` MUST stay absolute: the module writes the merged ini
|
||||
# to /var/lib/sabnzbd/sabnzbd.ini (eMMC), and both dirs are otherwise
|
||||
# relative to wherever the ini lives. Pointing them back at the ORIGINAL
|
||||
# /mnt/data location keeps the existing download queue/history database
|
||||
# (admin_dir) intact — a relative default here would silently "reset"
|
||||
# SABnzbd to an empty queue on first switch, even though nothing was deleted.
|
||||
# `admin_dir`/`log_dir` must stay absolute: the module writes the merged ini to
|
||||
# /var/lib/sabnzbd/sabnzbd.ini (eMMC), so a relative default would resolve
|
||||
# there instead of the original /mnt/data location — silently "resetting"
|
||||
# SABnzbd to an empty queue/history on first switch, without deleting anything.
|
||||
{
|
||||
services.sabnzbd = {
|
||||
enable = true;
|
||||
@@ -73,17 +68,15 @@
|
||||
# Write access to the shared downloads dir (owned darman:users on disk).
|
||||
users.users.sabnzbd.extraGroups = [ "users" ];
|
||||
|
||||
# download/complete/admin dirs all live on the array, but systemd only
|
||||
# derives RequiresMountsFor from /var/lib/sabnzbd (eMMC) — so with the array
|
||||
# absent sabnzbd would start and download onto the 29G OS disk.
|
||||
# download/complete/admin dirs live on the array, but systemd only derives
|
||||
# RequiresMountsFor from /var/lib/sabnzbd (eMMC) — without this, a missing
|
||||
# array lets sabnzbd start and download onto the 29G OS disk instead.
|
||||
systemd.services.sabnzbd.unitConfig.RequiresMountsFor = [ "/mnt/data" ];
|
||||
systemd.services.fix-downloads-perms.unitConfig.RequiresMountsFor = [ "/mnt/data" ];
|
||||
|
||||
# SABnzbd hardcodes completed job folders to 0700 on every job, ignoring
|
||||
# the ini's `umask` (that only covers files during unpack, not the job
|
||||
# dir itself). setgid on Downloads keeps the group as "users" but perm
|
||||
# bits still come back zeroed, locking out cinephage/mediamanager — sweep
|
||||
# it clean instead of fighting SABnzbd.
|
||||
# SABnzbd hardcodes completed job folders to 0700, ignoring the ini's `umask`
|
||||
# (unpack-only) — setgid keeps the group but perm bits still zero out and
|
||||
# lock out cinephage/mediamanager, so sweep it clean on a timer instead.
|
||||
systemd.services.fix-downloads-perms = {
|
||||
description = "Fix group perms SABnzbd resets on completed downloads";
|
||||
serviceConfig.Type = "oneshot";
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
{ ... }:
|
||||
|
||||
# Seerr (formerly Jellyseerr) — request manager for Jellyfin, talks to
|
||||
# Sonarr/Radarr to fulfill requests. Fresh install, no migrated data.
|
||||
#
|
||||
# configDir stays at the module default; bind-mount AppData onto it instead
|
||||
# of overriding configDir, so data lives on the RAID array and survives an
|
||||
# OS-disk reinstall (same DynamicUser/StateDirectory issue as prowlarr.nix —
|
||||
# see that file for why, and why the mount targets /var/lib/private/seerr
|
||||
# rather than the public path).
|
||||
# Seerr (formerly Jellyseerr) — request manager for Jellyfin, talking to
|
||||
# Sonarr/Radarr; fresh install, no migrated data. configDir stays at the
|
||||
# module default, with AppData bind-mounted onto it instead (same
|
||||
# DynamicUser/StateDirectory issue as prowlarr.nix — see that file for why,
|
||||
# and why the mount targets /var/lib/private/seerr rather than the public path).
|
||||
{
|
||||
services.seerr.enable = true;
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
{ ... }:
|
||||
|
||||
# Sonarr — TV library manager, feeds off SABnzbd/Prowlarr. dataDir points at
|
||||
# the config migrated from the old ZimaOS docker stack (indexers/download
|
||||
# client/history already set up). Unlike prowlarr, this module uses a static
|
||||
# `sonarr` user (no DynamicUser) and only auto-chowns dataDir when it's the
|
||||
# module's own default path — since we point at a pre-existing migrated dir,
|
||||
# chown it by hand once after first deploy:
|
||||
# Sonarr — TV library manager, feeds off SABnzbd/Prowlarr; dataDir points at
|
||||
# config migrated from the old ZimaOS docker stack. Unlike prowlarr, this
|
||||
# module uses a static `sonarr` user (no DynamicUser) and only auto-chowns
|
||||
# dataDir at its own default path, so the migrated dir needs a manual
|
||||
# one-time chown after first deploy:
|
||||
# chown -R sonarr:sonarr /mnt/data/AppData/sonarr/config
|
||||
{
|
||||
services.sonarr = {
|
||||
|
||||
Reference in New Issue
Block a user