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.
123 lines
6.6 KiB
Nix
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 -"
|
|
];
|
|
}
|