Immich in Kubernetes

I’ve been looking for a replacement for Google Photos for a while. My requirements were:

  • Self-hosted
  • Kubernetes-native
  • Mobile backup
  • Metadata stored in an open format
  • No vendor lock-in

I found Immich, which looked perfect, but… initially I tried the official Immich Helm chart. After spending a few hours with it, I eventually decided to abandon it and deploy Immich using plain Kubernetes manifests generated from the official Docker Compose file. This article documents the issues I encountered and how I solved them.

Why did I skip the official Helm chart?

The official chart has gradually become more of a deployment framework than a complete application. Some surprises I encountered:

  • PostgreSQL is no longer bundled.
  • Redis is no longer bundled.
  • You need to provide your own PostgreSQL with the required vector extension.
  • Storage configuration is more complicated than expected.
  • The chart requires pre-created PVCs.
  • There are several breaking changes between chart versions.

None of these are inherently bad, but if you need to bring your own PostgreSQL, Redis and PVCs anyway, the official Helm chart doesn’t provide much additional value. For me, a few simple Deployment, Service and PVC manifests (derived from the compose file) were much easier to understand and maintain, and they can simply be placed into a templates/ folder, and you have a Helm chart.

The immich-server.yaml

It contains the immich-server container (the v3-rc version).

apiVersion: apps/v1
kind: Deployment
metadata:
  name: immich-server
  namespace: others
spec:
  replicas: 1
  selector:
    matchLabels:
      app: immich-server
  template:
    metadata:
      labels:
        app: immich-server
    spec:
      containers:
        - name: immich-server
          image: ghcr.io/immich-app/immich-server:v3-rc
          ports:
            - containerPort: 2283
          envFrom:
            - configMapRef:
                name: immich-config
            - secretRef:
                name: immich-secrets
          volumeMounts:
            - name: immich-library
              mountPath: /data
            - name: immich-external-library
              mountPath: /external-library
      volumes:
        - name: immich-library
          persistentVolumeClaim:
            claimName: immich-library
        - name: immich-external-library
          persistentVolumeClaim:
            claimName: immich-external-library
---
apiVersion: v1
kind: Service
metadata:
  name: immich-server
  namespace: others
spec:
  selector:
    app: immich-server
  ports:
    - port: 2283
      targetPort: 2283
  type: NodePort
---

The redis.yaml

It contains the redis container (the valkey fork). Since Redis changed its license, the Immich project recommends Valkey, the community-driven fork. The protocol is fully compatible, so no additional configuration is required.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: immich-redis
  namespace: others
spec:
  replicas: 1
  selector:
    matchLabels:
      app: immich-redis
  template:
    metadata:
      labels:
        app: immich-redis
    spec:
      containers:
        - name: immich-redis
          image: docker.io/valkey/valkey:9
          ports:
            - containerPort: 6379
---
apiVersion: v1
kind: Service
metadata:
  name: immich-redis
  namespace: others
spec:
  selector:
    app: immich-redis
  ports:
    - port: 6379
      targetPort: 6379
  type: NodePort
---

The postgres.yaml

This deployment uses the official Immich PostgreSQL image, which includes the required vector extensions. Database credentials are provided through a secret.

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: immich-postgres
  namespace: others
spec:
  serviceName: immich-postgres
  replicas: 1
  selector:
    matchLabels:
      app: immich-postgres
  template:
    metadata:
      labels:
        app: immich-postgres
    spec:
      containers:
        - name: immich-postgres
          image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0
          ports:
            - containerPort: 5432
          env:
            - name: POSTGRES_USER
              valueFrom:
                secretKeyRef:
                  name: immich-secrets
                  key: DB_USERNAME
            - name: POSTGRES_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: immich-secrets
                  key: DB_PASSWORD
            - name: POSTGRES_DB
              valueFrom:
                secretKeyRef:
                  name: immich-secrets
                  key: DB_DATABASE_NAME
            - name: POSTGRES_INITDB_ARGS
              value: "--data-checksums"
          volumeMounts:
            - name: immich-postgres-data
              mountPath: /var/lib/postgresql/data
              subPath: data
      volumes:
        - name: immich-postgres-data
          persistentVolumeClaim:
            claimName: immich-postgres-data
---
apiVersion: v1
kind: Service
metadata:
  name: immich-postgres
  namespace: others
spec:
  selector:
    app: immich-postgres
  ports:
    - port: 5432
      targetPort: 5432
  type: NodePort
---

The pvc.yml

It creates two PV/PVC pair for the library and the external-library and a replicated PVC for the database, backed by Longhorn storage. The external-library contains your existing photos, the library will contain your newly uploaded photos and the thumbnails and so on.

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: immich-library
  namespace: others
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: manual
  volumeName: immich-library-pv
  resources:
    requests:
      storage: 100Gi
  volumeMode: Filesystem
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: immich-library-pv
spec:
  capacity:
    storage: 100Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: manual
  hostPath:
    type: DirectoryOrCreate
    path: "/others/immich"
  volumeMode: Filesystem
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: immich-external-library
  namespace: others
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: manual
  volumeName: immich-external-library-pv
  resources:
    requests:
      storage: 100Gi
  volumeMode: Filesystem
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: immich-external-library-pv
spec:
  capacity:
    storage: 100Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: manual
  hostPath:
    type: DirectoryOrCreate
    path: "/others/immich-external-library"
  volumeMode: Filesystem
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: immich-postgres-data
  namespace: others
spec:
  storageClassName: longhorn-replicated-3
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 4Gi
---

The secrets.yaml

This file contains the application secrets and configuration. I started with the most-minimal config, without machine learning.

apiVersion: v1
kind: Secret
metadata:
  name: immich-secrets
  namespace: others
type: Opaque
stringData:
  DB_USERNAME: immich
  DB_PASSWORD: change-me
  DB_DATABASE_NAME: immich
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: immich-config
  namespace: others
data:
  IMMICH_VERSION: "v3-rc"
  DB_HOSTNAME: "immich-postgres"
  DB_PORT: "5432"
  REDIS_HOSTNAME: "immich-redis"
  REDIS_PORT: "6379"
---

The ingress.yaml

An Ingress resource exposes the Immich server under your chosen hostname.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  namespace: others
  name: immich-ingress
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
    nginx.ingress.kubernetes.io/ssl-redirect: 'true'
    nginx.ingress.kubernetes.io/proxy-body-size: 16m
    nginx.ingress.kubernetes.io/proxy-connect-timeout: "180"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "180"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "180"
    kubernetes.io/ingress.class: "nginx"
spec:
  ingressClassName: nginx
  tls:
  - hosts:
    - "immich.host.tld"
    secretName: immich.host.tld-cert
  rules:
  - host: "immich.host.tld"
    http:
      paths:
      - pathType: Prefix
        path: /
        backend:
          service:
            name: immich-server
            port:
              number: 2283
---

That’s it, folks! 🙂

Leave a Comment

Your email address will not be published. Required fields are marked *


1 thought on “Immich in Kubernetes”

  1. Pingback: My Immich pitfalls – enaplo.hu

Scroll to Top