Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Immich

apps.immich runs Immich and reconciles the accounts plus the external libraries that belong to them. Where photos live stays with the consumer: mediaLocation, transcoding, job concurrency and the machine-learning subsystem are left at their nixpkgs defaults for you to set on services.immich.

Two consumer patterns

Immich stores uploads under its own media location, and can additionally scan directories you already have. The difference is one option:

selfhost.users.alice.services.immich.enable = true;                      # upload-only
selfhost.users.bob.services.immich = {
  enable = true;
  libraries = [ { name = "bob-photos"; importPaths = [ "/mnt/photos/bob" ]; } ];
};

libraries defaults to empty, so an account is upload-only unless you say otherwise, and a deployment that never uses external libraries declares no paths anywhere. Declared paths become writable to the service, since Immich writes sidecars and thumbnails beside the originals it imports.

A library’s name is its reconcile identity. Renaming one creates a second library rather than renaming the first.

Accounts

services.immich.admin defaults to the user’s isAdmin, so fleet-level admin carries into Immich without restating it. quotaBytes and storageLabel are reconciled on every run, so changing a quota moves the existing account.

passwordFile is a bootstrap credential: applied when the account is created and never reconciled, so a password changed in the app survives. An account given one is asked to change it on first login. Accounts without a passwordFile get a random password, which is the right default when sign-in is through OIDC. Without an OIDC provider the service’s oidc.enable composes to false, and each account needs a passwordFile to be reachable.

OIDC only authenticates. autoRegister is off, so an OIDC login lands on an account that selfhost.users already declared, matched by email, and a login with no declared account is refused. That keeps one source of truth for who exists.

The admin@immich.local account is separate from all of these. The reconciler authenticates as it to drive the API, and nothing else uses it.

Nothing is deleted

The reconcile creates and updates, never removes. Dropping a user or a library from your config leaves the Immich side untouched, because deleting either would take photos with it. Accounts created outside the framework are likewise left alone. Removals are a deliberate manual step in the app.

Backups

Uploaded photos live under Immich’s media location, not in your external libraries, so nothing else covers them. They are far too large to copy through a backup hook, so bind them into a target instead, which mounts them read-only with no second copy:

selfhost.backup.targets.<name>.bindings = {
  "/immich/library" = "${config.services.immich.mediaLocation}/library";
  "/immich/upload"  = "${config.services.immich.mediaLocation}/upload";
  "/immich/profile" = "${config.services.immich.mediaLocation}/profile";
};

Skip thumbs/ and encoded-video/, which Immich regenerates. If every account is backed by external libraries, those originals already live in directories you back up separately and there is nothing here to add.

The app registers no backup hook for the database. Immich writes its own nightly dump to <mediaLocation>/backups, so binding that path picks it up if you want the albums, people and favourites that a re-scan cannot rebuild.

Keeping up with Immich

Immich moves API routes across major releases. api-contract.json beside the module declares the request payloads configure.nu sends, and the vm-immich test asserts them against the server’s own /api/spec.json. Immich validates with zod, which strips unknown keys rather than rejecting them, so a renamed field would otherwise stop being applied with nothing to show for it. Bumping nixpkgs and running the test is what tells you the reconciler still holds.