Files
homelab/services/media/immich.nix
T
darman a4c7768625 immich: fix OIDC clientId, redirect logout to immich's own login page
clientId was still the placeholder "immich" instead of Authentik's actual
generated id, and the sops secret it points at (immich_oauth_client_secret)
was never declared on jupiter. Wire both up, and set endSessionEndpoint so
logout lands back on immich instead of Authentik's "logged out" page.
2026-07-23 23:14:11 +02:00

123 lines
6.6 KiB
Nix

{ 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.
#
# 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.
#
# ⚠️ 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:
# 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;
};
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, 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.
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.
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";
# 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.
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.
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 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.
};
# /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.
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.
systemd.tmpfiles.rules = [
"d /mnt/data/AppData/immich 0700 immich immich -"
];
}