Add Immich photo management service

Deploy Immich via Helm chart with:
- PostgreSQL cluster with pgvecto.rs (immich-pg) for AI vector search
- NFS storage on sifaka for photo library
- Tailscale Ingress + Caddy proxy for photos.ops.eblu.me access
- Machine learning service for face/object recognition

Immich provides a self-hosted Google Photos/iCloud alternative with
AI-powered search, face recognition, and support for RAW files.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Erich Blume 2026-01-26 07:52:24 -08:00
commit 5482f74500
13 changed files with 401 additions and 0 deletions

View file

@ -58,6 +58,9 @@ caddy_services:
- name: teslamate
host: "tesla.{{ caddy_domain }}"
backend: "https://tesla.tail8d86e.ts.net"
- name: immich
host: "photos.{{ caddy_domain }}"
backend: "https://photos.tail8d86e.ts.net"
# Layer 4 (TCP) services
# Format: { port: external_port, backend: "host:port" }

View file

@ -0,0 +1,25 @@
# Immich Storage - PersistentVolume and PVC for photo library
# Must be synced BEFORE the main immich app
#
# Prerequisites:
# 1. NFS share on sifaka at /volume1/photos with permissions for indri
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: immich-storage
namespace: argocd
spec:
project: default
source:
repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git
targetRevision: main
path: argocd/manifests/immich
# Only deploy kustomization resources (PV/PVC/Ingress), not values.yaml
directory:
include: "{kustomization.yaml,pv-nfs.yaml,pvc.yaml,ingress-tailscale.yaml}"
destination:
server: https://kubernetes.default.svc
namespace: immich
syncPolicy:
syncOptions:
- CreateNamespace=true

38
argocd/apps/immich.yaml Normal file
View file

@ -0,0 +1,38 @@
# Immich - Self-hosted photo and video management
# High-performance Google Photos/iCloud alternative with AI features
#
# Chart mirrored from https://github.com/immich-app/immich-charts to forge
#
# Prerequisites:
# 1. Mirror immich-charts to forge: https://github.com/immich-app/immich-charts
# 2. Create immich namespace and secrets:
# kubectl create namespace immich
# op inject -i argocd/manifests/immich/secret-db.yaml.tpl | kubectl apply -f -
# 3. Create immich-pg database and user (see immich-pg app)
# 4. Mount photos directory from indri to minikube
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: immich
namespace: argocd
spec:
project: default
sources:
# Helm chart from forge mirror (SSH via egress)
- repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/immich-charts.git
targetRevision: immich-0.10.0
path: charts/immich
helm:
releaseName: immich
valueFiles:
- $values/argocd/manifests/immich/values.yaml
# Values from our git repo
- repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git
targetRevision: main
ref: values
destination:
server: https://kubernetes.default.svc
namespace: immich
syncPolicy:
syncOptions:
- CreateNamespace=true

View file

@ -2,6 +2,13 @@
PostgreSQL clusters managed by CloudNativePG operator.
## Clusters
| Cluster | Image | Purpose |
|---------|-------|---------|
| blumeops-pg | cloudnative-pg/postgresql:18 | General services (miniflux, teslamate) |
| immich-pg | tensorchord/cloudnative-vectorchord:17 | Immich (requires pgvecto.rs extension) |
## blumeops-pg
Single-instance PostgreSQL cluster for blumeops services.
@ -99,3 +106,35 @@ from brew PostgreSQL (indri) to this k8s cluster. At that point:
1. Delete `service-tailscale.yaml` (the `k8s-pg` service)
2. Update/create a service with `tailscale.com/hostname: "pg"`
3. Verify the orphaned `k8s-pg` device is removed from tailnet
## immich-pg
PostgreSQL cluster for Immich with pgvecto.rs extension for AI-powered vector search.
### Configuration
- **Instances**: 1 (single-node for minikube)
- **Storage**: 10Gi on `standard` storage class
- **Image**: `tensorchord/cloudnative-vectorchord:17-v0.4.0` (includes pgvecto.rs)
- **Extensions**: `vectors`, `earthdistance`
### Connection
Immich connects via `immich-pg-rw.databases.svc.cluster.local:5432`.
The `immich` user password is auto-generated by CloudNativePG and stored in `immich-pg-app` secret:
```bash
# Get immich app credentials
kubectl -n databases get secret immich-pg-app -o jsonpath='{.data.password}' | base64 -d
```
### Status
```bash
# Check cluster health
kubectl -n databases get cluster immich-pg
# Check pods
kubectl -n databases get pods -l cnpg.io/cluster=immich-pg
```

