{ 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" ]; }