feat: add immich

This commit is contained in:
Morten Olsen
2025-12-28 19:30:36 +01:00
parent f80b838d41
commit d35827bcd6
14 changed files with 484 additions and 162 deletions

View File

@@ -1,6 +0,0 @@
apiVersion: homelab.mortenolsen.pro/v1
kind: PostgresDatabase
metadata:
name: '{{ .Release.Name }}'
spec:
environment: '{{ .Values.globals.environment }}'

View File

@@ -1,105 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: "{{ .Release.Name }}-server"
spec:
strategy:
type: Recreate
replicas: 1
revisionHistoryLimit: 0
selector:
matchLabels:
app: "{{ .Release.Name }}-server"
template:
metadata:
labels:
app: "{{ .Release.Name }}-server"
spec:
containers:
- name: "{{ .Release.Name }}-server"
image: "{{ .Values.server.image.repository }}:{{ .Values.server.image.tag }}"
imagePullPolicy: "{{ .Values.server.image.pullPolicy }}"
env:
- name: DB_URL
valueFrom:
secretKeyRef:
name: "{{ .Release.Name }}-pg-connection"
key: url
- name: DB_VECTOR_EXTENSION
value: pgvector
- name: REDIS_HOST
value: "{{ .Release.name }}-valkey.{{ .Release.Namespace }}.svc.cluster.local"
- name: IMMICH_PORT
value: "3003"
ports:
- name: http
containerPort: 3003
protocol: TCP
livenessProbe:
tcpSocket:
port: http
readinessProbe:
tcpSocket:
port: http
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: "{{ .Release.Name }}-ml"
spec:
strategy:
type: Recreate
replicas: 1
revisionHistoryLimit: 0
selector:
matchLabels:
app: "{{ .Release.Name }}-ml"
template:
metadata:
labels:
app: "{{ .Release.Name }}-ml"
spec:
containers:
- name: "{{ .Release.Name }}-ml"
image: "{{ .Values.ml.image.repository }}:{{ .Values.ml.image.tag }}"
imagePullPolicy: "{{ .Values.ml.image.pullPolicy }}"
env:
- name: DB_URL
valueFrom:
secretKeyRef:
name: "{{ .Release.Name }}-pg-connection"
key: url
# - name: DB_VECTOR_EXTENSION
# value: pgvector
- name: REDIS_HOST
value: "{{ .Release.name }}-valkey"
- name: IMMICH_PORT
value: "3003"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: "{{ .Release.Name }}-valkey"
spec:
strategy:
type: Recreate
replicas: 1
revisionHistoryLimit: 0
selector:
matchLabels:
app: "{{ .Release.Name }}-valkey"
template:
metadata:
labels:
app: "{{ .Release.Name }}-valkey"
spec:
containers:
- name: "{{ .Release.Name }}-valkey"
image: "{{ .Values.valkey.image.repository }}:{{ .Values.valkey.image.tag }}"
imagePullPolicy: "{{ .Values.valkey.image.pullPolicy }}"
ports:
- name: tcp
containerPort: 6379
protocol: TCP

View File

@@ -1,11 +0,0 @@
apiVersion: homelab.mortenolsen.pro/v1
kind: ExternalHttpService
metadata:
name: "{{ .Release.Name }}"
spec:
environment: "{{ .Values.globals.environment }}"
subdomain: "{{ .Values.subdomain }}"
destination:
host: "{{ .Release.Name }}.{{ .Release.Namespace }}.svc.cluster.local"
port:
number: 80

View File

@@ -1,18 +0,0 @@
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
name: "{{ .Release.Name }}"
namespace: "{{ .Release.Namespace }}"
spec:
gateways:
- "{{ .Values.globals.istio.gateway }}"
- mesh
hosts:
- "{{ .Values.subdomain }}.{{ .Values.globals.domain }}"
- mesh
http:
- route:
- destination:
host: "{{ .Release.Name }}"
port:
number: 80

View File

@@ -1,16 +0,0 @@
subdomain: penpot
server:
image:
repository: ghcr.io/immich-app/immich-server
tag: release@sha256:e6a6298e67ae077808fdb7d8d5565955f60b0708191576143fc02d30ab1389d1
pullPolicy: IfNotPresent
ml:
image:
repository: ghcr.io/immich-app/immich-machine-learning
tag: release@sha256:b3deefd1826f113824e9d7bc30d905e7f823535887d03f869330946b6db3b44a
pullPolicy: IfNotPresent
valkey:
image:
repository: valkey/valkey
tag: 9.0@sha256:546304417feac0874c3dd576e0952c6bb8f06bb4093ea0c9ca303c73cf458f63
pullPolicy: IfNotPresent