View file

@ -0,0 +1,47 @@
# PostgreSQL Cluster for Immich
# Uses tensorchord/cloudnative-vectorchord for pgvecto.rs extension (required by Immich for AI features)
# Managed by CloudNativePG operator
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: immich-pg
namespace: databases
spec:
instances: 1
# vectorchord image includes pgvecto.rs extension for vector similarity search
imageName: tensorchord/cloudnative-vectorchord:17-v0.4.0
storage:
size: 10Gi
storageClass: standard
# Bootstrap creates initial database and owner
bootstrap:
initdb:
database: immich
owner: immich
postInitSQL:
- CREATE EXTENSION IF NOT EXISTS vectors;
- CREATE EXTENSION IF NOT EXISTS earthdistance CASCADE;
# Resource limits for minikube environment
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "1Gi"
cpu: "500m"
# PostgreSQL configuration
postgresql:
parameters:
max_connections: "50"
shared_buffers: "128MB"
password_encryption: "scram-sha-256"
# pgvecto.rs settings
shared_preload_libraries: "vectors.so"
pg_hba:
# Allow connections from k8s pods
- host all all 0.0.0.0/0 scram-sha-256
- host all all ::/0 scram-sha-256

View file

@ -5,5 +5,6 @@ namespace: databases
resources:
- blumeops-pg.yaml
- immich-pg.yaml
- service-tailscale.yaml
- service-metrics-tailscale.yaml

View file

@ -0,0 +1,98 @@
# Immich
Self-hosted photo and video management solution with AI-powered search and face recognition.
## Prerequisites
1. **NFS Share**: Create `/volume1/photos` on sifaka with NFS permissions for indri
2. **PostgreSQL**: The `immich-pg` cluster (with pgvecto.rs) must be healthy
3. **Secrets**: Create the database password secret
## Deployment Order
1. Sync `blumeops-pg` (to get CloudNativePG operator if not already running)
2. Sync `immich-storage` (creates PV, PVC, and Tailscale Ingress)
3. Wait for `immich-pg` cluster to be healthy
4. Create secrets (see below)
5. Sync `immich` (deploys the Helm chart)
6. Run `mise run provision-indri -- --tags caddy` to update Caddy config
## Secret Setup
```bash
# Create namespace
kubectl create namespace immich
# Get the auto-generated immich password from CloudNativePG
kubectl -n databases get secret immich-pg-app -o jsonpath='{.data.password}' | base64 -d
# Store that password in 1Password under blumeops/immich-pg, then:
op inject -i argocd/manifests/immich/secret-db.yaml.tpl | kubectl apply -f -
```
## Access
- **URL**: https://photos.ops.eblu.me (after Caddy is updated)
- **Tailscale**: https://photos.tail8d86e.ts.net (direct)
## First-Time Setup
1. Navigate to https://photos.ops.eblu.me
2. Create an admin account
3. Configure external library (optional - for importing existing photos)
## External Library (iCloud Photos)
To import existing photos from iCloud sync on indri:
1. In Immich Admin > External Libraries, create a new library
2. Set the import path to the location where iCloud photos sync
3. Configure scan schedule or trigger manual scan
## Architecture
```
┌─────────────────┐ ┌─────────────────┐
│ immich-server │────▶│ immich-pg │
│ (web/api) │ │ (PostgreSQL │
└────────┬────────┘ │ + pgvecto.rs) │
│ └─────────────────┘
┌────────▼────────┐ ┌─────────────────┐
│ immich-ml │ │ valkey │
│ (ML inference) │ │ (Redis cache) │
└─────────────────┘ └─────────────────┘
┌────────▼────────┐
│ sifaka NFS │
│ /volume1/photos│
└─────────────────┘
```
## Helm Values
The Helm chart is configured via `values.yaml`. Key settings:
- `image.tag`: Immich version (update manually)
- `immich.persistence.library.existingClaim`: Points to `immich-library` PVC
- `machine-learning.enabled`: AI features for face/object recognition
- `valkey.enabled`: Redis cache included in chart
## Troubleshooting
```bash
# Check pods
kubectl -n immich get pods
# Check immich-pg cluster
kubectl -n databases get cluster immich-pg
# View server logs
kubectl -n immich logs -l app.kubernetes.io/name=immich-server
# View ML logs
kubectl -n immich logs -l app.kubernetes.io/name=immich-machine-learning
# Check PVC binding
kubectl -n immich get pvc
```

