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! 🙂
Pingback: My Immich pitfalls – enaplo.hu