C2(migrate-immich-to-ringtail): impl decommission minikube immich; add ringtail immich-pg tailscale service

GitOps decommission of immich + immich-pg on minikube:
- Delete argocd/apps/immich.yaml
- Delete argocd/manifests/immich/ entirely
- Delete argocd/manifests/databases/{immich-pg,external-secret-immich-borgmatic,service-immich-pg-tailscale}.yaml
- Remove those entries from databases/kustomization.yaml

Add ringtail-side immich-pg Tailscale LoadBalancer Service (hostname
"immich-pg") so borgmatic can keep using the same FQDN for nightly
backups. This claims the device name freed by deleting the minikube
service.

The ringtail manifest path stays as argocd/manifests/immich-ringtail/
and the ArgoCD app stays as immich-ringtail — renaming would force a
cascading delete + recreate, with a window where live resources
disappear.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erich Blume 2026-05-13 13:31:09 -07:00
commit 7573a72318
18 changed files with 6 additions and 581 deletions

View file

@ -1,30 +0,0 @@
# Immich - Self-hosted photo and video management
# High-performance Google Photos/iCloud alternative with AI features
#
# Kustomize manifests in argocd/manifests/immich/
# Components: server, machine-learning, valkey (Redis)
#
# Prerequisites:
# 1. Create immich namespace and secrets:
# kubectl create namespace immich
# kubectl --context=minikube-indri create secret generic immich-db -n immich \
# --from-literal=password="$(kubectl --context=minikube-indri -n databases get secret immich-pg-app -o jsonpath='{.data.password}' | base64 -d)"
# 2. Create immich-pg database and user (see immich-pg app)
# 3. NFS share on sifaka at /volume1/photos with read/write for indri
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: immich
namespace: argocd
spec:
project: default
source:
repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git
targetRevision: main
path: argocd/manifests/immich
destination:
server: https://kubernetes.default.svc
namespace: immich
syncPolicy:
syncOptions:
- CreateNamespace=true

View file

@ -6,3 +6,4 @@ namespace: databases
resources:
- immich-pg.yaml
- external-secret-immich-borgmatic.yaml
- service-immich-pg-tailscale.yaml

View file

@ -1,6 +1,8 @@
# Tailscale LoadBalancer for immich-pg PostgreSQL access
# Canonical hostname: immich-pg.tail8d86e.ts.net
# Caddy L4 proxies pg.ops.eblu.me:5433 → this service for borgmatic backups
# Tailscale LoadBalancer for immich-pg PostgreSQL access on ringtail.
# Canonical hostname: immich-pg.tail8d86e.ts.net (claimed from the
# minikube side after the minikube service was removed during the
# immich-to-ringtail migration). Borgmatic on indri uses this
# hostname for nightly backups.
apiVersion: v1
kind: Service
metadata:

View file

@ -1,29 +0,0 @@
# ExternalSecret for borgmatic backup user password on immich-pg cluster
#
# Reuses the same 1Password item as blumeops-pg-borgmatic.
# 1Password item: "borgmatic" in blumeops vault
# Field: "db-password"
#
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: immich-pg-borgmatic
namespace: databases
spec:
refreshInterval: 1h
secretStoreRef:
kind: ClusterSecretStore
name: onepassword-blumeops
target:
name: immich-pg-borgmatic
creationPolicy: Owner
template:
type: kubernetes.io/basic-auth
data:
username: borgmatic
password: "{{ .password }}"
data:
- secretKey: password
remoteRef:
key: borgmatic
property: db-password

View file

@ -1,69 +0,0 @@
# 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;
# Managed roles
# Note: connectionLimit, ensure, inherit are CNPG defaults added to prevent ArgoCD drift
managed:
roles:
# borgmatic read-only user for backups
- name: borgmatic
login: true
connectionLimit: -1
ensure: present
inherit: true
inRoles:
- pg_read_all_data
passwordSecret:
name: immich-pg-borgmatic
# 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,13 +5,10 @@ namespace: databases
resources:
- blumeops-pg.yaml
- immich-pg.yaml
- service-tailscale.yaml
- service-immich-pg-tailscale.yaml
- service-metrics-tailscale.yaml
- external-secret-eblume.yaml
- external-secret-borgmatic.yaml
- external-secret-immich-borgmatic.yaml
- external-secret-teslamate.yaml
- external-secret-authentik.yaml
- external-secret-paperless.yaml

