Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ Below is a list of available charts along with their links:
| **Name** | **Link** | **Deploy** |
|---|---|---|
| **HolmesGPT** | [helm.zop.dev/holmesgpt](https://helm.zop.dev/src/readme.html?id=holmesgpt) | <a href="https://zop.dev/zopday/app/deploy?install=holmesgpt"><img src="https://zop.dev/deploytozopday-inkhard.svg" alt="Deploy to Zopday" height="28"></a> |
| **Immich** | [helm.zop.dev/immich](https://helm.zop.dev/src/readme.html?id=immich) | <a href="https://zop.dev/zopday/app/deploy?install=immich"><img src="https://zop.dev/deploytozopday-inkhard.svg" alt="Deploy to Zopday" height="28"></a> |
| **JupyterHub** | [helm.zop.dev/jupyterhub](https://helm.zop.dev/src/readme.html?id=jupyterhub) | <a href="https://zop.dev/zopday/app/deploy?install=jupyterhub"><img src="https://zop.dev/deploytozopday-inkhard.svg" alt="Deploy to Zopday" height="28"></a> |
| **LiteLLM** | [helm.zop.dev/litellm](https://helm.zop.dev/src/readme.html?id=litellm) | <a href="https://zop.dev/zopday/app/deploy?install=litellm"><img src="https://zop.dev/deploytozopday-inkhard.svg" alt="Deploy to Zopday" height="28"></a> |
| **LocalAI** | [helm.zop.dev/localai](https://helm.zop.dev/src/readme.html?id=localai) | <a href="https://zop.dev/zopday/app/deploy?install=localai"><img src="https://zop.dev/deploytozopday-inkhard.svg" alt="Deploy to Zopday" height="28"></a> |
Expand Down
6 changes: 6 additions & 0 deletions charts/immich/Chart.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
dependencies:
- name: redis
repository: https://helm.zop.dev
version: v0.0.5
digest: sha256:045cdd36edfbf0caff91d1a3940f8c949d4fdabe35705e80232e49ce89b6abaf
generated: "2026-08-13T12:36:04.094344+05:30"
16 changes: 16 additions & 0 deletions charts/immich/Chart.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
apiVersion: v2
appVersion: "v3.1.0"
description: Helm chart for deploying Immich, a self-hosted photo and video backup solution
name: immich
version: 0.0.1
type: application
icon: "https://raw.githubusercontent.com/immich-app/immich/main/design/immich-logo-stacked-light.png"
maintainers:
- name: ZopDev
url: zop.dev
dependencies:
- name: redis
version: 0.0.5
repository: https://helm.zop.dev
annotations:
type: application
264 changes: 264 additions & 0 deletions charts/immich/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
# Immich Helm Chart

[Immich](https://github.com/immich-app/immich) is a self-hosted photo
and video backup solution, a drop-in alternative to Google
Photos/iCloud with a matching mobile app. This chart deploys all four
pieces Immich's own reference deployment runs: the server, machine
learning, its own Postgres (with the vector extension its search
needs), and Redis.

---

## Prerequisites

- Kubernetes 1.19+
- Helm 3+
- Prometheus Operator CRDs (`monitoring.coreos.com`) installed on the
cluster. The redis dependency renders a `PrometheusRule` and a
`ServiceMonitor` unconditionally — there's no flag to opt out — so
`helm install` fails outright without them.
- On `amd64` nodes, a CPU supporting the `x86-64-v2` microarchitecture
level or newer. Since Immich v3, the machine-learning image's ONNX
runtime requires it — an older node pool or VM CPU model will fail to
run that container. (`arm64` has no equivalent restriction.)

---

## Add Helm Repository

```bash
helm repo add zopdev https://helm.zop.dev
helm repo update
```

---

## Install Helm Chart

```bash
helm install my-immich zopdev/immich
```

Deploys all four components. First run: open the server's URL and
create the admin account — Immich has no default login.

---

## Uninstall Helm Chart

```bash
helm uninstall my-immich
```

PersistentVolumeClaims created from volume templates outlive the
release. Delete **all four** separately to reclaim the disks:

```bash
kubectl delete pvc library-my-immich-immich-server-0 \
cache-my-immich-immich-ml-0 \
data-my-immich-immich-postgres-0 \
my-immich-redis-persistent-storage-my-immich-redis-0
```

**If you're keeping the volumes to reinstall later, leave the postgres
password Secret alone too.** It's annotated `helm.sh/resource-policy:
keep` specifically so `helm uninstall` doesn't remove it — the
database volume is initialized with that password baked in, and a
reinstall that generates a *new* random password (because the old
Secret was deleted along with the volume being kept) can never
authenticate against it again. Delete the Secret and the postgres PVC
together, or keep both together — never split them. To reclaim
*everything*, including that Secret:

```bash
kubectl delete secret my-immich-immich-postgres-secret
```

**The same rule applies to the library PVC and the database.** Immich
writes a `.immich` marker file into `/data`'s subfolders on first run
and refuses to start if a later run finds that marker missing —
[system integrity checks](https://docs.immich.app/administration/system-integrity)
that catch a wrong volume mount or an incomplete restore. Delete
`library-my-immich-immich-server-0` together with the postgres PVC and
Secret, or keep all three together — mixing a fresh library volume
with a retained database (or vice versa) fails the same way.

---

## Configuration

| **Input** | **Type** | **Description** | **Default** |
|---|---|---|---|
| `server.image.tag` | `string` | Server image tag. | `v3.1.0` |
| `server.service.type` | `string` | Service type for the web UI/API. | `ClusterIP` |
| `server.service.port` | `int` | Service port for the web UI/API. | `2283` |
| `server.diskSize` | `string` | Size of the photo/video library volume. | `"20Gi"` |
| `server.resources` | `object` | CPU and memory for the server pod. | 500m/1Gi – 2000m/4Gi |
| `server.env` | `object` | Extra environment variables for the server. | `{}` |
| `server.startupProbe` | `object` | `periodSeconds`/`failureThreshold` budget for first boot (schema migrations). | 10s × 30 = 300s |
| `machineLearning.image.tag` | `string` | Machine-learning image tag. | `v3.1.0` |
| `machineLearning.diskSize` | `string` | Size of the ML model cache volume. | `"5Gi"` |
| `machineLearning.resources` | `object` | CPU and memory for the ML pod. | 1000m/2Gi – 4000m/4Gi |
| `machineLearning.startupProbe` | `object` | `periodSeconds`/`failureThreshold` budget for cold start (model loading). | 10s × 60 = 600s |
| `postgres.database` | `string` | Database name. | `"immich"` |
| `postgres.username` | `string` | Database user. | `"immich"` |
| `postgres.password` | `string` | Pin a known database password instead of generating one. See *Database password* below. Not safe to edit after install. | `""` |
| `postgres.existingSecret` | `string` | Name of an existing Secret carrying `POSTGRES_PASSWORD`, used instead of one this chart manages. | `""` |
| `postgres.diskSize` | `string` | Size of the database volume. | `"10Gi"` |
| `postgres.resources` | `object` | CPU and memory for the database pod. | 500m/2Gi – 2000m/3Gi |
| `postgres.env` | `object` | Extra environment variables for postgres, e.g. `IGNORE_DATABASE_FSTYPE` for non-local storage. | `{}` |
| `postgres.startupProbe` | `object` | `periodSeconds`/`failureThreshold` budget for initdb plus the vchord preload. | 5s × 36 = 180s |
| `redis.name` | `string` | Service name labelled on the redis subchart's alerts. Required — see *Redis* below. | `"immich"` |
| `ingress.enabled` | `bool` | Create an Ingress for the web UI/API. | `false` |
| `ingress.className` | `string` | IngressClass name. | `""` |
| `ingress.host` | `string` | Hostname. Required when the ingress is enabled. | `""` |
| `ingress.annotations` | `object` | Ingress annotations. Defaults to ingress-nginx annotations lifting the body-size/timeout limits large uploads need — see *Ingress upload limits* below. | see below |
| `ingress.tlsSecretName` | `string` | Existing TLS secret for the host. | `""` |

### Why a separate Postgres, not the zopdev postgres chart

Immich's search features (`smart search`, duplicate detection) need a
vector extension. Upstream ships their own Postgres image with it
built in (`ghcr.io/immich-app/postgres`) — the zopdev `postgres` chart
runs plain bitnami Postgres, which doesn't have it, so this chart
brings its own Postgres StatefulSet instead of depending on that one.

### Machine learning is always deployed

There's no toggle to turn it off. Upstream's own reference
docker-compose always runs it too — the server calls it internally
for face detection and smart search, and it needs no configuration of
its own.

### Redis

Reuses the zopdev `redis` chart as the job queue backing Immich's
background workers (thumbnail generation, metadata extraction, etc.).

`redis.name` labels the subchart's own `PrometheusRule` — it has no
default of its own, and an unset value renders a null label the CRD
rejects, failing the whole install. This chart sets it for you; there
should be no need to change it.

### Database password

Left unset, `postgres.password` is auto-generated and stored in a
Secret annotated `helm.sh/resource-policy: keep`, so it survives
`helm uninstall` alongside the postgres PVC (see *Uninstall* above) —
a reinstall against that same retained volume still authenticates.

Set `postgres.password` to pin a known password instead — e.g. to
restore into a fresh volume from a backup taken under a specific
password. Set `postgres.existingSecret` (name of a Secret you manage,
carrying a `POSTGRES_PASSWORD` key) to skip this chart's own Secret
entirely; it takes precedence over `postgres.password`.

Once postgres has initialized with a given password, changing
`postgres.password` on a running release rotates the Secret without
touching the already-initialized database — the server picks up the
new value and can no longer authenticate. Treat it the same as
`postgres.username`/`postgres.database`: set it before the first
install, not after.

Under `helm template`, `--dry-run`, or a GitOps renderer without live
cluster access, a fresh password is generated on every render rather
than the stored one being reused, because the chart can only find the
existing Secret by querying the cluster directly (`lookup`), which
those tools don't give it — applying such a render would rotate the
Secret against a volume that still holds the old password. This does
not affect the real `helm install`/`helm upgrade` path (including a
reinstall after `helm uninstall`, which is what the paragraph above
covers): those always run against the live cluster, so `lookup` always
finds and reuses the stored password there. Pin `postgres.password` or
`postgres.existingSecret` if you render manifests without cluster
access.

### Ingress upload limits

`ingress.annotations` defaults to ingress-nginx annotations that lift
its own defaults (1m body size, 60s read/send timeouts) — Immich
[recommends](https://docs.immich.app/administration/reverse-proxy) no
body-size limit and 600s timeouts to cover multi-gigabyte video
uploads. These are no-ops on a non-nginx ingress controller; override
`ingress.annotations` entirely with your controller's own equivalents
if you're not running ingress-nginx.

### Minimum node size

Summed across all four components, the defaults request **2.5 CPU /
5.3Gi** and allow bursting to **9.5 CPU / 12.1Gi** — machine learning
alone requests 1 CPU / 2Gi. A 2-CPU node cannot schedule this
release. Size accordingly, or lower `machineLearning.resources` if
inference latency isn't a concern.

---

## Example `values.yaml`

```yaml
server:
diskSize: "200Gi"
resources:
requests:
cpu: "1000m"
memory: "2Gi"
limits:
cpu: "4000m"
memory: "8Gi"

ingress:
enabled: true
className: nginx
host: photos.example.com
```

```bash
helm install my-immich zopdev/immich -f values.yaml
```

---

## Features

- All four components from Immich's own reference deployment: server, machine learning, vector-extension Postgres, Redis
- Persistent volumes for the photo/video library, the database, and the ML model cache
- Ingress with optional TLS — point the Immich mobile app at that URL for auto-backup
- Configurations that cannot work (a bad ingress setup) are rejected when the chart renders, naming the cause, rather than installing and crash-looping

---

## Connection Config

The web UI, REST API, and mobile app all talk to `server.service.port`
(2283) of the `<release>-immich-server` service.

```bash
kubectl port-forward svc/my-immich-immich-server 2283:2283
open http://localhost:2283
```

- **`/`** — the web UI.
- **`/api/...`** — the REST API the mobile app and web UI both use.
- **`/api/server/ping`** — readiness and liveness.

---

## Contributing

We welcome contributions to improve this Helm chart. Please refer to the
[CONTRIBUTING.md](../../CONTRIBUTING.md) file for contribution
guidelines.

---

## Code of Conduct

To maintain a healthy and collaborative community, please adhere to our
[Code of Conduct](../../CODE_OF_CONDUCT.md).

---

## License

This project is licensed under the [LICENSE](../../LICENSE). Please
review it for terms of use.
20 changes: 20 additions & 0 deletions charts/immich/templates/NOTES.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Immich is installed as release {{ .Release.Name }}.

Reach the web UI / mobile app server:

kubectl port-forward -n {{ .Release.Namespace }} \
svc/{{ include "immich.serverFullname" . }} 2283:{{ .Values.server.service.port }}

open http://localhost:2283
{{- if .Values.ingress.enabled }}

Or over the ingress at http{{ if .Values.ingress.tlsSecretName }}s{{ end }}://{{ .Values.ingress.host }}

Point the Immich mobile app at that URL to start auto-backup.
{{- end }}

First run: open the URL above and create the admin account -- Immich
has no default login.

Machine learning (face detection, smart search) runs as a separate
internal component and needs no configuration.
Loading
Loading