Files
homelab/services/media/immich.nix
T
darmanandClaude Sonnet 5 6f24ab69ad 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
2026-09-18 21:36:30 +02:00

104 lines
5.4 KiB
Nix

{ config, pkgs, inputs, ... }:
# 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 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
# 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
unstable = import inputs.nixpkgs-unstable {
inherit (pkgs.stdenv.hostPlatform) system;
};
in
{
services.immich = {
enable = true;
# Both the server and immich-machine-learning follow this: the module takes
# the ML service from cfg.package.machine-learning (passthru).
package = unstable.immich;
# Listens on all interfaces: :2283 stays closed on the LAN (no
# openFirewall), reachable over tailscale0 and via localhost (caddy).
host = "0.0.0.0";
port = 2283;
mediaLocation = "/mnt/data/AppData/immich";
machine-learning.enable = true;
# ⚠️ 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 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
# appends /.well-known/openid-configuration to it.
issuerUrl = "https://auth.mgaction.town/application/o/immich/";
# Authentik generates this; not a secret (it is sent in the browser
# redirect), so it lives here rather than in sops.
clientId = "FxrkWFe8keBrN83tu03UbAshSck52he2GphJDYSQ";
# Read at runtime via systemd LoadCredential — never lands in the
# world-readable /nix/store copy of the rendered config.
clientSecret._secret = config.sops.secrets.immich_oauth_client_secret.path;
scope = "openid email profile";
buttonText = "Login with Authentik";
# 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;
# 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.
mobileOverrideEnabled = true;
mobileRedirectUri = "https://immich.mgaction.town/api/oauth/mobile-redirect";
};
};
# 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 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 — 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 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" ];
}