immich: add the service and import the ZimaOS library

jupiter had a leftover docker-compose Immich on the RAID (/mnt/data/Immich,
9.9G) that survived the NixOS install. Native module now, media at
/mnt/data/AppData/immich, caddy vhost on 2283 with a 50GB body limit
(caddy's default rejects video uploads).

The package comes from nixpkgs-unstable, the module from the 26.05 pin:
26.05 ships immich 2.7.5, but that database was last written by 3.0.0 and
migrations only run forward --

  corrupted migrations: previously executed migration
  1776217577402-DropAuditTable is missing

Safe because the two module files are byte-identical at these revisions;
services/media/immich.nix carries the diff command to re-check on a bump.
Drop the input once the stable pin ships >= 3.0.0.

immich needs group "users" only to traverse /mnt/data/AppData (drwx--x---);
its own dir stays 0700 immich:immich. mediaLocation is outside /var/lib, so
the module's tmpfiles entry only ADJUSTS it -- add a rule that creates it.

scripts/immich-import-legacy-db does the database half: boots a copy of the
legacy PGDATA under the matching image (PG14 + vchord 0.3.0 + pgvector
0.8.1), dumps it with the local pg_dump 17, restores into a scratch DB,
fixes ownership, and only swaps after confirmation. Never touches the
original. The old cluster ran VectorChord, not pgvecto.rs, so the smart
search and face embeddings survive -- no ML re-run.

Imported: 666 assets, 25 people, 647 clip + 359 face embeddings, 2 users.
This commit is contained in:
2026-07-21 00:51:10 +02:00
parent 3a6950779e
commit 6ce61ab519
5 changed files with 336 additions and 0 deletions
+62
View File
@@ -0,0 +1,62 @@
{ 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;
# 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 -"
];
}