Files
homelab/services/media/immich.nix
darmanandClaude Opus 5 0ec3c6413c jupiter: keep booting when the data array is missing
After the rack move one of the RAID0 disks failed to enumerate, and jupiter
boot-looped into an emergency shell nobody could use — root is locked, so
sulogin offers a prompt with no answer, and there is no ssh from there:

  Timed out waiting for device /dev/disk/by-uuid/dadbff6f-...
  Dependency failed for /mnt/data.
  Dependency failed for /var/lib/private/prowlarr.
  Dependency failed for Local File Systems.
  local-fs.target: Job local-fs.target/start failed with result 'dependency'
  Reached target Emergency Mode.

`nofail` on /mnt/data did not help, because the prowlarr and seerr bind
mounts layered on top of it had none: without it a mount is RequiredBy
local-fs.target, so those two failed the target on the array's behalf. Give
them `nofail` too and let them fail alone. `systemd.enableEmergencyMode =
false` then keeps a bad array from costing a reachable box at all — far more
useful on a headless host than a console prompt.

Booting further is only safe if nothing quietly relocates onto the 29G eMMC,
so pin the array-backed services to the mount. systemd derives
RequiresMountsFor from a unit's own paths, which for these is somewhere under
/var/lib (eMMC) — nothing pointed immich at mediaLocation or sabnzbd at its
configFile, so with the array gone they would have started and written to the
OS disk, into directories that go invisible the moment /mnt/data mounts over
them. jellyfin, sonarr, radarr and gitea already had a real dependency and
are untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 03:32:42 +02:00

129 lines
7.0 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 -"
];
# 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.
systemd.services.immich-server.unitConfig.RequiresMountsFor = [ "/mnt/data" ];
}