View file

@ -1,115 +0,0 @@
# 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. Wait for `immich-pg` cluster to be healthy
3. Create secrets (see below)
4. Sync `immich` (deploys all resources: storage, services, deployments)
5. Run `mise run provision-indri -- --tags caddy` to update Caddy config
## Components
| Component | Deployment | Service | Port |
|-----------|------------|---------|------|
| Server (web/API) | `immich-server` | `immich-server` | 2283 |
| Machine Learning | `immich-machine-learning` | `immich-machine-learning` | 3003 |
| Valkey (Redis) | `immich-valkey` | `immich-valkey` | 6379 |
## Secret Setup
The `immich-db` secret contains the database password, which is auto-generated by CloudNativePG
in the `immich-pg-app` secret. To create or regenerate the secret:
```bash
# Create namespace if needed
kubectl --context=minikube-indri create namespace immich
# Copy password from CNPG secret to immich namespace
kubectl --context=minikube-indri create secret generic immich-db -n immich \
--from-literal=password="$(kubectl --context=minikube-indri -n databases get secret immich-pg-app -o jsonpath='{.data.password}' | base64 -d)"
```
Note: This secret is not managed by ExternalSecrets since the source of truth is the CNPG-generated secret.
## 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│
└─────────────────┘
```
## Version Management
Image versions are controlled via `kustomization.yaml`:
```yaml
images:
- name: ghcr.io/immich-app/immich-server
newTag: v2.6.3
- name: ghcr.io/immich-app/immich-machine-learning
newTag: v2.6.3
- name: docker.io/valkey/valkey
newTag: "8.1-alpine"
```
To upgrade, update `newTag` values and sync via ArgoCD.
## Troubleshooting
```bash
# Check pods
kubectl --context=minikube-indri -n immich get pods
# Check immich-pg cluster
kubectl --context=minikube-indri -n databases get cluster immich-pg
# View server logs
kubectl --context=minikube-indri -n immich logs -l app=immich,component=server
# View ML logs
kubectl --context=minikube-indri -n immich logs -l app=immich,component=machine-learning
# Check PVC binding
kubectl --context=minikube-indri -n immich get pvc
```

View file

@ -1,63 +0,0 @@
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: immich-machine-learning
namespace: immich
spec:
replicas: 1
selector:
matchLabels:
app: immich
component: machine-learning
template:
metadata:
labels:
app: immich
component: machine-learning
spec:
securityContext:
seccompProfile:
type: RuntimeDefault
containers:
- name: machine-learning
image: ghcr.io/immich-app/immich-machine-learning:kustomized
ports:
- name: http
containerPort: 3003
env:
- name: TZ
value: "America/Los_Angeles"
- name: TRANSFORMERS_CACHE
value: /cache
- name: HF_XET_CACHE
value: /cache/huggingface-xet
- name: MPLCONFIGDIR
value: /cache/matplotlib-config
volumeMounts:
- name: cache
mountPath: /cache
livenessProbe:
httpGet:
path: /ping
port: 3003
initialDelaySeconds: 30
periodSeconds: 30
timeoutSeconds: 5
readinessProbe:
httpGet:
path: /ping
port: 3003
initialDelaySeconds: 15
periodSeconds: 10
timeoutSeconds: 5
resources:
requests:
memory: "512Mi"
cpu: "100m"
limits:
memory: "4Gi"
volumes:
- name: cache
persistentVolumeClaim:
claimName: immich-ml-cache

View file

@ -1,74 +0,0 @@
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: immich-server
namespace: immich
spec:
replicas: 1
selector:
matchLabels:
app: immich
component: server
template:
metadata:
labels:
app: immich
component: server
spec:
securityContext:
seccompProfile:
type: RuntimeDefault
containers:
- name: server
image: ghcr.io/immich-app/immich-server:kustomized
ports:
- name: http
containerPort: 2283
env:
- name: TZ
value: "America/Los_Angeles"
- name: DB_HOSTNAME
value: "immich-pg-rw.databases.svc.cluster.local"
- name: DB_PORT
value: "5432"
- name: DB_DATABASE_NAME
value: "immich"
- name: DB_USERNAME
value: "immich"
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: immich-db
key: password
- name: REDIS_HOSTNAME
value: immich-valkey
- name: IMMICH_MACHINE_LEARNING_URL
value: "http://immich-machine-learning:3003"
volumeMounts:
- name: library
mountPath: /usr/src/app/upload
livenessProbe:
httpGet:
path: /api/server/ping
port: 2283
initialDelaySeconds: 30
periodSeconds: 30
timeoutSeconds: 5
readinessProbe:
httpGet:
path: /api/server/ping
port: 2283
initialDelaySeconds: 15
periodSeconds: 10
timeoutSeconds: 5
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "2Gi"
volumes:
- name: library
persistentVolumeClaim:
claimName: immich-library

View file

@ -1,42 +0,0 @@
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: immich-valkey
namespace: immich
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: immich
component: valkey
template:
metadata:
labels:
app: immich
component: valkey
spec:
securityContext:
seccompProfile:
type: RuntimeDefault
containers:
- name: valkey
image: docker.io/valkey/valkey:kustomized
ports:
- name: redis
containerPort: 6379
volumeMounts:
- name: data
mountPath: /data
resources:
requests:
memory: "64Mi"
cpu: "25m"
limits:
memory: "256Mi"
volumes:
- name: data
emptyDir:
sizeLimit: 1Gi

View file

@ -1,39 +0,0 @@
# 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"
tailscale.com/proxy-group: "ingress"
gethomepage.dev/enabled: "true"
gethomepage.dev/name: "Immich"
gethomepage.dev/group: "Content"
gethomepage.dev/icon: "immich.png"
gethomepage.dev/description: "Photo management"
gethomepage.dev/href: "https://photos.ops.eblu.me"
gethomepage.dev/pod-selector: "app=immich,component=server"
# TODO: Add Immich widget - requires API key from Account Settings > API Keys
# See: https://gethomepage.dev/widgets/services/immich/
# gethomepage.dev/widget.type: "immich"
# gethomepage.dev/widget.url: "https://photos.ops.eblu.me"
# gethomepage.dev/widget.key: "{{HOMEPAGE_VAR_IMMICH_API_KEY}}"
# gethomepage.dev/widget.version: "2"
spec:
ingressClassName: tailscale
rules:
- http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: immich-server
port:
number: 2283
tls:
- hosts:
- photos

View file

@ -1,23 +0,0 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: immich
resources:
- deployment-server.yaml
- deployment-ml.yaml
- deployment-valkey.yaml
- service.yaml
- service-ml.yaml
- service-valkey.yaml
- pvc-ml-cache.yaml
- pv-nfs.yaml
- pvc.yaml
- ingress-tailscale.yaml
images:
- name: ghcr.io/immich-app/immich-server
newTag: v2.6.3
- name: ghcr.io/immich-app/immich-machine-learning
newTag: v2.6.3
- name: docker.io/valkey/valkey
newName: registry.ops.eblu.me/blumeops/valkey
newTag: v8.1.6-r0-fabca04

View file

@ -1,22 +0,0 @@
# 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

@ -1,12 +0,0 @@
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: immich-ml-cache
namespace: immich
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi

View file

@ -1,15 +0,0 @@
# 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

@ -1,14 +0,0 @@
---
apiVersion: v1
kind: Service
metadata:
name: immich-machine-learning
namespace: immich
spec:
selector:
app: immich
component: machine-learning
ports:
- name: http
port: 3003
targetPort: 3003

View file

@ -1,14 +0,0 @@
---
apiVersion: v1
kind: Service
metadata:
name: immich-valkey
namespace: immich
spec:
selector:
app: immich
component: valkey
ports:
- name: redis
port: 6379
targetPort: 6379

View file

@ -1,14 +0,0 @@
---
apiVersion: v1
kind: Service
metadata:
name: immich-server
namespace: immich
spec:
selector:
app: immich
component: server
ports:
- name: http
port: 2283
targetPort: 2283