Immich¶
Self-hosted photo and video backup for TAPPaaS — the private alternative to Google Photos / iCloud Photos. Mobile auto-upload, timeline, albums, sharing, ML-powered smart search and face recognition, all on your own hardware.
- Upstream: https://immich.app
- NixOS wiki: https://wiki.nixos.org/wiki/Immich
- License of this module: MPL-2.0 (see repository LICENSE)
Overview¶
| VM | immich (VMID 361), in the environment's zone (zone0 from config/environments/<env>.json) |
| App | Immich 3 (services.immich, module + package from nixos-unstable until NixOS 26.11), HTTP on port 2283 |
| Database | PostgreSQL + VectorChord — auto-provisioned by the NixOS module, unix-socket peer auth (no DB password exists) |
| Cache | Redis (named instance immich), unix socket |
| ML | immich-machine-learning on localhost:3003 — smart search + face recognition; always on, the 8 GB VM is sized for it |
| TLS | Terminated by Caddy on the OPNsense firewall (network:proxy); the VM serves plain HTTP |
| Photos | On the VM's own disk (diskSize, 500G on tanka1), in /var/lib/immich (Immich's mediaLocation) |
| HA | cluster:ha — the disk is replicated to the HA node every 15 min; Proxmox HA restarts the VM there if its node fails |
Storage¶
One disk. The photo library lives on the VM's own disk (diskSize in immich.json, default 500G on tanka1), in Immich's mediaLocation, /var/lib/immich, where Immich keeps upload/, library/, thumbs/, encoded-video/, profile/ and backups/.
Why not a separate photo disk: one disk is what the platform already looks after. The disk-threshold check watches df / and grows scsi0 through diskSize (grow-only); cluster:ha replicates it to the HA node; PBS backs it up. A second disk would be invisible to the first and need its own size setting. More room for photos: module-manager module modify immich --set diskSize=<size>; boot.growPartition grows the filesystem.
Photos are the one thing here that cannot be re-created, so the disk is on tanka, the replicated tier. It is thin-provisioned (thinDisk: true, #773): an empty library takes almost nothing on either node and the disk grows into its 500G as photos arrive, instead of reserving 500G on each node from day one.
Services¶
immich-server— API + web UI on0.0.0.0:2283(firewall: 22, 2283 only)immich-machine-learning— CLIP smart search, face recognition (localhost:3003; models ~1–2 GB download to/var/cache/immichon first job). Always on; its memory is capped (MemoryHigh 4G, MemoryMax 5G) so a bulk import cannot starve PostgreSQL and the server.postgresql/redis-immich— module-managed, no TCP listenerspostgresqlBackup-immich— daily SQL dump at 02:30 (staggered after Immich's built-in ~02:00 dump) to/var/backup/immich/postgresql, 30-day retention (monthly cleanup timer)
Backups¶
Three layers:
- PBS (
backup:vm) snapshots the VM disk, photos included. - Immich built-in: nightly
pg_dumpallinto/var/lib/immich/backups(on the same disk, so it rides along with PBS). - postgresqlBackup: daily plain-SQL dump (02:30) to
/var/backup/immich— restorable without Immich itself.
Authentication & SSO¶
install.sh bootstraps a local admin account and prints the generated password once (change it after first login) — this is the account of last resort if SSO is ever broken (see Lockout recovery in INSTALL.md).
Everyone else signs in through Authentik, on web and in the mobile app, via a Login with OAuth button — no separate Immich account has to be created up front, and no manual configuration is needed in either the Authentik or the Immich admin UI. immich.json depends on identity:identity (ADR-006) and declares an identity block; every install-module.sh/update-module.sh run reconciles the Authentik side and applies the client credentials to Immich itself.
How it wires together¶
graph TD
classDef default font-size:12px,font-weight:bold;
installsvc["identity:identity install-service.sh<br/><small>generic ADR-006 mechanism, unmodified</small>"]
authentikcli["authentik-controller CLI"]
authentikapi["Authentik REST API"]
secretsenv["/etc/secrets/immich.env<br/><small>OIDC_CLIENT_ID / SECRET / DISCOVERY_URI</small>"]
companion["/etc/nixos/immich.json<br/><small>proxyDomain — deployed by update-os.sh</small>"]
installsh["immich/install.sh"]
apikeyfile["/etc/secrets/immich-admin-api-key"]
configuresvc["immich-configure-oidc.service"]
immichapi["Immich /api/system-config"]
installsvc -->|"oidc-app-ensure + app-bind-groups"| authentikcli --> authentikapi
installsvc -->|writes| secretsenv
installsh -->|"mints a scoped API key at admin bootstrap"| apikeyfile
secretsenv --> configuresvc
apikeyfile --> configuresvc
companion -->|"public URL, at build time"| configuresvc
configuresvc -->|"GET, merge, PUT the .oauth section"| immichapi Two things happen in parallel, both driven by immich.json's identity block:
- The Authentik side is handled entirely by the generic
identity:identitymechanism that every OIDC-native TAPPaaS module uses (Nextcloud included) — no module-specific code. It readsidentity.oidcRedirectPaths(three plainhttps://<proxyDomain><path>URIs:/auth/login,/user-settings, and/api/oauth/mobile-redirect), creates/updates the OAuth2 provider and application, binds access to theusersgroup (Authentik denies all access to an application with no binding, so this is applied unconditionally), and writes the resulting client credentials plus the discovery URL into/etc/secrets/immich.envon the VM.
The mobile app's OAuth callback is a custom URI scheme, app.immich:///oauth-callback — that value is never registered anywhere directly, since neither Authentik nor Immich's own validation reliably accepts a non-https redirect URI. Immich ships a built-in server route, /api/oauth/mobile-redirect, that forwards to the custom scheme internally, so that plain HTTPS path is what gets registered and configured everywhere instead — this is Immich's own documented mechanism (see Immich OAuth docs), not something specific to this module.
- Immich's own side is handled by
immich-configure-oidc.service(defined inimmich.nix). Immich has no OIDC-provider CLI comparable to Nextcloud'socc user_oidc:provider— its OAuth settings live in the system config, editable only through the admin UI or/api/system-config— so a declarativeservices.immich.settingswould freeze that entire config read-only just to set OAuth. Instead, this service authenticates with a narrowly-scoped API key (permissionssystemConfig.read+systemConfig.update, minted byinstall.shright after admin bootstrap — the admin password itself is never written to disk), reads the current system config, merges in the OAuth fields (issuer, client credentials, scope, the mobile redirect override), and writes the merged document back — leaving every other setting the admin has tuned untouched. The issuer value is derived from the discovery URL and must match Authentik's own issuer string exactly, trailing slash included, since Immich's OIDC client validates it per the OIDC discovery specification.
The mobile redirect override needs this module's own public URL, which /etc/secrets/immich.env does not carry (it only holds the OIDC_* keys). immich.nix reads it from /etc/nixos/immich.json — the module's config, which update-os.sh deploys beside it with the name the proxy publishes it under (proxyDomain, #715), the same idiom vaultwarden and logging use. The URL is built into the service, so a changed domain changes the unit and the next rebuild re-runs it; a module that is not published gets web OAuth only.
Access can be narrowed beyond "every TAPPaaS login" with authentik-controller app-bind-groups immich --group <narrower-group> (see authentik-controller/README.md), and per-user storage quotas can be wired through an Authentik scope mapping plus identity.scopes in immich.json — see INSTALL.md for both. Mobile installs use the same public URL and credentials as the web app.
Logging¶
Logs live in the local journal:
Verbosity is set via IMMICH_LOG_LEVEL in immich.nix (verbose, debug, log, warn, error). Central log shipping to the logging foundation module (Loki) is not wired up: app zones have no route to the mgmt zone (Tier-0 accepts no inbound pinholes by design), so Tier-1 app VMs cannot push to Loki today. When the platform provides an ingest path, the Promtail client snippet in TAPPaaS/src/foundation/logging/DESIGN.md is a copy-paste job.
Module dependencies & traffic flow¶
graph LR
classDef default font-size:12px,font-weight:bold;
immich["immich<br/><small>zone: env zone, VM :2283</small>"]
cluster["cluster:vm"]
templates["templates:nixos"]
backup["backup:vm"]
netproxy["network:proxy / network:rules"]
identity["identity:identity"]
caddy["Caddy reverse proxy<br/><small>OPNsense firewall, :443</small>"]
homeclients["home-zone clients<br/><small>LAN</small>"]
vpnpeers["netbird VPN peers<br/><small>remote devices</small>"]
internet["internet<br/><small>public</small>"]
authentik["Authentik<br/><small>identity module</small>"]
immich -->|dependsOn| cluster
immich -->|dependsOn| templates
immich -->|dependsOn| backup
immich -->|dependsOn| netproxy
immich -->|dependsOn| identity
homeclients -.->|"https://immich.<domain>"| caddy
homeclients -.->|"direct in-zone: http://immich.<zone>.internal:2283"| immich
vpnpeers -.->|"same vhost, via WireGuard overlay"| caddy
internet -.->|"proxyAllowedZones includes internet by default"| caddy
caddy -.->|"upstream :2283"| immich
immich -.->|"OIDC login + discovery"| authentik Solid arrows are declared dependsOn edges (install-order dependencies). Dashed arrows are traffic: network:proxy creates the immich.<domain> vhost on the Caddy reverse proxy, and proxyAllowedZones (home, netbird, internet by default) controls which source zones that vhost accepts — including the public internet, so mobile auto-upload works away from home without a VPN. Home-zone clients can also bypass the proxy and hit the VM directly in-zone. immich:identity traffic is the OIDC login/discovery flow against Authentik, described above.
Immich deliberately uses no NFS share: its photo library is high-value data on its own 500G VM disk, replicated for HA — not on the re-creatable media tier that storage:nfs serves (see Storage).
Future enhancements¶
- Hardware transcoding: set
accelerationDevicesinimmich.nix(see INSTALL.md → Variants).
Quick Start¶
# on tappaas-cicd, from "${TAPPAAS_REPO:-/home/tappaas/TAPPaaS}"/src/home/immich
install-module.sh immich # VM + config + admin bootstrap
./test.sh # health checks
Then open https://immich.<your-domain>, log in with the printed admin credentials, and install the mobile app.