diff --git a/ansible/roles/caddy/defaults/main.yml b/ansible/roles/caddy/defaults/main.yml index 08eb341..105b139 100644 --- a/ansible/roles/caddy/defaults/main.yml +++ b/ansible/roles/caddy/defaults/main.yml @@ -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" } diff --git a/argocd/apps/immich-storage.yaml b/argocd/apps/immich-storage.yaml new file mode 100644 index 0000000..718b4c7 --- /dev/null +++ b/argocd/apps/immich-storage.yaml @@ -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 diff --git a/argocd/apps/immich.yaml b/argocd/apps/immich.yaml new file mode 100644 index 0000000..54d0789 --- /dev/null +++ b/argocd/apps/immich.yaml @@ -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 diff --git a/argocd/manifests/databases/README.md b/argocd/manifests/databases/README.md index c82f4d1..02bd792 100644 --- a/argocd/manifests/databases/README.md +++ b/argocd/manifests/databases/README.md @@ -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 +``` diff --git a/argocd/manifests/databases/immich-pg.yaml b/argocd/manifests/databases/immich-pg.yaml new file mode 100644 index 0000000..d6e40d6 --- /dev/null +++ b/argocd/manifests/databases/immich-pg.yaml @@ -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 diff --git a/argocd/manifests/databases/kustomization.yaml b/argocd/manifests/databases/kustomization.yaml index e44bdaf..e2eaa0c 100644 --- a/argocd/manifests/databases/kustomization.yaml +++ b/argocd/manifests/databases/kustomization.yaml @@ -5,5 +5,6 @@ namespace: databases resources: - blumeops-pg.yaml + - immich-pg.yaml - service-tailscale.yaml - service-metrics-tailscale.yaml diff --git a/argocd/manifests/immich/README.md b/argocd/manifests/immich/README.md new file mode 100644 index 0000000..0c1bfef --- /dev/null +++ b/argocd/manifests/immich/README.md @@ -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 +``` diff --git a/argocd/manifests/immich/ingress-tailscale.yaml b/argocd/manifests/immich/ingress-tailscale.yaml new file mode 100644 index 0000000..007fb6c --- /dev/null +++ b/argocd/manifests/immich/ingress-tailscale.yaml @@ -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 diff --git a/argocd/manifests/immich/kustomization.yaml b/argocd/manifests/immich/kustomization.yaml new file mode 100644 index 0000000..1c1c6d8 --- /dev/null +++ b/argocd/manifests/immich/kustomization.yaml @@ -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 diff --git a/argocd/manifests/immich/pv-nfs.yaml b/argocd/manifests/immich/pv-nfs.yaml new file mode 100644 index 0000000..0bd6ee2 --- /dev/null +++ b/argocd/manifests/immich/pv-nfs.yaml @@ -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 diff --git a/argocd/manifests/immich/pvc.yaml b/argocd/manifests/immich/pvc.yaml new file mode 100644 index 0000000..c764636 --- /dev/null +++ b/argocd/manifests/immich/pvc.yaml @@ -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 diff --git a/argocd/manifests/immich/secret-db.yaml.tpl b/argocd/manifests/immich/secret-db.yaml.tpl new file mode 100644 index 0000000..cec6328 --- /dev/null +++ b/argocd/manifests/immich/secret-db.yaml.tpl @@ -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 }}" diff --git a/argocd/manifests/immich/values.yaml b/argocd/manifests/immich/values.yaml new file mode 100644 index 0000000..16c9c49 --- /dev/null +++ b/argocd/manifests/immich/values.yaml @@ -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"