View File

@@ -1,10 +1,10 @@
apiVersion: homelab.mortenolsen.pro/v1 apiVersion: homelab.mortenolsen.pro/v1
kind: OidcClient kind: OidcClient
metadata: metadata:
name: "{{ .Release.Name }}" name: '{{ .Release.Name }}'
spec: spec:
environment: "{{ .Values.globals.environment }}" environment: '{{ .Values.globals.environment }}'
redirectUris: redirectUris:
- path: /api/oauth/mobile-redirect - path: /api/oauth/mobile-redirect
subdomain: "{{ .Values.subdomain }}" subdomain: '{{ .Values.subdomain }}'
matchingMode: strict matchingMode: strict

View File

@@ -0,0 +1,114 @@
apiVersion: homelab.mortenolsen.pro/v1
kind: GenerateSecret
metadata:
name: "{{ .Release.Name }}-postgres-secret"
spec:
fields:
- name: password
encoding: base64
length: 64
---
apiVersion: v1
kind: Secret
metadata:
name: "{{ .Release.Name }}-postgres"
type: Opaque
stringData:
POSTGRES_DB: immich
POSTGRES_USER: immich
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: "{{ .Release.Name }}-postgres"
spec:
strategy:
type: Recreate
replicas: 1
revisionHistoryLimit: 0
selector:
matchLabels:
app: "{{ .Release.Name }}-postgres"
template:
metadata:
labels:
app: "{{ .Release.Name }}-postgres"
spec:
containers:
- name: postgres
image: "{{ .Values.postgres.image.repository }}:{{ .Values.postgres.image.tag }}"
imagePullPolicy: "{{ .Values.postgres.image.pullPolicy }}"
env:
- name: POSTGRES_DB
valueFrom:
secretKeyRef:
name: "{{ .Release.Name }}-postgres"
key: POSTGRES_DB
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: "{{ .Release.Name }}-postgres"
key: POSTGRES_USER
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: "{{ .Release.Name }}-postgres-secret"
key: password
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
ports:
- name: postgres
containerPort: 5432
protocol: TCP
volumeMounts:
- mountPath: /var/lib/postgresql/data
name: postgres-data
livenessProbe:
exec:
command:
- /bin/sh
- -c
- pg_isready -U immich
readinessProbe:
exec:
command:
- /bin/sh
- -c
- pg_isready -U immich
volumes:
- name: postgres-data
persistentVolumeClaim:
claimName: "{{ .Release.Name }}-postgres-data"
---
apiVersion: v1
kind: Service
metadata:
name: "{{ .Release.Name }}-postgres"
labels:
app: "{{ .Release.Name }}-postgres"
spec:
type: ClusterIP
ports:
- port: 5432
targetPort: 5432
protocol: TCP
name: postgres
selector:
app: "{{ .Release.Name }}-postgres"
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: "{{ .Release.Name }}-postgres-data"
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 50Gi
storageClassName: "{{ .Values.globals.environment }}"

View File

@@ -0,0 +1,85 @@
apiVersion: batch/v1
kind: Job
metadata:
name: "{{ .Release.Name }}-db-config-generator"
annotations:
"helm.sh/hook": pre-install,pre-upgrade
"helm.sh/hook-weight": "5"
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
template:
metadata:
annotations:
sidecar.istio.io/inject: "false"
spec:
restartPolicy: OnFailure
serviceAccountName: "{{ .Release.Name }}-db-config-sa"
containers:
- name: generator
image: python:3.11-slim
command:
- /bin/bash
- -c
- |
set -e
# Install kubectl
apt-get update -qq && apt-get install -y -qq curl > /dev/null 2>&1 && \
curl -sSL "https://dl.k8s.io/release/$(curl -sL https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" -o /tmp/kubectl && \
chmod +x /tmp/kubectl && mv /tmp/kubectl /usr/local/bin/kubectl
PASSWORD=$(cat /secrets/password)
# URL encode the password to handle special characters using Python
ENCODED_PASSWORD=$(python3 -c "import urllib.parse; import sys; print(urllib.parse.quote(sys.stdin.read().strip(), safe=''))" <<< "$PASSWORD")
DB_URL="postgresql://immich:${ENCODED_PASSWORD}@{{ .Release.Name }}-postgres.{{ .Release.Namespace }}.svc.cluster.local:5432/immich"
# Create or update the ConfigMap
kubectl create configmap {{ .Release.Name }}-db-config --from-literal=url="${DB_URL}" --dry-run=client -o yaml | kubectl apply -f -
echo "ConfigMap {{ .Release.Name }}-db-config created/updated successfully"
volumeMounts:
- name: postgres-secret
mountPath: /secrets
readOnly: true
volumes:
- name: postgres-secret
secret:
secretName: "{{ .Release.Name }}-postgres-secret"
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: "{{ .Release.Name }}-db-config-sa"
annotations:
"helm.sh/hook": pre-install,pre-upgrade
"helm.sh/hook-weight": "-10"
"helm.sh/hook-delete-policy": before-hook-creation
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: "{{ .Release.Name }}-db-config-role"
annotations:
"helm.sh/hook": pre-install,pre-upgrade
"helm.sh/hook-weight": "-9"
"helm.sh/hook-delete-policy": before-hook-creation
rules:
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "create", "update", "patch", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: "{{ .Release.Name }}-db-config-rolebinding"
annotations:
"helm.sh/hook": pre-install,pre-upgrade
"helm.sh/hook-weight": "-8"
"helm.sh/hook-delete-policy": before-hook-creation
subjects:
- kind: ServiceAccount
name: "{{ .Release.Name }}-db-config-sa"
roleRef:
kind: Role
name: "{{ .Release.Name }}-db-config-role"
apiGroup: rbac.authorization.k8s.io

