Add Immich photo management + migrate forge URLs (#62)

## Summary
- Migrate all ArgoCD app repo URLs from `indri.tail8d86e.ts.net:2200` to `forge.ops.eblu.me:2222`
- Add Immich self-hosted photo management service with:
  - Helm chart deployment via ArgoCD
  - PostgreSQL cluster with pgvecto.rs for AI vector search (immich-pg)
  - NFS storage on sifaka for photo library (2Ti)
  - Tailscale Ingress + Caddy proxy for `photos.ops.eblu.me`
  - Machine learning service for face/object recognition

## Deployment and Testing
- [x] Update ArgoCD repo-creds-forge secret with new URL (one-time manual step)
- [ ] Sync `apps` to pick up new applications
- [ ] Sync all existing apps to verify new forge URL works
- [ ] Sync `blumeops-pg` to deploy immich-pg cluster
- [ ] Wait for immich-pg to be healthy
- [ ] Create immich-db secret from auto-generated password
- [ ] Sync `immich-storage` (PV, PVC, Ingress)
- [ ] Sync `immich` (Helm chart)
- [ ] Run `mise run provision-indri -- --tags caddy` to add photos.ops.eblu.me
- [ ] Verify Immich UI is accessible

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/62
This commit is contained in:
Erich Blume 2026-01-26 11:20:11 -08:00
commit 8621996343
33 changed files with 451 additions and 34 deletions

View file

@ -10,9 +10,11 @@ blumeops is Erich Blume's GitOps repository for personal infrastructure manageme
## Rules
1. At the start of every session, even if the user asked to do something else, run `mise run zk-docs -- --style=header --color=never --decorations=always` in order to review the `blumeops` documentation in the zettelkasten (zk). zk lives at `~/code/personal/zk`, and is managed via obsidian-sync (not git).
1. **CRITICAL: Always use `--context=minikube-indri` with kubectl commands.** The user has work contexts configured that must never be touched. Every kubectl command must explicitly specify the context to prevent accidental operations against the wrong cluster.
2. When making any changes, start by making sure you're on the `main` git branch and up-to-date, and then create a feature branch. Commit often while working, and create a PR using:
2. At the start of every session, even if the user asked to do something else, run `mise run zk-docs -- --style=header --color=never --decorations=always` in order to review the `blumeops` documentation in the zettelkasten (zk). zk lives at `~/code/personal/zk`, and is managed via obsidian-sync (not git).
3. When making any changes, start by making sure you're on the `main` git branch and up-to-date, and then create a feature branch. Commit often while working, and create a PR using:
```fish
tea pr create --title "Description of change" --description "$(cat <<'EOF'
## Summary
@ -33,17 +35,17 @@ mise run pr-comments <pr_number>
```
Address each unresolved comment before proceeding. The user will resolve comments on the Forge UI as they are addressed.
3. Always keep the zk cards up to date with any changes, and suggest new links to new cards whenever appropriate. Refer back to the zk docs often during the process of planning and making corrections to ensure accuracy, and if you make a mistake, figure out a way to guard against it using the zk.
4. Always keep the zk cards up to date with any changes, and suggest new links to new cards whenever appropriate. Refer back to the zk docs often during the process of planning and making corrections to ensure accuracy, and if you make a mistake, figure out a way to guard against it using the zk.
4. Use `Brewfile` and `mise.toml` to install tools needed on the development workstation (typically hostnamed "gilbert", username "eblume").
5. Use `Brewfile` and `mise.toml` to install tools needed on the development workstation (typically hostnamed "gilbert", username "eblume").
5. Services are hosted either on indri directly (via ansible) or in Kubernetes (via ArgoCD). See the "Service Deployment" section below for details.
6. Services are hosted either on indri directly (via ansible) or in Kubernetes (via ArgoCD). See the "Service Deployment" section below for details.
6. Try to always test changes before applying them. Use syntax checkers, do dry runs (`--check --diff`), run commands manually via `ssh indri 'some command'`, etc.
7. Try to always test changes before applying them. Use syntax checkers, do dry runs (`--check --diff`), run commands manually via `ssh indri 'some command'`, etc.
7. **Wait for user review before deploying.** After creating a PR, do not run deployment commands until the user has had a chance to review the changes. The user will indicate when they're ready to deploy.
8. **Wait for user review before deploying.** After creating a PR, do not run deployment commands until the user has had a chance to review the changes. The user will indicate when they're ready to deploy.
8. After deploying changes, try to verify the result. Use `mise run indri-services-check` to do a general service health check.
9. After deploying changes, try to verify the result. Use `mise run indri-services-check` to do a general service health check.
## Project Structure

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

@ -6,7 +6,7 @@ metadata:
spec:
project: default
source:
repoURL: ssh://forgejo@indri.tail8d86e.ts.net:2200/eblume/blumeops.git
repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git
targetRevision: main
path: argocd/manifests/alloy-k8s
destination:

View file

@ -8,7 +8,7 @@ metadata:
spec:
project: default
source:
repoURL: ssh://forgejo@indri.tail8d86e.ts.net:2200/eblume/blumeops.git
repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git
targetRevision: main
path: argocd/apps
destination:

View file

@ -8,7 +8,7 @@ metadata:
spec:
project: default
source:
repoURL: ssh://forgejo@indri.tail8d86e.ts.net:2200/eblume/blumeops.git
repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git
targetRevision: main
path: argocd/manifests/argocd
destination:

View file

@ -12,7 +12,7 @@ metadata:
spec:
project: default
source:
repoURL: ssh://forgejo@indri.tail8d86e.ts.net:2200/eblume/blumeops.git
repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git
targetRevision: main
path: argocd/manifests/databases
destination:

View file

@ -11,7 +11,7 @@ spec:
project: default
sources:
# Helm chart from forge mirror (SSH via egress)
- repoURL: ssh://forgejo@indri.tail8d86e.ts.net:2200/eblume/cloudnative-pg-charts.git
- repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/cloudnative-pg-charts.git
targetRevision: cloudnative-pg-v0.27.0
path: charts/cloudnative-pg
helm:
@ -19,7 +19,7 @@ spec:
valueFiles:
- $values/argocd/manifests/cloudnative-pg/values.yaml
# Values from our git repo
- repoURL: ssh://forgejo@indri.tail8d86e.ts.net:2200/eblume/blumeops.git
- repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git
targetRevision: main
ref: values
destination:

View file

@ -18,7 +18,7 @@ metadata:
spec:
project: default
source:
repoURL: ssh://forgejo@indri.tail8d86e.ts.net:2200/eblume/blumeops.git
repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git
targetRevision: main
path: argocd/manifests/devpi
destination:

View file

@ -13,7 +13,7 @@ metadata:
spec:
project: default
source:
repoURL: ssh://forgejo@indri.tail8d86e.ts.net:2200/eblume/blumeops.git
repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git
targetRevision: main
path: argocd/manifests/grafana-config
destination:

View file

@ -14,7 +14,7 @@ spec:
project: default
sources:
# Helm chart from forge mirror (SSH via egress)
- repoURL: ssh://forgejo@indri.tail8d86e.ts.net:2200/eblume/grafana-helm-charts.git
- repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/grafana-helm-charts.git
targetRevision: grafana-8.8.2
path: charts/grafana
helm:
@ -22,7 +22,7 @@ spec:
valueFiles:
- $values/argocd/manifests/grafana/values.yaml
# Values from our git repo
- repoURL: ssh://forgejo@indri.tail8d86e.ts.net:2200/eblume/blumeops.git
- repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git
targetRevision: main
ref: values
destination:

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 storage resources (PV/PVC/Ingress), not Helm values.yaml
directory:
include: "{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.3
path: charts/immich
helm:
releaseName: immich
valueFiles:
- $values/argocd/manifests/immich/values.yaml
# Values from our git repo (use feature branch for testing, reset to main after merge)
- repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git
targetRevision: feature/immich
ref: values
destination:
server: https://kubernetes.default.svc
namespace: immich
syncPolicy:
syncOptions:
- CreateNamespace=true

View file

@ -7,7 +7,7 @@ metadata:
spec:
project: default
source:
repoURL: ssh://forgejo@indri.tail8d86e.ts.net:2200/eblume/blumeops.git
repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git
targetRevision: main
path: argocd/manifests/kiwix
destination:

View file

@ -6,7 +6,7 @@ metadata:
spec:
project: default
source:
repoURL: ssh://forgejo@indri.tail8d86e.ts.net:2200/eblume/blumeops.git
repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git
targetRevision: main
path: argocd/manifests/kube-state-metrics
destination:

View file

@ -6,7 +6,7 @@ metadata:
spec:
project: default
source:
repoURL: ssh://forgejo@indri.tail8d86e.ts.net:2200/eblume/blumeops.git
repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git
targetRevision: main
path: argocd/manifests/loki
destination:

View file

@ -16,7 +16,7 @@ metadata:
spec:
project: default
source:
repoURL: ssh://forgejo@indri.tail8d86e.ts.net:2200/eblume/blumeops.git
repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git
targetRevision: main
path: argocd/manifests/miniflux
destination:

View file

@ -6,7 +6,7 @@ metadata:
spec:
project: default
source:
repoURL: ssh://forgejo@indri.tail8d86e.ts.net:2200/eblume/blumeops.git
repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git
targetRevision: main
path: argocd/manifests/prometheus
destination:

View file

@ -14,7 +14,7 @@ spec:
jsonPointers:
- /spec/externalName
source:
repoURL: ssh://forgejo@indri.tail8d86e.ts.net:2200/eblume/blumeops.git
repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git
targetRevision: main
path: argocd/manifests/tailscale-operator
destination:

View file

@ -21,7 +21,7 @@ metadata:
spec:
project: default
source:
repoURL: ssh://forgejo@indri.tail8d86e.ts.net:2200/eblume/blumeops.git
repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git
targetRevision: main
path: argocd/manifests/teslamate
destination:

View file

@ -7,7 +7,7 @@ metadata:
spec:
project: default
source:
repoURL: ssh://forgejo@indri.tail8d86e.ts.net:2200/eblume/blumeops.git
repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git
targetRevision: main
path: argocd/manifests/torrent
destination:

View file

@ -32,7 +32,7 @@ argocd account update-password
PRIV_KEY=$(op read "op://vg6xf6vvfmoh5hqjjhlhbeoaie/csjncynh6htjvnh2l2da65y32q/private key?ssh-format=openssh")$'\n' && \
kubectl create secret generic repo-creds-forge -n argocd \
--from-literal=type=git \
--from-literal=url='ssh://forgejo@indri.tail8d86e.ts.net:2200/eblume/' \
--from-literal=url='ssh://forgejo@forge.ops.eblu.me:2222/eblume/' \
--from-literal=insecure=true \
--from-literal=sshPrivateKey="$PRIV_KEY" && \
kubectl label secret repo-creds-forge -n argocd argocd.argoproj.io/secret-type=repo-creds
@ -82,7 +82,7 @@ metadata:
spec:
project: default
source:
repoURL: ssh://forgejo@indri.tail8d86e.ts.net:2200/eblume/blumeops.git
repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git
targetRevision: main
path: argocd/manifests/my-app
destination:

View file

@ -1,5 +1,5 @@
# Patch to add forge (indri) SSH host key to ArgoCD known_hosts
# Includes upstream defaults plus indri.tail8d86e.ts.net:2200
# Patch to add forge SSH host key to ArgoCD known_hosts
# Includes upstream defaults plus forge.ops.eblu.me:2222
apiVersion: v1
kind: ConfigMap
metadata:
@ -21,5 +21,5 @@ data:
gitlab.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCsj2bNKTBSpIYDEGk9KxsGh3mySTRgMtXL583qmBpzeQ+jqCMRgBqB98u3z++J1sKlXHWfM9dyhSevkMwSbhoR8XIq/U0tCNyokEi/ueaBMCvbcTHhO7FcwzY92WK4Yt0aGROY5qX2UKSeOvuP4D6TPqKF1onrSzH9bx9XUf2lEdWT/ia1NEKjunUqu1xOB/StKDHMoX4/OKyIzuS0q/T1zOATthvasJFoPrAjkohTyaDUz2LN5JoH839hViyEG82yB+MjcFV5MU3N1l1QL3cVUCh93xSaua1N85qivl+siMkPGbO5xR/En4iEY6K2XPASUEMaieWVNTRCtJ4S8H+9
ssh.dev.azure.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC7Hr1oTWqNqOlzGJOfGJ4NakVyIzf1rXYd4d7wo6jBlkLvCA4odBlL0mDUyZ0/QUfTTqeu+tm22gOsv+VrVTMk6vwRU75gY/y9ut5Mb3bR5BV58dKXyq9A9UeB5Cakehn5Zgm6x1mKoVyf+FFn26iYqXJRgzIZZcZ5V6hrE0Qg39kZm4az48o0AUbf6Sp4SLdvnuMa2sVNwHBboS7EJkm57XQPVU3/QpyNLHbWDdzwtrlS+ez30S3AdYhLKEOxAG8weOnyrtLJAUen9mTkol8oII1edf7mWWbWVf0nBmly21+nZcmCTISQBtdcyPaEno7fFQMDD26/s0lfKob4Kw8H
vs-ssh.visualstudio.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC7Hr1oTWqNqOlzGJOfGJ4NakVyIzf1rXYd4d7wo6jBlkLvCA4odBlL0mDUyZ0/QUfTTqeu+tm22gOsv+VrVTMk6vwRU75gY/y9ut5Mb3bR5BV58dKXyq9A9UeB5Cakehn5Zgm6x1mKoVyf+FFn26iYqXJRgzIZZcZ5V6hrE0Qg39kZm4az48o0AUbf6Sp4SLdvnuMa2sVNwHBboS7EJkm57XQPVU3/QpyNLHbWDdzwtrlS+ez30S3AdYhLKEOxAG8weOnyrtLJAUen9mTkol8oII1edf7mWWbWVf0nBmly21+nZcmCTISQBtdcyPaEno7fFQMDD26/s0lfKob4Kw8H
# Forge (indri) - Forgejo SSH on port 2200
[indri.tail8d86e.ts.net]:2200 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDlGQT5w03XxlhmEiDVtGq2SkhLIZU4vYhdMey/T2tFLp7kEiOwCWgDgbBn12VDfqXTXJreykBuREqYNSx4tL4Znwap0+HjLOjTIVri8af2ZFF6IP52pcmJEOnxm/yUZhJCosu1wOZwLOoQEPBYM6sPN4OY9PFOsrsxMO2LWPJAZujPlnsfKOTsIS5iRpiT4yU7Z+oWB21rMxjZ9sXZRn8PI2MbUIs/Yazpah2XPJm2YJ7C+kqTLmld4mXQaQtHhzvPaRNB59RS8xyinuaRs618tD3DQq3Qpt8ZZKZydLVv4CIrGvjdqavt0l+4rsNGBh8dWvDR7l2Z6wo9ggDCej957+J6tInfZ82KHSW3ONdm2mUOHObUVSte2xUPlRpnIBFt3lcCapifPULE7PuN0Xdw4r+ewr+6R65RzdptqGfKyyAYsERhbq904ryNZ9fy30vH8+j9imL5AhMkCbP8S/UW49rDIdfN6MvZlX9MoBhmbrkv+kETB7qz9zaOrocEOZOE3fzB9iZxNwlXjstUnjkqi4P1yY/SKpyLC/yDCUpxC79FbCAKIJwar3C2mZaLeBGyqL31HPKOx175VsSxIbjeJX8uNO9WhbFPlcbRETeEoq+dczeU25OESCyyelGb72tTNJYObn2R8Br9NFPiwGZJX6TLlKqaE7x3D0M64ncTJQ==
# Forge - Forgejo SSH on port 2222
[forge.ops.eblu.me]:2222 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDlGQT5w03XxlhmEiDVtGq2SkhLIZU4vYhdMey/T2tFLp7kEiOwCWgDgbBn12VDfqXTXJreykBuREqYNSx4tL4Znwap0+HjLOjTIVri8af2ZFF6IP52pcmJEOnxm/yUZhJCosu1wOZwLOoQEPBYM6sPN4OY9PFOsrsxMO2LWPJAZujPlnsfKOTsIS5iRpiT4yU7Z+oWB21rMxjZ9sXZRn8PI2MbUIs/Yazpah2XPJm2YJ7C+kqTLmld4mXQaQtHhzvPaRNB59RS8xyinuaRs618tD3DQq3Qpt8ZZKZydLVv4CIrGvjdqavt0l+4rsNGBh8dWvDR7l2Z6wo9ggDCej957+J6tInfZ82KHSW3ONdm2mUOHObUVSte2xUPlRpnIBFt3lcCapifPULE7PuN0Xdw4r+ewr+6R65RzdptqGfKyyAYsERhbq904ryNZ9fy30vH8+j9imL5AhMkCbP8S/UW49rDIdfN6MvZlX9MoBhmbrkv+kETB7qz9zaOrocEOZOE3fzB9iZxNwlXjstUnjkqi4P1yY/SKpyLC/yDCUpxC79FbCAKIJwar3C2mZaLeBGyqL31HPKOx175VsSxIbjeJX8uNO9WhbFPlcbRETeEoq+dczeU25OESCyyelGb72tTNJYObn2R8Br9NFPiwGZJX6TLlKqaE7x3D0M64ncTJQ==

View file

@ -11,7 +11,7 @@
# PRIV_KEY=$(op read "op://vg6xf6vvfmoh5hqjjhlhbeoaie/csjncynh6htjvnh2l2da65y32q/private key?ssh-format=openssh")$'\n' && \
# kubectl create secret generic repo-creds-forge -n argocd \
# --from-literal=type=git \
# --from-literal=url='ssh://forgejo@indri.tail8d86e.ts.net:2200/eblume/' \
# --from-literal=url='ssh://forgejo@forge.ops.eblu.me:2222/eblume/' \
# --from-literal=insecure=true \
# --from-literal=sshPrivateKey="$PRIV_KEY" && \
# kubectl label secret repo-creds-forge -n argocd argocd.argoproj.io/secret-type=repo-creds
@ -25,7 +25,7 @@ metadata:
argocd.argoproj.io/secret-type: repo-creds
stringData:
type: git
url: ssh://forgejo@indri.tail8d86e.ts.net:2200/eblume/
url: ssh://forgejo@forge.ops.eblu.me:2222/eblume/
insecure: "true"
sshPrivateKey: |
# Key from 1Password: op://vg6xf6vvfmoh5hqjjhlhbeoaie/csjncynh6htjvnh2l2da65y32q/private key

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 VectorChord extension for AI-powered vector search.
### Configuration
- **Instances**: 1 (single-node for minikube)
- **Storage**: 10Gi on `standard` storage class
- **Image**: `ghcr.io/tensorchord/cloudnative-vectorchord:17-0.5.0` (VectorChord 0.5.0 for Immich compatibility)
- **Extensions**: `vector`, `vchord`, `cube`, `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,54 @@
# PostgreSQL Cluster for Immich
# Uses VectorChord (successor to pgvecto.rs) for AI-powered vector search
# See: https://github.com/immich-app/immich/discussions/9060
# Managed by CloudNativePG operator
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: immich-pg
namespace: databases
spec:
instances: 1
# VectorChord image for PostgreSQL 17 with VectorChord 0.5.0
# Immich v2.4.1 requires VectorChord >=0.3 <0.6
# See: https://github.com/tensorchord/VectorChord
imageName: ghcr.io/tensorchord/cloudnative-vectorchord:17-0.5.0
storage:
size: 10Gi
storageClass: standard
# Bootstrap creates initial database and owner
bootstrap:
initdb:
database: immich
owner: immich
postInitSQL:
# Extensions required by Immich
- CREATE EXTENSION IF NOT EXISTS vector;
- CREATE EXTENSION IF NOT EXISTS vchord CASCADE;
- CREATE EXTENSION IF NOT EXISTS cube CASCADE;
- 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:
# VectorChord requires vchord.so in shared_preload_libraries
shared_preload_libraries:
- "vchord.so"
parameters:
max_connections: "50"
shared_buffers: "128MB"
password_encryption: "scram-sha-256"
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,71 @@
# Immich Helm values for blumeops
# Chart: https://github.com/immich-app/immich-charts (v0.10.3)
#
# Immich requires:
# - PostgreSQL with VectorChord extension (separate immich-pg cluster)
# - Redis/Valkey (included in chart)
# - Library storage PVC (photos directory from sifaka NFS)
# Shared environment variables
env:
TZ: "America/Los_Angeles"
# Shared controller settings - image tag and DB connection
controllers:
main:
containers:
main:
image:
tag: v2.4.1
env:
DB_HOSTNAME: "immich-pg-rw.databases.svc.cluster.local"
DB_PORT: "5432"
DB_DATABASE_NAME: "immich"
DB_USERNAME: "immich"
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:
enabled: true
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:
data:
enabled: true
type: emptyDir
size: 1Gi
# Server resources for minikube
server:
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "2Gi"
cpu: "2000m"