From adf449d6baa41ea90b244314caaab1e008c3a207 Mon Sep 17 00:00:00 2001 From: 0xFEEDC0DE64 Date: Thu, 22 Oct 2026 10:35:11 +0200 Subject: [PATCH] Add Argo-managed Immich stack --- apps/immich-postgresql/cluster.yaml | 26 +++++ apps/immich-postgresql/kustomization.yaml | 4 + apps/immich/README.md | 33 ++++++ apps/immich/resources/kustomization.yaml | 4 + apps/immich/resources/library-storage.yaml | 45 ++++++++ apps/immich/values.yaml | 116 +++++++++++++++++++++ bootstrap/cnpg-operator-project.yaml | 40 +++++++ bootstrap/cnpg-operator.yaml | 26 +++++ bootstrap/immich-postgresql.yaml | 23 ++++ bootstrap/immich-project.yaml | 37 +++++++ bootstrap/immich.yaml | 31 ++++++ bootstrap/kustomization.yaml | 5 + bootstrap/platform-project.yaml | 1 + 13 files changed, 391 insertions(+) create mode 100644 apps/immich-postgresql/cluster.yaml create mode 100644 apps/immich-postgresql/kustomization.yaml create mode 100644 apps/immich/README.md create mode 100644 apps/immich/resources/kustomization.yaml create mode 100644 apps/immich/resources/library-storage.yaml create mode 100644 apps/immich/values.yaml create mode 100644 bootstrap/cnpg-operator-project.yaml create mode 100644 bootstrap/cnpg-operator.yaml create mode 100644 bootstrap/immich-postgresql.yaml create mode 100644 bootstrap/immich-project.yaml create mode 100644 bootstrap/immich.yaml diff --git a/apps/immich-postgresql/cluster.yaml b/apps/immich-postgresql/cluster.yaml new file mode 100644 index 0000000..dda0337 --- /dev/null +++ b/apps/immich-postgresql/cluster.yaml @@ -0,0 +1,26 @@ +apiVersion: postgresql.cnpg.io/v1 +kind: Cluster +metadata: + name: immich-postgresql + namespace: immich +spec: + instances: 3 + imageName: docker.io/tensorchord/cloudnative-vectorchord:18.6 + enableSuperuserAccess: true + affinity: + podAntiAffinityType: required + bootstrap: + initdb: + database: immich + owner: feedc0de + encoding: UTF8 + localeCollate: C + localeCType: C + dataChecksums: true + postInitApplicationSQL: + - ALTER ROLE feedc0de WITH SUPERUSER + storage: + size: 50Gi + storageClass: rook-ceph-block-ec + monitoring: + enablePodMonitor: true diff --git a/apps/immich-postgresql/kustomization.yaml b/apps/immich-postgresql/kustomization.yaml new file mode 100644 index 0000000..993fbde --- /dev/null +++ b/apps/immich-postgresql/kustomization.yaml @@ -0,0 +1,4 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - cluster.yaml diff --git a/apps/immich/README.md b/apps/immich/README.md new file mode 100644 index 0000000..60a62db --- /dev/null +++ b/apps/immich/README.md @@ -0,0 +1,33 @@ +# Immich on arschrock + +Argo CD installs the upstream Immich chart directly; there is no homelab +application/chart repository. The chart's upstream application release tag is +selected in `values.yaml`. The Immich server and machine-learning workload are +pinned to `arschrock`; only the existing `/mnt/Bilder` bcachefs library is +host-local. Immich PostgreSQL and Valkey run separately in this namespace with +Ceph-backed storage. + +The local PV is retained and bound to `/mnt/Bilder`. It does not copy or move +the photo library. The server has a read-only startup check for the bcachefs +filesystem magic and expected `upload` directory, in addition to a node label +that prevents scheduling until the host helper confirms the mount. + +Before unmounting `/mnt`, run `./immich-storage-offline.sh` from the workspace +root and wait for it to report that the server and machine-learning Pods have +stopped. Do not unmount if that helper fails. After mounting `/mnt` again, run +`./immich-storage-online.sh`; it validates bcachefs and the Immich upload +directory, forces fresh Pod mounts, and then restores the readiness label. +These helpers assume the default context `kubernetes-admin@kubernetes` and +refuse other contexts. No systemd hook controlling `/mnt` was found during +migration setup, so a future systemd-based mount change must explicitly order +these helpers before unmount and after mount. + +The database operator creates its application password Secret automatically; +the cleartext credential is not stored in Git. The initial restore preserves +the source database's PostgreSQL 18.6 and pgvector extension versions. The +shared Valkey instance is not copied wholesale: only keys identified as +Immich-owned are imported, leaving all other applications' keys untouched. + +The old Podman stack and host PostgreSQL must remain available as rollback +sources until the new application, database, monitoring, and routes have been +verified. Never run both Immich servers against `/mnt/Bilder` concurrently. diff --git a/apps/immich/resources/kustomization.yaml b/apps/immich/resources/kustomization.yaml new file mode 100644 index 0000000..683a977 --- /dev/null +++ b/apps/immich/resources/kustomization.yaml @@ -0,0 +1,4 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - library-storage.yaml diff --git a/apps/immich/resources/library-storage.yaml b/apps/immich/resources/library-storage.yaml new file mode 100644 index 0000000..d2d0e17 --- /dev/null +++ b/apps/immich/resources/library-storage.yaml @@ -0,0 +1,45 @@ +apiVersion: v1 +kind: PersistentVolume +metadata: + name: immich-library + annotations: + argocd.argoproj.io/sync-wave: "-2" + labels: + immich.brunner.ninja/library: "true" +spec: + capacity: + storage: 10Ti + volumeMode: Filesystem + accessModes: + - ReadWriteOnce + persistentVolumeReclaimPolicy: Retain + storageClassName: "" + local: + path: /mnt/Bilder + nodeAffinity: + required: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: + - arschrock +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: immich-library + namespace: immich + annotations: + argocd.argoproj.io/sync-wave: "-1" +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Ti + storageClassName: "" + volumeName: immich-library + selector: + matchLabels: + immich.brunner.ninja/library: "true" diff --git a/apps/immich/values.yaml b/apps/immich/values.yaml new file mode 100644 index 0000000..902fc3d --- /dev/null +++ b/apps/immich/values.yaml @@ -0,0 +1,116 @@ +controllers: + main: + containers: + main: + image: + # Upstream chart 0.13.4 still defaults to v3.2.0; keep the running + # application at v3.3.1 until the chart default catches up. + tag: v3.3.1 + +immich: + metrics: + enabled: true + persistence: + library: + existingClaim: immich-library + +server: + controllers: + main: + pod: + nodeSelector: + kubernetes.io/hostname: arschrock + immich.brunner.ninja/library-ready: "true" + initContainers: + check-bcachefs-library: + image: + repository: docker.io/library/alpine + tag: 3.22.2 + pullPolicy: IfNotPresent + command: + - /bin/sh + - -ec + - test "$(stat -f -c %t /usr/src/app/upload)" = ca451a4e && test -d /usr/src/app/upload/upload + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 65534 + runAsGroup: 65534 + containers: + main: + env: + DB_HOSTNAME: immich-postgresql-rw + DB_DATABASE_NAME: immich + DB_VECTOR_EXTENSION: pgvector + DB_USERNAME: + valueFrom: + secretKeyRef: + name: immich-postgresql-app + key: username + DB_PASSWORD: + valueFrom: + secretKeyRef: + name: immich-postgresql-app + key: password + ingress: + main: + enabled: false + persistence: + data: + existingClaim: '{{ .Values.immich.persistence.library.existingClaim }}' + advancedMounts: + main: + main: + - path: /usr/src/app/upload + check-bcachefs-library: + - path: /usr/src/app/upload + readOnly: true + +machine-learning: + controllers: + main: + pod: + nodeSelector: + kubernetes.io/hostname: arschrock + immich.brunner.ninja/library-ready: "true" + initContainers: + check-bcachefs-library: + image: + repository: docker.io/library/alpine + tag: 3.22.2 + pullPolicy: IfNotPresent + command: + - /bin/sh + - -ec + - test "$(stat -f -c %t /usr/src/app/upload)" = ca451a4e && test -d /usr/src/app/upload/upload + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 65534 + runAsGroup: 65534 + persistence: + library: + existingClaim: '{{ .Values.immich.persistence.library.existingClaim }}' + advancedMounts: + main: + check-bcachefs-library: + - path: /usr/src/app/upload + readOnly: true + +valkey: + enabled: true + persistence: + data: + enabled: true + type: persistentVolumeClaim + accessMode: ReadWriteOnce + size: 2Gi + storageClass: rook-ceph-block-ec diff --git a/bootstrap/cnpg-operator-project.yaml b/bootstrap/cnpg-operator-project.yaml new file mode 100644 index 0000000..2aea75d --- /dev/null +++ b/bootstrap/cnpg-operator-project.yaml @@ -0,0 +1,40 @@ +apiVersion: argoproj.io/v1alpha1 +kind: AppProject +metadata: + name: cnpg-operator + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "-3" +spec: + description: CloudNativePG operator and its required cluster-scoped resources + sourceRepos: + - ghcr.io/cloudnative-pg/charts + destinations: + - namespace: cnpg-system + server: https://kubernetes.default.svc + clusterResourceWhitelist: + - group: apiextensions.k8s.io + kind: CustomResourceDefinition + - group: rbac.authorization.k8s.io + kind: ClusterRole + - group: rbac.authorization.k8s.io + kind: ClusterRoleBinding + - group: admissionregistration.k8s.io + kind: MutatingWebhookConfiguration + - group: admissionregistration.k8s.io + kind: ValidatingWebhookConfiguration + namespaceResourceWhitelist: + - group: "" + kind: ConfigMap + - group: "" + kind: Secret + - group: "" + kind: Service + - group: "" + kind: ServiceAccount + - group: apps + kind: Deployment + - group: rbac.authorization.k8s.io + kind: Role + - group: rbac.authorization.k8s.io + kind: RoleBinding diff --git a/bootstrap/cnpg-operator.yaml b/bootstrap/cnpg-operator.yaml new file mode 100644 index 0000000..99e4525 --- /dev/null +++ b/bootstrap/cnpg-operator.yaml @@ -0,0 +1,26 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: cnpg-operator + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "-2" +spec: + project: cnpg-operator + source: + repoURL: ghcr.io/cloudnative-pg/charts + chart: cloudnative-pg + targetRevision: 0.29.1 + helm: + releaseName: cnpg-operator + destination: + server: https://kubernetes.default.svc + namespace: cnpg-system + syncPolicy: + automated: + enabled: true + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/bootstrap/immich-postgresql.yaml b/bootstrap/immich-postgresql.yaml new file mode 100644 index 0000000..4ca83e5 --- /dev/null +++ b/bootstrap/immich-postgresql.yaml @@ -0,0 +1,23 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: immich-postgresql + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "-1" +spec: + project: immich + source: + repoURL: https://code.brunner.ninja/feedc0de/homelab-gitops.git + targetRevision: main + path: apps/immich-postgresql + destination: + server: https://kubernetes.default.svc + namespace: immich + syncPolicy: + automated: + enabled: true + prune: false + selfHeal: true + syncOptions: + - CreateNamespace=true diff --git a/bootstrap/immich-project.yaml b/bootstrap/immich-project.yaml new file mode 100644 index 0000000..642fe19 --- /dev/null +++ b/bootstrap/immich-project.yaml @@ -0,0 +1,37 @@ +apiVersion: argoproj.io/v1alpha1 +kind: AppProject +metadata: + name: immich + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "-3" +spec: + description: Immich, its database, and host-local library binding + sourceRepos: + - https://code.brunner.ninja/feedc0de/homelab-gitops.git + - ghcr.io/immich-app/immich-charts + destinations: + - namespace: immich + server: https://kubernetes.default.svc + clusterResourceWhitelist: + - group: "" + kind: PersistentVolume + namespaceResourceWhitelist: + - group: "" + kind: ConfigMap + - group: "" + kind: PersistentVolumeClaim + - group: "" + kind: Service + - group: "" + kind: ServiceAccount + - group: apps + kind: Deployment + - group: networking.k8s.io + kind: Ingress + - group: monitoring.coreos.com + kind: ServiceMonitor + - group: postgresql.cnpg.io + kind: Cluster + - group: traefik.io + kind: IngressRoute diff --git a/bootstrap/immich.yaml b/bootstrap/immich.yaml new file mode 100644 index 0000000..e964a67 --- /dev/null +++ b/bootstrap/immich.yaml @@ -0,0 +1,31 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: immich + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "0" +spec: + project: immich + sources: + - repoURL: ghcr.io/immich-app/immich-charts + chart: immich + targetRevision: 0.13.4 + helm: + releaseName: immich + valueFiles: + - $values/apps/immich/values.yaml + - repoURL: https://code.brunner.ninja/feedc0de/homelab-gitops.git + targetRevision: main + ref: values + path: apps/immich/resources + destination: + server: https://kubernetes.default.svc + namespace: immich + syncPolicy: + automated: + enabled: true + prune: false + selfHeal: true + syncOptions: + - CreateNamespace=true diff --git a/bootstrap/kustomization.yaml b/bootstrap/kustomization.yaml index df90224..25aded5 100644 --- a/bootstrap/kustomization.yaml +++ b/bootstrap/kustomization.yaml @@ -7,6 +7,11 @@ resources: - grafana-project.yaml - homeassistant-postgresql-project.yaml - platform-project.yaml +- cnpg-operator-project.yaml +- cnpg-operator.yaml +- immich-project.yaml +- immich-postgresql.yaml +- immich.yaml - sealed-secrets.yaml - immich-sync.yaml - immichframe.yaml diff --git a/bootstrap/platform-project.yaml b/bootstrap/platform-project.yaml index e1dd881..412a449 100644 --- a/bootstrap/platform-project.yaml +++ b/bootstrap/platform-project.yaml @@ -7,6 +7,7 @@ spec: description: Cluster services installed from maintained upstream charts sourceRepos: - https://bitnami.github.io/sealed-secrets + - https://cloudnative-pg.github.io/charts destinations: - namespace: sealed-secrets server: https://kubernetes.default.svc