View File

@@ -0,0 +1,194 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: "{{ .Release.Name }}-server"
spec:
strategy:
type: Recreate
replicas: 1
revisionHistoryLimit: 0
selector:
matchLabels:
app: "{{ .Release.Name }}-server"
template:
metadata:
labels:
app: "{{ .Release.Name }}-server"
spec:
containers:
- name: "{{ .Release.Name }}-server"
image: "{{ .Values.server.image.repository }}:{{ .Values.server.image.tag }}"
imagePullPolicy: "{{ .Values.server.image.pullPolicy }}"
env:
- name: DB_URL
valueFrom:
configMapKeyRef:
name: "{{ .Release.Name }}-db-config"
key: url
- name: DB_VECTOR_EXTENSION
value: pgvector
- name: REDIS_HOSTNAME
value: "{{ .Release.Name }}-valkey.{{ .Release.Namespace }}.svc.cluster.local"
- name: REDIS_HOST
value: "{{ .Release.Name }}-valkey.{{ .Release.Namespace }}.svc.cluster.local"
- name: REDIS_PORT
value: "6379"
- name: REDIS_URL
value: "redis://{{ .Release.Name }}-valkey.{{ .Release.Namespace }}.svc.cluster.local:6379"
- name: IMMICH_REDIS_HOSTNAME
value: "{{ .Release.Name }}-valkey.{{ .Release.Namespace }}.svc.cluster.local"
- name: IMMICH_PORT
value: "3003"
- name: IMMICH_UPLOAD_LOCATION
value: /usr/src/app/upload
- name: IMMICH_MACHINE_LEARNING_URL
value: http://{{ .Release.Name }}-ml.{{ .Release.Namespace }}.svc.cluster.local:3003
- name: OAUTH_AUTO_REGISTER
value: "true"
- name: OAUTH_AUTO_LAUNCH
value: "true"
- name: OAUTH_BUTTON_TEXT
value: "Login with OAuth"
- name: OAUTH_ENABLED
value: "true"
- name: OAUTH_ISSUER_URL
valueFrom:
secretKeyRef:
name: "{{ .Release.Name }}-client"
key: configuration
- name: OAUTH_CLIENT_ID
valueFrom:
secretKeyRef:
name: "{{ .Release.Name }}-client"
key: clientId
- name: OAUTH_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: "{{ .Release.Name }}-client"
key: clientSecret
- name: OAUTH_SCOPE
value: "openid profile email"
- name: OAUTH_STORAGE_LABEL_CLAIM
value: "preferred_username"
ports:
- name: http
containerPort: 3003
protocol: TCP
volumeMounts:
- mountPath: /usr/src/app/upload
name: upload
- mountPath: /usr/src/app/library
name: library
- mountPath: /mnt/media/nas
name: nas
livenessProbe:
tcpSocket:
port: http
readinessProbe:
tcpSocket:
port: http
volumes:
- name: upload
persistentVolumeClaim:
claimName: "{{ .Release.Name }}-upload"
- name: library
persistentVolumeClaim:
claimName: "{{ .Release.Name }}-library"
- name: nas
persistentVolumeClaim:
claimName: pictures
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: "{{ .Release.Name }}-ml"
spec:
strategy:
type: Recreate
replicas: 1
revisionHistoryLimit: 0
selector:
matchLabels:
app: "{{ .Release.Name }}-ml"
template:
metadata:
labels:
app: "{{ .Release.Name }}-ml"
spec:
containers:
- name: "{{ .Release.Name }}-ml"
image: "{{ .Values.ml.image.repository }}:{{ .Values.ml.image.tag }}"
imagePullPolicy: "{{ .Values.ml.image.pullPolicy }}"
env:
- name: DB_URL
valueFrom:
configMapKeyRef:
name: "{{ .Release.Name }}-db-config"
key: url
- name: DB_VECTOR_EXTENSION
value: pgvector
- name: REDIS_HOSTNAME
value: "{{ .Release.Name }}-valkey.{{ .Release.Namespace }}.svc.cluster.local"
- name: REDIS_HOST
value: "{{ .Release.Name }}-valkey.{{ .Release.Namespace }}.svc.cluster.local"
- name: REDIS_PORT
value: "6379"
- name: REDIS_URL
value: "redis://{{ .Release.Name }}-valkey.{{ .Release.Namespace }}.svc.cluster.local:6379"
- name: IMMICH_REDIS_HOSTNAME
value: "{{ .Release.Name }}-valkey.{{ .Release.Namespace }}.svc.cluster.local"
- name: IMMICH_PORT
value: "3003"
- name: IMMICH_MACHINE_LEARNING_MODEL_PATH
value: /cache
ports:
- name: http
containerPort: 3003
protocol: TCP
volumeMounts:
- mountPath: /cache
name: model-cache
livenessProbe:
tcpSocket:
port: http
readinessProbe:
tcpSocket:
port: http
volumes:
- name: model-cache
persistentVolumeClaim:
claimName: "{{ .Release.Name }}-model-cache"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: "{{ .Release.Name }}-valkey"
spec:
strategy:
type: Recreate
replicas: 1
revisionHistoryLimit: 0
selector:
matchLabels:
app: "{{ .Release.Name }}-valkey"
template:
metadata:
labels:
app: "{{ .Release.Name }}-valkey"
spec:
containers:
- name: "{{ .Release.Name }}-valkey"
image: "{{ .Values.valkey.image.repository }}:{{ .Values.valkey.image.tag }}"
imagePullPolicy: "{{ .Values.valkey.image.pullPolicy }}"
ports:
- name: tcp
containerPort: 6379
protocol: TCP
livenessProbe:
tcpSocket:
port: tcp
readinessProbe:
tcpSocket:
port: tcp

