feat(web-arm): deploy Immich with Authelia OIDC, group-gated access #178

Closed
opened 2026-06-26 14:13:59 +02:00 by dominik.polakovics · 0 comments

Goal

Deploy Immich on web-arm using the native services.immich module (nixpkgs 26.05), fronted at https://immich.cloonar.com, with login via Authelia OIDC restricted to members of the immich LDAP group. Photo originals live on a slow Hetzner Storage-Box CIFS share; thumbnails stay on local SSD. Machine learning is on but CPU-throttled so it never starves the websites.

This is the thinnest end-to-end slice: a working, access-gated, empty Immich you can log into. Data migration is a separate human issue.

Human prerequisites (already done — do NOT redo)

  • sops immich-oidc-client-secret (plaintext OIDC client secret) present in hosts/web-arm/secrets.yaml.
  • sops immich-smb-credentials (CIFS username=… / password=…) present in hosts/web-arm/secrets.yaml.
  • LDAP groupOfNames cn=immich,ou=groups,dc=cloonar,dc=com exists with the admin as member.
  • DNS immich.cloonar.com → web-arm.
  • Hetzner Storage-Box sub-account for Immich, share path: //u149513.your-backup.de/u149513-sub12/.
  • Authelia client secret digest — commit this verbatim as the client secret:
    $pbkdf2-sha512$310000$GeKgtfu/HhHSY9UmOOSeaQ$zgMVpKEkQlG1Bt6Tvf2Bq.lrTdIxgLbix8MuVwVwMTo3hw2qjeAoFuEb8DyTCYQh3G.orhqoNOyETBCD9iyWeA

Implementation

Create hosts/web-arm/modules/immich/default.nix, import it from hosts/web-arm/configuration.nix. Host-local module style: inline concrete values, no options/mkIf boilerplate.

