apiVersion: helm.toolkit.fluxcd.io/v2 kind: HelmRelease metadata: name: immich-server namespace: photo spec: interval: 30m chartRef: kind: OCIRepository name: immich driftDetection: mode: enabled values: controllers: server: strategy: Recreate podDisruptionBudget: maxUnavailable: "0" initContainers: fix-permissions: image: repository: hectorm/nfs-ganesha tag: v10 command: - sh - -c - chmod -R a+rwX /disks/library-recent /disks/library-archive # native sidecar (restartPolicy: Always): mounts the two library # disks and exposes their union at /disks/merged-library via # FUSE. It must be up and actually mounted *before* the "app" # container is created, since HostToContainer propagation only # relays mounts made *after* the receiving container's own mount # exists - it won't retroactively pick up a FUSE mount that's # already there. Running it as a restartPolicy:Always init # container (started, gated on the startupProbe below, before # regular containers) guarantees that ordering; a regular sidecar # container racing "app" on every pod start does not. mergerfs: dependsOn: - fix-permissions restartPolicy: Always image: repository: ghcr.io/hotio/mergerfs tag: release-v2.42.0 args: - "-o" # library-recent listed first: category.create=ff means new # files created through the merged mount (i.e. new immich # uploads going forward) land on library-recent by default, # which is exactly what's wanted for freshly-uploaded photos. - "allow_other,use_ino,func.create=ff,category.action=all,category.create=ff,category.search=newest" - "/disks/library-recent:/disks/library-archive" - "/disks/merged-library" securityContext: privileged: true probes: startup: enabled: true custom: true spec: exec: command: - sh - -c - grep -q fuse.mergerfs /proc/mounts periodSeconds: 1 timeoutSeconds: 2 failureThreshold: 30 resources: requests: cpu: 50m memory: 64Mi limits: memory: 512Mi containers: app: image: repository: ghcr.io/immich-app/immich-server tag: release env: TZ: Europe/Moscow DB_HOSTNAME: immich-postgres DB_PORT: "5432" DB_USERNAME: postgres DB_DATABASE_NAME: immich DB_PASSWORD: valueFrom: secretKeyRef: name: immich-secrets key: DB_PASSWORD REDIS_HOSTNAME: immich-redis REDIS_PORT: "6379" IMMICH_MACHINE_LEARNING_URL: http://immich-ml:3003 probes: # first boot runs DB migrations + vector index setup, which # can take a while - without a startup probe the default # liveness probe (periodSeconds=10, failureThreshold=3) kills # the container ~30s in, well before it's finished binding # the port, causing an endless restart loop. startup: enabled: true spec: periodSeconds: 5 failureThreshold: 60 liveness: enabled: true readiness: enabled: true resources: requests: cpu: 200m memory: 512Mi limits: memory: 4Gi # one-shot migration copy from the old NAS-hosted docker install # into this install's own volumes (see "persistence" below). # rsync is idempotent, so a pod restart mid-copy just resumes # cleanly; once done it sleeps forever instead of exiting, so the # container doesn't restart-loop and the status page stays up. # (starts after all initContainers - fix-permissions and the # mergerfs sidecar's startupProbe - complete, same as "app") rsync: image: repository: python tag: 3.13-alpine command: - sh - -c - apk add --no-cache rsync >/dev/null && python3 /app/rsync.py resources: requests: cpu: 100m memory: 256Mi limits: memory: 1Gi rsync-web: image: repository: nginx tag: 1.27-alpine probes: # explicit port: this controller has two services (app:2283, # rsync-web:80) and probe auto-detection picks the # controller's primary service port rather than this # container's own - without this it checks the wrong port. liveness: enabled: true port: 80 readiness: enabled: true port: 80 resources: requests: cpu: 10m memory: 16Mi limits: memory: 64Mi service: app: controller: server ports: http: port: 2283 rsync-web: controller: server ports: http: port: 80 ingress: app: className: traefik annotations: cert-manager.io/cluster-issuer: letsencrypt-prod hosts: - host: photo.aggtaa.com paths: - path: / pathType: Prefix service: identifier: app port: http tls: - secretName: immich-tls hosts: - photo.aggtaa.com rsync-web: className: traefik annotations: cert-manager.io/cluster-issuer: letsencrypt-prod hosts: - host: rsync.photo.aggtaa.com paths: - path: / pathType: Prefix service: identifier: rsync-web port: http tls: - secretName: immich-rsync-tls hosts: - rsync.photo.aggtaa.com persistence: # Immich's "library" folder, mergerfs-merged from two disks: recent # files (per the rsync migration's cutoff) and everything else. # New uploads going forward land on library-recent (see mergerfs # category.create above). Not reusing the old single 800Gi library # PVC/PV - this replaces it (that PVC becomes orphaned, retained, # and unused; clean it up separately if no longer wanted). library-recent: type: persistentVolumeClaim accessMode: ReadWriteOnce size: 200Gi storageClass: longhorn-single-replica retain: true advancedMounts: server: mergerfs: - path: /disks/library-recent fix-permissions: - path: /disks/library-recent rsync: - path: /disks/library-recent library-archive: type: persistentVolumeClaim accessMode: ReadWriteOnce size: 500Gi storageClass: longhorn-single-replica retain: true advancedMounts: server: mergerfs: - path: /disks/library-archive fix-permissions: - path: /disks/library-archive rsync: - path: /disks/library-archive merged-library: type: emptyDir advancedMounts: server: mergerfs: - path: /disks/merged-library mountPropagation: Bidirectional app: - path: /data/library mountPropagation: HostToContainer dev-fuse: type: hostPath hostPath: /dev/fuse advancedMounts: server: mergerfs: - path: /dev/fuse # everything immich keeps outside "library" (backups, encoded-video, # profile, thumbs, upload) - plain volume, no mergerfs involved. other: type: persistentVolumeClaim accessMode: ReadWriteOnce size: 200Gi storageClass: longhorn-single-replica retain: true advancedMounts: server: app: - path: /data rsync: - path: /data nas-source: type: persistentVolumeClaim existingClaim: immich-nas-source advancedMounts: server: rsync: - path: /source readOnly: true rsync-script: type: configMap name: immich-rsync-script advancedMounts: server: rsync: - path: /app/rsync.py subPath: rsync.py readOnly: true rsync-www: type: emptyDir advancedMounts: server: rsync: - path: /www rsync-web: - path: /usr/share/nginx/html