Immich
Immich is a self-hosted photo and video library with a native mobile app that auto-uploads from the phone's camera roll, face / object recognition that runs locally, and a timeline UI that feels lifted straight from Google Photos. In this homelab it's the canonical photo store — backed by CNPG Postgres, Valkey for the queue and cache, Longhorn-encrypted PVCs for the thumbnails and ML models, and the photo originals themselves on an NFS mount from TrueNAS.
Why Immich
- Mobile-first auto-upload. The Android/iOS app handles background sync without the user having to think about it; this is what makes a self-hosted replacement actually viable. Most of the alternatives skipped this.
- Local ML. Face clustering, object tagging, and CLIP-based semantic search all run in-cluster on the
immich-machine-learningDeployment. No third-party API calls, no photos leaving the house. - Shared albums + external editors out of the box. Day-to-day photo workflow with another person doesn't require workarounds.
- Active development. The project moves fast — features that were missing six months ago (live photos, RAW handling, search) ship steadily. The cost is occasional schema migrations on upgrade; it's the right trade.
Alternatives considered
| Option | Hosted | Self-hosted | Why not |
|---|---|---|---|
| Google Photos | ✓ | The whole point of the homelab is to leave this | |
| Apple Photos / iCloud | ✓ | Locked to Apple devices; per-seat pricing for storage | |
| PhotoPrism | ✓ | Strong indexer, weaker mobile auto-upload story | |
| LibrePhotos | ✓ | Tracked, but Immich's UX + release cadence pulled ahead | |
| Ente (self-hosted) | ✓ | ✓ | End-to-end encrypted, which is great — but rules out server-side ML/search |
Installation
The Immich install is the most substantial app in the cluster — two workloads, a Postgres cluster, a Valkey StatefulSet, three PVCs, and an NFS mount — composed from the platform primitives:
- Two Deployments in the
immichnamespace.immich-serverhandles the API + web UI;immich-machine-learningruns the model inference. Both digest-pinned, both bumped by Renovate at the same version. - Postgres via CNPG. A dedicated
Clusterwith thepgvecto.rsextension that Immich requires for CLIP embeddings; data PVCs on thelonghorn-encryptedstorage class. - Cache + queue via Valkey. A small StatefulSet rather than a Deployment because it owns durable state for queued ML jobs.
- Storage split across three roles.
immich-dataPVC — thumbnails, transcoded previews, profile images.longhorn-encrypted, annotated for k8up backup.immich-machine-learning-dataPVC — the downloaded model weights. Reproducible, so not backed up.- NFS PV
immich-truenas-nfs-images— the original photos and videos, on TrueNAS. Read-write from the cluster, snapshotted at the storage layer.
- Routing via
HTTPRouteon the public Envoy Gateway listener; TLS via cert-manager at the gateway. - Security context. App containers run as
runAsUser: 999, non-root, all capabilities dropped. Valkey runs an init container as root to fix permissions, then the main container drops down.
Administration
- Initial admin setup. First browser hit creates the admin account; subsequent users are invited from the admin panel or self-register if signup is enabled.
- OIDC sign-in. Immich supports OIDC against Keycloak; the config lives in the admin UI rather than env vars. Map the email claim to the Immich email field.
- External libraries. Admin → External Libraries points Immich at the NFS-mounted photo paths; scans run periodically. This is how the existing photo archive shows up alongside fresh mobile uploads.
- Reindexing after a model bump. When the machine-learning image version changes the embedding format, the search index needs a rerun: Admin → Jobs → Smart Search → Run (all). CLIP and face recognition each have their own job.
- Upgrades. Renovate auto-merges patch / digest bumps per the Renovate policy; minor and major bumps wait for a human read because they sometimes carry irreversible schema migrations. Always read the release notes for migrations before merging.
- Backups. Two-track: the CNPG cluster uses
k8up.io/backupcommand: pg_dump(so the restic snapshot captures a consistent SQL dump rather than a hot data directory), and theimmich-dataPVC is backed up via thek8up.io/backupannotation. Originals on NFS are TrueNAS's responsibility — see Topics → Backups end-to-end for the full chain.
Usage
- Mobile app (App Store / Play Store, also on F-Droid) — point at
https://immich.web.kueber.eu, log in, enable background backup. That's the whole onboarding. - Web UI for browsing the timeline, managing albums, running search ("photos of X at Y"), and admin tasks.
- Shared albums. Add another Immich account (or a public link with optional password / expiry) to share a slice of the library.
- External editors. The mobile app hands off to whatever the OS has registered for image editing; edits get re-uploaded.
- CLI / API.
immich-clifor bulk uploads from a workstation (useful for ingesting an existing photo archive); the REST API is documented and stable enough for ad-hoc automation.
Cluster Deployment
Immich — Talos cluster
Cluster-specific notes only. General product info, "why we use it", and alternatives live in docusaurus/docs/apps/immich.mdx.
Deviations from defaults
Defaults live in docusaurus/docs/apps/immich.mdx — document anything this cluster does differently here, with a one-line reason.
- Image:
ghcr.io/immich-app/immich-machine-learning:v2.7.5@sha256:a2501141440f10516d329fdfba2c68082e19eb9ba6016c061ac80d23beadf7f3 - Image:
ghcr.io/immich-app/immich-server:v2.7.5@sha256:c15bff75068effb03f4355997d03dc7e0fc58720c2b54ad6f7f10d1bc57efaa5 - Image:
valkey/valkey:9.1.0-alpine@sha256:a35428eba9043cc0b79dbe54100f0c92784f2de00ad09b01182bfb1c5c83d1bd
Rendered manifests (kustomize build)
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
kustomize.toolkit.fluxcd.io/force: enabled
labels:
app: immich-machine-learning
name: immich-machine-learning
namespace: immich
spec:
replicas: 1
selector:
matchLabels:
app: immich-machine-learning
ingress: public
strategy:
type: Recreate
template:
metadata:
labels:
app: immich-machine-learning
ingress: public
spec:
containers:
- env:
- name: DB_HOSTNAME
valueFrom:
secretKeyRef:
key: host
name: cnpg-app
- name: DB_DATABASE_NAME
valueFrom:
secretKeyRef:
key: dbname
name: cnpg-app
- name: DB_USERNAME
valueFrom:
secretKeyRef:
key: user
name: cnpg-app
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
key: password
name: cnpg-app
- name: DB_PORT
valueFrom:
secretKeyRef:
key: port
name: cnpg-app
envFrom:
- secretRef:
name: immich
image: >-
ghcr.io/immich-app/immich-machine-learning:v2.7.5@sha256:a2501141440f10516d329fdfba2c68082e19eb9ba6016c061ac80d23beadf7f3
imagePullPolicy: Always
name: immich-machine-learning
ports:
- containerPort: 3003
name: web
resources:
limits:
memory: 4004Mi
requests:
cpu: 10m
memory: 804Mi
volumeMounts:
- mountPath: /cache
name: immich-machine-learning-data
securityContext:
seccompProfile:
type: RuntimeDefault
volumes:
- name: immich-machine-learning-data
persistentVolumeClaim:
claimName: immich-machine-learning-data
---
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
kustomize.toolkit.fluxcd.io/force: enabled
labels:
app: immich-server
name: immich-server
namespace: immich
spec:
replicas: 1
selector:
matchLabels:
app: immich-server
ingress: public
strategy:
rollingUpdate: null
type: Recreate
template:
metadata:
labels:
app: immich-server
ingress: public
spec:
containers:
- env:
- name: DB_HOSTNAME
valueFrom:
secretKeyRef:
key: host
name: cnpg-app
- name: DB_DATABASE_NAME
valueFrom:
secretKeyRef:
key: dbname
name: cnpg-app
- name: DB_USERNAME
valueFrom:
secretKeyRef:
key: user
name: cnpg-app
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
key: password
name: cnpg-app
- name: DB_PORT
valueFrom:
secretKeyRef:
key: port
name: cnpg-app
envFrom:
- secretRef:
name: immich
image: >-
ghcr.io/immich-app/immich-server:v2.7.5@sha256:c15bff75068effb03f4355997d03dc7e0fc58720c2b54ad6f7f10d1bc57efaa5
imagePullPolicy: Always
name: immich-server
ports:
- containerPort: 2283
name: web
readinessProbe:
httpGet:
path: /
port: 2283
initialDelaySeconds: 5
periodSeconds: 5
successThreshold: 1
resources:
limits: {}
requests:
cpu: 10m
memory: 984Mi
volumeMounts:
- mountPath: /photos
name: immich-files
subPath: photos
- mountPath: /usr/src/app/upload
name: immich-data
- mountPath: /usr/src/app/upload/upload
name: immich-files
subPath: immich
securityContext:
seccompProfile:
type: RuntimeDefault
volumes:
- name: immich-files
persistentVolumeClaim:
claimName: immich-truenas-nfs-images
- name: immich-data
persistentVolumeClaim:
claimName: immich-data
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: valkey
namespace: immich
spec:
replicas: 1
selector:
matchLabels:
app: valkey
serviceName: valkey
template:
metadata:
labels:
app: valkey
spec:
containers:
- args:
- valkey-server
image: valkey/valkey:9.1.0-alpine@sha256:a35428eba9043cc0b79dbe54100f0c92784f2de00ad09b01182bfb1c5c83d1bd
livenessProbe:
initialDelaySeconds: 10
periodSeconds: 10
tcpSocket:
port: 6379
name: valkey
ports:
- containerPort: 6379
name: client
readinessProbe:
initialDelaySeconds: 3
periodSeconds: 5
tcpSocket:
port: 6379
resources:
limits:
memory: 512Mi
requests:
cpu: 50m
memory: 128Mi
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
privileged: false
volumeMounts:
- mountPath: /conf
name: conf
- mountPath: /data
name: data
securityContext:
fsGroup: 1000
fsGroupChangePolicy: OnRootMismatch
runAsGroup: 1000
runAsNonRoot: true
runAsUser: 999
seccompProfile:
type: RuntimeDefault
volumes:
- emptyDir: {}
name: conf
- emptyDir: {}
name: data