1. Immich service (native module)

  • enable = true; host = "127.0.0.1", port = 2283, openFirewall = false (nginx fronts it).
  • mediaLocation = "/var/lib/immich" (local SSD).
  • database.enable = true / database.createDB = true — reuses the existing postgresql_14 instance; the module adds the immich DB + pgvector/vectorchord and flips shared_preload_libraries. ⚠️ First deploy restarts Postgres, briefly interrupting PowerSync/Grafana — expected; call it out in the commit message.
  • redis.enable = true (the module's own immich redis instance — do NOT share authelia's).
  • machine-learning.enable = true.
  • Pin a stable users.users.immich.uid + users.groups.immich.gid to a free id so the CIFS mount can map ownership (mirror how nextcloud's uid/gid are fixed).

2. Storage split — originals on CIFS, thumbnails local

Only library/ (originals) goes to CIFS; thumbs/, upload/, profile/ stay on local SSD under /var/lib/immich.

fileSystems."/var/lib/immich/library" = {
  device = "//u149513.your-backup.de/u149513-sub12/";
  fsType = "cifs";
  options = let
    automount_opts = "x-systemd.automount,noauto,x-systemd.idle-timeout=60,x-systemd.device-timeout=5s,x-systemd.mount-timeout=5s,file_mode=0770,dir_mode=0770";
  in [ "${automount_opts},credentials=${config.sops.secrets.immich-smb-credentials.path},uid=<immich-uid>,gid=<immich-gid>" ];
};
  • sops.secrets.immich-smb-credentials = {}; (root-readable is fine — the mount reads it at mount time).
  • Order the immich units after the mount (RequiresMountsFor = "/var/lib/immich/library" on immich-server), and ensure /var/lib/immich exists owned by immich before the mount (the module's StateDirectory handles the parent). Verify the CIFS submount is writable by the immich user after deploy.

3. Declarative system config via sops template (OAuth + password-login off + transcoding off + ML + job concurrency)

Render the Immich config JSON with the OIDC secret injected from sops; point Immich at it. This keeps the secret out of the world-readable Nix store.

sops.templates."immich.json" = {
  owner = "immich";
  content = builtins.toJSON {
    oauth = {
      enabled = true;
      issuerUrl = "https://auth.cloonar.com";
      clientId = "immich";
      clientSecret = config.sops.placeholder.immich-oidc-client-secret;
      scope = "openid profile email";
      buttonText = "Login with Authelia";
      autoRegister = true;
      autoLaunch = true;            # password login is off; ?autoLaunch=0 is the recovery escape hatch
    };
    passwordLogin.enabled = false;
    newVersionCheck.enabled = false;
    ffmpeg.transcode = "disabled";  # no transcoding (saves ARM CPU + disk)
    machineLearning.enabled = true;
    job = {                          # throttle heavy jobs so they don't pile up
      smartSearch.concurrency = 1;
      faceDetection.concurrency = 1;
      thumbnailGeneration.concurrency = 1;
    };
  };
};
services.immich.environment.IMMICH_CONFIG_FILE = config.sops.templates."immich.json".path;

⚠️ The Immich config schema drifts between releases. The keys above are the intended settings, not guaranteed verbatim — validate exact key names / enum values against the Immich version shipped in 26.05 before relying on them (esp. ffmpeg.transcode's enum and the job.*.concurrency keys). A bad key fails startup — check journalctl -u immich-server after deploy. Once a config file is present, the entire system-settings UI is read-only by design.

4. CPU throttling — websites stay snappy, ML/thumbs just take longer

systemd.services.immich-server.serviceConfig.CPUWeight = 20;
systemd.services.immich-machine-learning.serviceConfig.CPUWeight = 20;
# optional if RAM proves tight: MemoryHigh on immich-machine-learning

Default CPUWeight is 100, which the websites keep — so under contention they get ~5× the share; when the box is idle Immich runs full speed.

5. Authelia OIDC client + authorization policy (hosts/web-arm/modules/authelia.nix)

Add the policy:

authorization_policies.immich = {
  default_policy = "deny";
  rules = [ { policy = "one_factor"; subject = "group:immich"; } ];
};

Add the client to identity_providers.oidc.clients:

{
  id = "immich";
  description = "Immich";
  secret = "$pbkdf2-sha512$310000$GeKgtfu/HhHSY9UmOOSeaQ$zgMVpKEkQlG1Bt6Tvf2Bq.lrTdIxgLbix8MuVwVwMTo3hw2qjeAoFuEb8DyTCYQh3G.orhqoNOyETBCD9iyWeA";
  public = false;
  authorization_policy = "immich";
  redirect_uris = [
    "https://immich.cloonar.com/auth/login"
    "https://immich.cloonar.com/user-settings"
    "app.immich:///oauth-callback"
  ];
  consent_mode = "implicit";
  scopes = [ "openid" "profile" "email" ];
  userinfo_signing_algorithm = "none";
}

No claims_policy needed — access is gated at Authelia, so Immich doesn't need the groups claim. If the mobile app's custom-scheme redirect is rejected, switch to Immich mobileOverrideEnabled + register https://immich.cloonar.com/api/oauth/mobile-redirect.

6. nginx vhost

services.nginx.virtualHosts."immich.cloonar.com" = {
  enableACME = true;
  forceSSL = true;
  acmeRoot = null;
  locations."/" = {
    proxyPass = "http://127.0.0.1:2283";
    proxyWebsockets = true;            # Immich uses websockets for live updates
    extraConfig = ''
      client_max_body_size 50G;        # large photo/video uploads
      proxy_request_buffering off;
      proxy_read_timeout 600s;
    '';
  };
};

7. Backups

  • Add /var/lib/immich to the borg exclude list in utils/modules/borgbackup.nix (CIFS originals must not be pulled over the wire; thumbs are regenerable) — mirror the existing dont backup nextcloud line with a comment.
  • DB: no change — existing services.postgresqlBackup already dumps all DBs (immich auto-included); borg picks up /var/backup/postgresql.
  • Originals: protected by the Storage-Box snapshot policy on sub12 (Hetzner side).

Acceptance criteria

  • https://immich.cloonar.com serves Immich over TLS.
  • Login redirects to Authelia; a group:immich member can log in (first login becomes the Immich admin); a non-member is denied at Authelia.
  • Password login is not offered (OAuth-only).
  • UI/timeline loads; a test upload lands in /var/lib/immich/library (CIFS) and its thumbnail appears under local /var/lib/immich/thumbs.
  • immich-server + immich-machine-learning run with CPUWeight=20; journalctl shows no config-schema errors.
  • Pre-commit dry-build passes for web-arm.

Notes / risks

  • Postgres restart on first deploy (shared_preload_libraries) briefly blips PowerSync/Grafana.
  • Immich config-file schema can drift across releases — verify keys against the 26.05 Immich; startup fails loudly if wrong.
  • Do not bump system.stateVersion.
## Goal Deploy Immich on **web-arm** using the native `services.immich` module (nixpkgs 26.05), fronted at `https://immich.cloonar.com`, with login via **Authelia OIDC restricted to members of the `immich` LDAP group**. Photo originals live on a slow Hetzner Storage-Box CIFS share; thumbnails stay on local SSD. Machine learning is **on but CPU-throttled** so it never starves the websites. This is the thinnest end-to-end slice: a working, access-gated, **empty** Immich you can log into. Data migration is a separate human issue. ## Human prerequisites (already done — do NOT redo) - sops `immich-oidc-client-secret` (plaintext OIDC client secret) present in `hosts/web-arm/secrets.yaml`. - sops `immich-smb-credentials` (CIFS `username=…` / `password=…`) present in `hosts/web-arm/secrets.yaml`. - LDAP `groupOfNames` `cn=immich,ou=groups,dc=cloonar,dc=com` exists with the admin as `member`. - DNS `immich.cloonar.com` → web-arm. - Hetzner Storage-Box sub-account for Immich, share path: `//u149513.your-backup.de/u149513-sub12/`. - Authelia client secret **digest** — commit this verbatim as the client `secret`: `$pbkdf2-sha512$310000$GeKgtfu/HhHSY9UmOOSeaQ$zgMVpKEkQlG1Bt6Tvf2Bq.lrTdIxgLbix8MuVwVwMTo3hw2qjeAoFuEb8DyTCYQh3G.orhqoNOyETBCD9iyWeA` ## Implementation Create `hosts/web-arm/modules/immich/default.nix`, import it from `hosts/web-arm/configuration.nix`. Host-local module style: inline concrete values, no `options`/`mkIf` boilerplate. ### 1. Immich service (native module) - `enable = true`; `host = "127.0.0.1"`, `port = 2283`, `openFirewall = false` (nginx fronts it). - `mediaLocation = "/var/lib/immich"` (local SSD). - `database.enable = true` / `database.createDB = true` — reuses the existing `postgresql_14` instance; the module adds the immich DB + pgvector/vectorchord and flips `shared_preload_libraries`. ⚠️ **First deploy restarts Postgres**, briefly interrupting PowerSync/Grafana — expected; call it out in the commit message. - `redis.enable = true` (the module's **own** immich redis instance — do NOT share authelia's). - `machine-learning.enable = true`. - Pin a stable `users.users.immich.uid` + `users.groups.immich.gid` to a free id so the CIFS mount can map ownership (mirror how nextcloud's uid/gid are fixed). ### 2. Storage split — originals on CIFS, thumbnails local Only `library/` (originals) goes to CIFS; `thumbs/`, `upload/`, `profile/` stay on local SSD under `/var/lib/immich`. ```nix fileSystems."/var/lib/immich/library" = { device = "//u149513.your-backup.de/u149513-sub12/"; fsType = "cifs"; options = let automount_opts = "x-systemd.automount,noauto,x-systemd.idle-timeout=60,x-systemd.device-timeout=5s,x-systemd.mount-timeout=5s,file_mode=0770,dir_mode=0770"; in [ "${automount_opts},credentials=${config.sops.secrets.immich-smb-credentials.path},uid=<immich-uid>,gid=<immich-gid>" ]; }; ``` - `sops.secrets.immich-smb-credentials = {};` (root-readable is fine — the mount reads it at mount time). - Order the immich units **after** the mount (`RequiresMountsFor = "/var/lib/immich/library"` on `immich-server`), and ensure `/var/lib/immich` exists owned by immich before the mount (the module's `StateDirectory` handles the parent). Verify the CIFS submount is writable by the immich user after deploy. ### 3. Declarative system config via sops template (OAuth + password-login off + transcoding off + ML + job concurrency) Render the Immich config JSON with the OIDC secret injected from sops; point Immich at it. This keeps the secret out of the world-readable Nix store. ```nix sops.templates."immich.json" = { owner = "immich"; content = builtins.toJSON { oauth = { enabled = true; issuerUrl = "https://auth.cloonar.com"; clientId = "immich"; clientSecret = config.sops.placeholder.immich-oidc-client-secret; scope = "openid profile email"; buttonText = "Login with Authelia"; autoRegister = true; autoLaunch = true; # password login is off; ?autoLaunch=0 is the recovery escape hatch }; passwordLogin.enabled = false; newVersionCheck.enabled = false; ffmpeg.transcode = "disabled"; # no transcoding (saves ARM CPU + disk) machineLearning.enabled = true; job = { # throttle heavy jobs so they don't pile up smartSearch.concurrency = 1; faceDetection.concurrency = 1; thumbnailGeneration.concurrency = 1; }; }; }; services.immich.environment.IMMICH_CONFIG_FILE = config.sops.templates."immich.json".path; ``` ⚠️ **The Immich config schema drifts between releases.** The keys above are the *intended settings*, not guaranteed verbatim — **validate exact key names / enum values against the Immich version shipped in 26.05** before relying on them (esp. `ffmpeg.transcode`'s enum and the `job.*.concurrency` keys). A bad key fails startup — check `journalctl -u immich-server` after deploy. Once a config file is present, the entire system-settings UI is read-only by design. ### 4. CPU throttling — websites stay snappy, ML/thumbs just take longer ```nix systemd.services.immich-server.serviceConfig.CPUWeight = 20; systemd.services.immich-machine-learning.serviceConfig.CPUWeight = 20; # optional if RAM proves tight: MemoryHigh on immich-machine-learning ``` Default `CPUWeight` is 100, which the websites keep — so under contention they get ~5× the share; when the box is idle Immich runs full speed. ### 5. Authelia OIDC client + authorization policy (`hosts/web-arm/modules/authelia.nix`) Add the policy: ```nix authorization_policies.immich = { default_policy = "deny"; rules = [ { policy = "one_factor"; subject = "group:immich"; } ]; }; ``` Add the client to `identity_providers.oidc.clients`: ```nix { id = "immich"; description = "Immich"; secret = "$pbkdf2-sha512$310000$GeKgtfu/HhHSY9UmOOSeaQ$zgMVpKEkQlG1Bt6Tvf2Bq.lrTdIxgLbix8MuVwVwMTo3hw2qjeAoFuEb8DyTCYQh3G.orhqoNOyETBCD9iyWeA"; public = false; authorization_policy = "immich"; redirect_uris = [ "https://immich.cloonar.com/auth/login" "https://immich.cloonar.com/user-settings" "app.immich:///oauth-callback" ]; consent_mode = "implicit"; scopes = [ "openid" "profile" "email" ]; userinfo_signing_algorithm = "none"; } ``` No `claims_policy` needed — access is gated at Authelia, so Immich doesn't need the groups claim. If the mobile app's custom-scheme redirect is rejected, switch to Immich `mobileOverrideEnabled` + register `https://immich.cloonar.com/api/oauth/mobile-redirect`. ### 6. nginx vhost ```nix services.nginx.virtualHosts."immich.cloonar.com" = { enableACME = true; forceSSL = true; acmeRoot = null; locations."/" = { proxyPass = "http://127.0.0.1:2283"; proxyWebsockets = true; # Immich uses websockets for live updates extraConfig = '' client_max_body_size 50G; # large photo/video uploads proxy_request_buffering off; proxy_read_timeout 600s; ''; }; }; ``` ### 7. Backups - Add `/var/lib/immich` to the borg `exclude` list in `utils/modules/borgbackup.nix` (CIFS originals must not be pulled over the wire; thumbs are regenerable) — mirror the existing `dont backup nextcloud` line with a comment. - DB: no change — existing `services.postgresqlBackup` already dumps all DBs (immich auto-included); borg picks up `/var/backup/postgresql`. - Originals: protected by the Storage-Box snapshot policy on sub12 (Hetzner side). ## Acceptance criteria - `https://immich.cloonar.com` serves Immich over TLS. - Login redirects to Authelia; **a `group:immich` member can log in** (first login becomes the Immich admin); **a non-member is denied** at Authelia. - Password login is not offered (OAuth-only). - UI/timeline loads; a test upload lands in `/var/lib/immich/library` (CIFS) and its thumbnail appears under local `/var/lib/immich/thumbs`. - `immich-server` + `immich-machine-learning` run with `CPUWeight=20`; `journalctl` shows no config-schema errors. - Pre-commit dry-build passes for web-arm. ## Notes / risks - Postgres restart on first deploy (shared_preload_libraries) briefly blips PowerSync/Grafana. - Immich config-file schema can drift across releases — verify keys against the 26.05 Immich; startup fails loudly if wrong. - Do **not** bump `system.stateVersion`.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/nixos#178
No description provided.