From 8621996343c943be66540766483b02b36aa199e8 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Mon, 26 Jan 2026 11:20:11 -0800 Subject: [PATCH] Add Immich photo management + migrate forge URLs (#62) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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 --- CLAUDE.md | 18 ++-- ansible/roles/caddy/defaults/main.yml | 3 + argocd/apps/alloy-k8s.yaml | 2 +- argocd/apps/apps.yaml | 2 +- argocd/apps/argocd.yaml | 2 +- argocd/apps/blumeops-pg.yaml | 2 +- argocd/apps/cloudnative-pg.yaml | 4 +- argocd/apps/devpi.yaml | 2 +- argocd/apps/grafana-config.yaml | 2 +- argocd/apps/grafana.yaml | 4 +- argocd/apps/immich-storage.yaml | 25 +++++ argocd/apps/immich.yaml | 38 +++++++ argocd/apps/kiwix.yaml | 2 +- argocd/apps/kube-state-metrics.yaml | 2 +- argocd/apps/loki.yaml | 2 +- argocd/apps/miniflux.yaml | 2 +- argocd/apps/prometheus.yaml | 2 +- argocd/apps/tailscale-operator.yaml | 2 +- argocd/apps/teslamate.yaml | 2 +- argocd/apps/torrent.yaml | 2 +- argocd/manifests/argocd/README.md | 4 +- .../argocd/argocd-ssh-known-hosts-cm.yaml | 8 +- .../argocd/repo-forge-secret.yaml.tpl | 4 +- argocd/manifests/databases/README.md | 39 ++++++++ argocd/manifests/databases/immich-pg.yaml | 54 ++++++++++ argocd/manifests/databases/kustomization.yaml | 1 + argocd/manifests/immich/README.md | 98 +++++++++++++++++++ .../manifests/immich/ingress-tailscale.yaml | 26 +++++ argocd/manifests/immich/kustomization.yaml | 11 +++ argocd/manifests/immich/pv-nfs.yaml | 22 +++++ argocd/manifests/immich/pvc.yaml | 15 +++ argocd/manifests/immich/secret-db.yaml.tpl | 12 +++ argocd/manifests/immich/values.yaml | 71 ++++++++++++++ 33 files changed, 451 insertions(+), 34 deletions(-) create mode 100644 argocd/apps/immich-storage.yaml create mode 100644 argocd/apps/immich.yaml create mode 100644 argocd/manifests/databases/immich-pg.yaml create mode 100644 argocd/manifests/immich/README.md create mode 100644 argocd/manifests/immich/ingress-tailscale.yaml create mode 100644 argocd/manifests/immich/kustomization.yaml create mode 100644 argocd/manifests/immich/pv-nfs.yaml create mode 100644 argocd/manifests/immich/pvc.yaml create mode 100644 argocd/manifests/immich/secret-db.yaml.tpl create mode 100644 argocd/manifests/immich/values.yaml diff --git a/CLAUDE.md b/CLAUDE.md index 793ad55..5cc36aa 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 ``` 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 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/alloy-k8s.yaml b/argocd/apps/alloy-k8s.yaml index 29d996c..9b652bc 100644 --- a/argocd/apps/alloy-k8s.yaml +++ b/argocd/apps/alloy-k8s.yaml @@ -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: diff --git a/argocd/apps/apps.yaml b/argocd/apps/apps.yaml index c028062..0eebe54 100644 --- a/argocd/apps/apps.yaml +++ b/argocd/apps/apps.yaml @@ -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: diff --git a/argocd/apps/argocd.yaml b/argocd/apps/argocd.yaml index f056ef0..c5e89e8 100644 --- a/argocd/apps/argocd.yaml +++ b/argocd/apps/argocd.yaml @@ -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: diff --git a/argocd/apps/blumeops-pg.yaml b/argocd/apps/blumeops-pg.yaml index 54f20c5..6a9e57e 100644 --- a/argocd/apps/blumeops-pg.yaml +++ b/argocd/apps/blumeops-pg.yaml @@ -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: diff --git a/argocd/apps/cloudnative-pg.yaml b/argocd/apps/cloudnative-pg.yaml index d2f6e81..73c3bf0 100644 --- a/argocd/apps/cloudnative-pg.yaml +++ b/argocd/apps/cloudnative-pg.yaml @@ -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: diff --git a/argocd/apps/devpi.yaml b/argocd/apps/devpi.yaml index e294f5b..4a15672 100644 --- a/argocd/apps/devpi.yaml +++ b/argocd/apps/devpi.yaml @@ -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: diff --git a/argocd/apps/grafana-config.yaml b/argocd/apps/grafana-config.yaml index e363933..f98399c 100644 --- a/argocd/apps/grafana-config.yaml +++ b/argocd/apps/grafana-config.yaml @@ -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: diff --git a/argocd/apps/grafana.yaml b/argocd/apps/grafana.yaml index 1a748d8..ec9262e 100644 --- a/argocd/apps/grafana.yaml +++ b/argocd/apps/grafana.yaml @@ -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: diff --git a/argocd/apps/immich-storage.yaml b/argocd/apps/immich-storage.yaml new file mode 100644 index 0000000..7227681 --- /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 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 diff --git a/argocd/apps/immich.yaml b/argocd/apps/immich.yaml new file mode 100644 index 0000000..b0597c3 --- /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.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 diff --git a/argocd/apps/kiwix.yaml b/argocd/apps/kiwix.yaml index 70be2c1..36e5b93 100644 --- a/argocd/apps/kiwix.yaml +++ b/argocd/apps/kiwix.yaml @@ -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: diff --git a/argocd/apps/kube-state-metrics.yaml b/argocd/apps/kube-state-metrics.yaml index 91df2cd..1644532 100644 --- a/argocd/apps/kube-state-metrics.yaml +++ b/argocd/apps/kube-state-metrics.yaml @@ -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: diff --git a/argocd/apps/loki.yaml b/argocd/apps/loki.yaml index cb9dd41..834c86c 100644 --- a/argocd/apps/loki.yaml +++ b/argocd/apps/loki.yaml @@ -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: diff --git a/argocd/apps/miniflux.yaml b/argocd/apps/miniflux.yaml index 36cff8d..d9165bb 100644 --- a/argocd/apps/miniflux.yaml +++ b/argocd/apps/miniflux.yaml @@ -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: diff --git a/argocd/apps/prometheus.yaml b/argocd/apps/prometheus.yaml index b53a243..3348736 100644 --- a/argocd/apps/prometheus.yaml +++ b/argocd/apps/prometheus.yaml @@ -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: diff --git a/argocd/apps/tailscale-operator.yaml b/argocd/apps/tailscale-operator.yaml index e3cc2c8..4ca5ea7 100644 --- a/argocd/apps/tailscale-operator.yaml +++ b/argocd/apps/tailscale-operator.yaml @@ -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: diff --git a/argocd/apps/teslamate.yaml b/argocd/apps/teslamate.yaml index 9c22c42..6165b8e 100644 --- a/argocd/apps/teslamate.yaml +++ b/argocd/apps/teslamate.yaml @@ -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: diff --git a/argocd/apps/torrent.yaml b/argocd/apps/torrent.yaml index 91e5fdc..7fd4135 100644 --- a/argocd/apps/torrent.yaml +++ b/argocd/apps/torrent.yaml @@ -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: diff --git a/argocd/manifests/argocd/README.md b/argocd/manifests/argocd/README.md index 42762df..344b1e2 100644 --- a/argocd/manifests/argocd/README.md +++ b/argocd/manifests/argocd/README.md @@ -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: diff --git a/argocd/manifests/argocd/argocd-ssh-known-hosts-cm.yaml b/argocd/manifests/argocd/argocd-ssh-known-hosts-cm.yaml index 61525aa..cf3b728 100644 --- a/argocd/manifests/argocd/argocd-ssh-known-hosts-cm.yaml +++ b/argocd/manifests/argocd/argocd-ssh-known-hosts-cm.yaml @@ -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== diff --git a/argocd/manifests/argocd/repo-forge-secret.yaml.tpl b/argocd/manifests/argocd/repo-forge-secret.yaml.tpl index e72b037..9d6187e 100644 --- a/argocd/manifests/argocd/repo-forge-secret.yaml.tpl +++ b/argocd/manifests/argocd/repo-forge-secret.yaml.tpl @@ -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 diff --git a/argocd/manifests/databases/README.md b/argocd/manifests/databases/README.md index c82f4d1..b5794d2 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 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 +``` diff --git a/argocd/manifests/databases/immich-pg.yaml b/argocd/manifests/databases/immich-pg.yaml new file mode 100644 index 0000000..ec60387 --- /dev/null +++ b/argocd/manifests/databases/immich-pg.yaml @@ -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 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..0cb4ecf --- /dev/null +++ b/argocd/manifests/immich/values.yaml @@ -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"