View File

@@ -0,0 +1,11 @@
apiVersion: homelab.mortenolsen.pro/v1
kind: ExternalHttpService
metadata:
name: '{{ .Release.Name }}'
spec:
environment: '{{ .Values.globals.environment }}'
subdomain: '{{ .Values.subdomain }}'
destination:
host: '{{ .Release.Name }}.{{ .Release.Namespace }}.svc.cluster.local'
port:
number: 80

View File

@@ -0,0 +1,37 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: "{{ .Release.Name }}-upload"
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 100Gi
storageClassName: "{{ .Values.globals.environment }}"
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: "{{ .Release.Name }}-library"
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 100Gi
storageClassName: "{{ .Values.globals.environment }}"
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: "{{ .Release.Name }}-model-cache"
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
storageClassName: "{{ .Values.globals.environment }}"

View File

@@ -14,13 +14,30 @@ spec:
selector: selector:
app: "{{ .Release.Name }}-valkey" app: "{{ .Release.Name }}-valkey"
---
apiVersion: v1
kind: Service
metadata:
name: "{{ .Release.Name }}-ml"
labels:
app: "{{ .Release.Name }}-ml"
spec:
type: ClusterIP
ports:
- port: 3003
targetPort: 3003
protocol: TCP
name: http
selector:
app: "{{ .Release.Name }}-ml"
--- ---
apiVersion: v1 apiVersion: v1
kind: Service kind: Service
metadata: metadata:
name: "{{ .Release.Name }}" name: "{{ .Release.Name }}"
labels: labels:
app: "{{ .Release.Name }}" app: "{{ .Release.Name }}-server"
spec: spec:
type: ClusterIP type: ClusterIP
ports: ports:
@@ -29,5 +46,4 @@ spec:
protocol: TCP protocol: TCP
name: http name: http
selector: selector:
app: "{{ .Release.Name }}" app: "{{ .Release.Name }}-server"

View File

@@ -0,0 +1,21 @@
subdomain: immich
server:
image:
repository: ghcr.io/immich-app/immich-server
tag: release
pullPolicy: IfNotPresent
ml:
image:
repository: ghcr.io/immich-app/immich-machine-learning
tag: release
pullPolicy: IfNotPresent
valkey:
image:
repository: valkey/valkey
tag: 9.0
pullPolicy: IfNotPresent
postgres:
image:
repository: pgvector/pgvector
tag: pg16
pullPolicy: IfNotPresent