View file

@ -0,0 +1,26 @@
# Tailscale Ingress for Immich
# Exposes Immich at photos.tail8d86e.ts.net
# Caddy will proxy photos.ops.eblu.me to this endpoint
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: immich-tailscale
namespace: immich
annotations:
tailscale.com/funnel: "false"
spec:
ingressClassName: tailscale
rules:
- host: photos
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: immich-server
port:
number: 2283
tls:
- hosts:
- photos

View file

@ -0,0 +1,11 @@
# Immich non-Helm resources (storage)
# These must be deployed before the Helm chart
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: immich
resources:
- pv-nfs.yaml
- pvc.yaml
- ingress-tailscale.yaml

View file

@ -0,0 +1,22 @@
# NFS PersistentVolume for Immich photo library
# Requires: NFS share on sifaka at /volume1/photos with NFS permissions for indri
#
# To create on Synology:
# 1. Control Panel > Shared Folder > Create
# 2. Name: photos, Location: Volume 1
# 3. Control Panel > File Services > NFS > NFS Rules
# 4. Add rule for "photos" share: Hostname=indri, Privilege=Read/Write, Squash=No mapping
apiVersion: v1
kind: PersistentVolume
metadata:
name: immich-library-nfs-pv
spec:
capacity:
storage: 2Ti
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
storageClassName: ""
nfs:
server: sifaka
path: /volume1/photos

View file

@ -0,0 +1,15 @@
# PersistentVolumeClaim for Immich photo library
# Binds to the NFS PV for sifaka:/volume1/photos
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: immich-library
namespace: immich
spec:
accessModes:
- ReadWriteMany
storageClassName: ""
volumeName: immich-library-nfs-pv
resources:
requests:
storage: 2Ti

View file

@ -0,0 +1,12 @@
# Immich database password secret
# Apply with: op inject -i argocd/manifests/immich/secret-db.yaml.tpl | kubectl apply -f -
apiVersion: v1
kind: Secret
metadata:
name: immich-db
namespace: immich
type: Opaque
stringData:
# Password is auto-generated by CloudNativePG and stored in immich-pg-app secret
# Retrieve with: kubectl -n databases get secret immich-pg-app -o jsonpath='{.data.password}' | base64 -d
password: "{{ op://blumeops/immich-pg/password }}"

View file

@ -0,0 +1,64 @@
# Immich Helm values for blumeops
# Chart: https://github.com/immich-app/immich-charts
#
# Immich requires:
# - PostgreSQL with pgvecto.rs extension (separate immich-pg cluster)
# - Redis/Valkey (included in chart)
# - Library storage PVC (photos directory from indri)
# Image version - explicitly set to avoid drift
image:
tag: v1.125.7
# Environment variables for all components
env:
TZ: "America/Los_Angeles"
# Database connection - uses immich-pg cluster
DB_HOSTNAME: "immich-pg-rw.databases.svc.cluster.local"
DB_PORT: "5432"
DB_DATABASE_NAME: "immich"
DB_USERNAME: "immich"
# Password injected from secret
DB_PASSWORD:
valueFrom:
secretKeyRef:
name: immich-db
key: password
# Immich server configuration
immich:
persistence:
library:
existingClaim: immich-library
# Machine Learning service
machine-learning:
enabled: true
persistence:
cache:
type: persistentVolumeClaim
accessMode: ReadWriteOnce
size: 10Gi
resources:
requests:
memory: "512Mi"
cpu: "100m"
limits:
memory: "4Gi"
cpu: "2000m"
# Valkey (Redis fork) - included in chart
valkey:
enabled: true
persistence:
size: 1Gi
# Server resources for minikube
server:
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "2Gi"
cpu: "2000m"