# Self-Hosting Immich for Photos and Videos

> Octelium documentation. Canonical page: <https://octelium.com/docs/octelium/latest/management/guide/service/homelab/immich-helm-self-host>.

[Immich](https://immich.app/) is a popular open source, self-hosted photo and video management solution, and a common self-hosted alternative to Google Photos and iCloud Photos. This guide shows you how to deploy Immich via its official Helm chart on the same underlying Kubernetes cluster that is running the Octelium *Cluster*, and then serve it as an Octelium *Service*. Octelium provides the following:

- Private client-based access over WireGuard/QUIC via the `octelium connect` command (read more [here](https://octelium.com/docs/octelium/latest/user/cli/connect.md)) from your laptops and desktops, including bulk uploads via the Immich CLI.
- Public clientless BeyondCorp access (read more [here](https://octelium.com/docs/octelium/latest/management/core/service/clientless.md)) from any web browser, where your *Users* authenticate via OpenID Connect, SAML 2.0 or GitHub *IdentityProviders* (read more [here](https://octelium.com/docs/octelium/latest/management/core/identity-providers.md)) as well as Passkeys before they are able to reach Immich at all.
- Clientless access for the Immich mobile app, including automatic background backups, via per-device access tokens.
- Immich's own login and onboarding pages are never exposed to anonymous internet traffic.
- Identity-based, L7 aware access control on a per-request basis according to the HTTP request's path, method, headers, etc... via policy-as-code with CEL and OPA (read more about *Policies* and access control [here](https://octelium.com/docs/octelium/latest/management/core/policy.md)).
- OpenTelemetry-native, identity-based, L7 aware visibility and auditing (read more [here](https://octelium.com/docs/octelium/latest/management/core/visibility.md)).

> **Note:**
>
> If you installed the *Cluster* via the quick installation guide, you can simply use the command `export KUBECONFIG="/etc/rancher/k3s/k3s.yaml"` in your *Cluster* VM/VPS before running the `kubectl` and `helm` commands below.

## Install Immich

Immich consists of a server, a machine learning container that powers the smart search and facial recognition features, a Valkey (i.e. Redis-compatible) instance and a PostgreSQL database with the [VectorChord](https://github.com/tensorchord/VectorChord) extension. The official [Immich Helm chart](https://github.com/immich-app/immich-charts) deploys the first three components, while it expects you to provide the PostgreSQL database as well as the persistent volume that stores your photo and video library yourself.

### Install CloudNativePG

The Immich chart recommends deploying the PostgreSQL database via [CloudNativePG](https://cloudnative-pg.io/), the open source PostgreSQL operator for Kubernetes. First, we install the CloudNativePG operator via its Helm chart as follows:

```bash
helm repo add cnpg https://cloudnative-pg.github.io/charts
helm repo update
helm upgrade --install cnpg --namespace cnpg-system --create-namespace cnpg/cloudnative-pg
```

### Create the Database and the Library Volume

Now we create a dedicated Kubernetes namespace for Immich as follows:

```bash
kubectl create namespace immich
```

Now we define the persistent volume claim of the Immich library as well as the PostgreSQL database in a `storage.yaml` file as follows:

```yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: immich-library
  namespace: immich
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 100Gi
---
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: immich-database
  namespace: immich
spec:
  instances: 1
  # !mark
  imageName: ghcr.io/tensorchord/cloudnative-vectorchord:18.6-1.1.1
  postgresql:
    shared_preload_libraries:
      - vchord.so
  bootstrap:
    initdb:
      database: immich
      owner: immich
      # !mark(1:3)
      postInitApplicationSQL:
        - CREATE EXTENSION IF NOT EXISTS vchord CASCADE;
        - CREATE EXTENSION IF NOT EXISTS earthdistance CASCADE;
  storage:
    size: 10Gi
```

The [cloudnative-vectorchord](https://github.com/tensorchord/cloudnative-vectorchord) image is the CloudNativePG PostgreSQL image with the VectorChord extension installed, which is loaded via the `shared_preload_libraries` field. The `postInitApplicationSQL` queries are executed once as the PostgreSQL superuser upon the creation of the database. They create the extensions that require superuser permissions, so that Immich itself can connect as the ordinary `immich` owner of the database without needing superuser permissions (read more [here](https://docs.immich.app/administration/postgres-standalone#without-superuser-permission)). CloudNativePG automatically generates the database password and stores the connection details in the `immich-database-app` Kubernetes secret.

Now apply the creation of both resources and wait for the database to become ready as follows:

```bash
kubectl apply -f /PATH/TO/STORAGE.YAML
kubectl wait --for=condition=Ready cluster/immich-database -n immich --timeout=10m
```

> **Note:**
>
> Both the persistent volume claim and the database use the default storage class of your Kubernetes cluster. For example, the k3s cluster installed by the quick installation guide uses the `local-path` storage class which stores the volumes on the node's own disk under the `/var/lib/rancher/k3s/storage` directory. If you want to store your library somewhere else, such as on a NAS, you can explicitly set the `storageClassName` field of the persistent volume claim.

> **Note:**
>
> Immich's built-in automatic database backups use `pg_dumpall`, which requires superuser permissions (read more [here](https://docs.immich.app/administration/postgres-standalone)). Since Immich connects as an ordinary database owner in this setup, you should instead back up the database via CloudNativePG's own backup mechanisms (read more [here](https://cloudnative-pg.io/documentation/current/backup/)).

### Install the Immich Chart

Now we create a `values.yaml` file for the Immich Helm chart (see the chart [here](https://github.com/immich-app/immich-charts/tree/main/charts/immich)) that uses the library volume, enables the bundled Valkey instance, and reads the database connection details from the `immich-database-app` secret as follows:

```yaml
controllers:
  main:
    containers:
      main:
        image:
          tag: v3.2.0

immich:
  persistence:
    library:
      existingClaim: immich-library

valkey:
  enabled: true

server:
  controllers:
    main:
      containers:
        main:
          env:
            DB_HOSTNAME:
              valueFrom:
                secretKeyRef:
                  name: immich-database-app
                  key: host
            DB_USERNAME:
              valueFrom:
                secretKeyRef:
                  name: immich-database-app
                  key: user
            DB_PASSWORD:
              valueFrom:
                secretKeyRef:
                  name: immich-database-app
                  key: password
            DB_DATABASE_NAME:
              valueFrom:
                secretKeyRef:
                  name: immich-database-app
                  key: dbname
```

Now install the chart and wait for the Immich server to become ready as follows:

```bash
helm install immich oci://ghcr.io/immich-app/immich-charts/immich --version 0.13.2 --namespace immich -f /PATH/TO/VALUES.YAML
kubectl rollout status deployment/immich-server -n immich --timeout=10m
```

> **Note:**
>
> The Immich chart does not update its default image tag with every Immich release. Therefore, it is recommended to explicitly pin the Immich version via the `image.tag` value as shown above, and to upgrade it deliberately after reading the Immich release notes. Note that all the Immich components share the same version.

> **Note:**
>
> The machine learning container is the most resource-hungry component of Immich. If you are running the *Cluster* on a small VM or a single-board computer, you can disable it via the `machine-learning.enabled` value at the cost of losing smart search and facial recognition.

The chart creates the `immich-server` Kubernetes service in the `immich` Kubernetes namespace which listens over the port `2283`.

## Create the Service

Now we create an Octelium *Service* for the Immich server whose upstream is the `immich-server` Kubernetes service. We also define a `family` *Group* (read more about *Groups* [here](https://octelium.com/docs/octelium/latest/management/core/group.md)) where only its members are allowed to access the *Service* as follows:

```yaml
kind: Group
metadata:
  name: family
spec: {}
---
kind: Service
metadata:
  name: immich
spec:
  mode: WEB
  isPublic: true
  config:
    upstream:
      url: http://immich-server.immich.svc:2283
  authorization:
    inlinePolicies:
      - spec:
          rules:
            - effect: ALLOW
              condition:
                match: '"family" in ctx.user.spec.groups'
```

The `isPublic` field enables the clientless BeyondCorp access to the *Service* (read more [here](https://octelium.com/docs/octelium/latest/management/core/service/clientless.md)), while the client-based access is always available. The `WEB` mode simply marks the *Service* as a web app in the web *Portal* (read more [here](https://octelium.com/docs/octelium/latest/management/core/service/http.md#web-app-mode)) and is otherwise identical to the `HTTP` mode. The *Service* port is automatically inferred from the upstream URL, which means that the *Service* also listens over the port `2283` (read more [here](https://octelium.com/docs/octelium/latest/management/core/service/overview.md#port)).

Now add your *Users* to the `family` *Group*. Here is an example:

```yaml
kind: User
metadata:
  name: john
spec:
  type: HUMAN
  email: john@example.com
  # !mark
  groups: ["family"]
```

You can now apply the creation of the *Group*, the *Service* and the *Users* via the `octeliumctl apply` command (read more [here](https://octelium.com/docs/octelium/latest/management/core/overview.md)) as follows:

```bash
octeliumctl apply /PATH/TO/YAML_FILE_OR_PARENT_DIRECTORY
```

> **Note:**
>
> We intentionally keep the default HTTP header behavior of the *Service*. By default, the *Service* drops all the `Forwarded`, `X-Forwarded-*` and `X-Real-IP` request headers (read more [here](https://octelium.com/docs/octelium/latest/management/core/service/http.md#forwarded-header)). Immich trusts the `X-Forwarded-Proto` header of proxies with private IP addresses by default. For a public *Service*, enabling the `OBFUSCATE` forwarded mode or adding your own `X-Forwarded-Proto: https` header would make Immich believe that every request, including the client-based plaintext HTTP requests, arrives over HTTPS. Immich would then set its session cookies with the `Secure` attribute, which browsers refuse to store over plaintext HTTP, and you could no longer login via the client-based mode.

> **Note:**
>
> By default, neither the *Cluster*'s [*Ingress*](https://octelium.com/docs/octelium/latest/reference/components.md#ingress) nor the *Service* limits the size of the request body or enforces a total timeout per request, and WebSocket connections, which Immich uses for real-time updates, are supported out of the box. Therefore, you do not need to tune any upload limits for your large videos as you would typically do with many reverse proxies. However, you should not enable the request body buffering (read more [here](https://octelium.com/docs/octelium/latest/management/core/service/http.md#request-body)) for this *Service*, since it buffers the entire body of every upload in memory.

Now open Immich, either via the client-based or the clientless mode as shown in the following sections, and create the Immich admin account via the onboarding page. Unlike exposing Immich directly to the internet, only your authorized *Users* are able to reach that onboarding page in the first place. You might also want to set the URL `https://immich.<DOMAIN>` in Immich's **Administration** > **Settings** > **Server Settings** > **External domain** setting, so that Immich uses the public URL of the *Service* in its shared links and email notifications.

## Client-Based Access

Once connected to the *Cluster* via the `octelium connect` command (read more [here](https://octelium.com/docs/octelium/latest/user/cli/connect.md)), you can simply visit Immich from your browser at the URL `http://immich:2283`.

You can also connect via the rootless mode and map the *Service* to your host's localhost (read more [here](https://octelium.com/docs/octelium/latest/user/cli/connect.md#rootless)) as follows:

```bash
# Connect as non-root/unprivileged OS user
octelium connect -p immich:2283

# Now access the Service at localhost
curl http://localhost:2283/api/server/ping
```

The client-based mode is especially useful for bulk uploads of your existing photo and video library from your laptop or desktop via the Immich CLI (read more [here](https://docs.immich.app/features/command-line-interface)). First, create an Immich API key via your Immich **Account Settings** > **API Keys**, and then login and upload your directory as follows:

```bash
npm i -g @immich/cli

immich login http://immich:2283/api <IMMICH_API_KEY>
immich upload --recursive /PATH/TO/PHOTOS
```

The Immich CLI authenticates to Immich via the `x-api-key` header which is passed as is to Immich, while the *Cluster* authenticates your connected *Session* itself. You can also use the `--watch` flag to keep watching the directory and automatically upload new files.

## Clientless Access

### Web Browsers

Authorized `HUMAN` *Users* can access Immich via the clientless BeyondCorp mode directly from their browsers at the URL `https://immich.<DOMAIN>` without having to install any client. An unauthenticated *User* is first redirected to the *Cluster*'s login page in order to authenticate via an *IdentityProvider* (read more [here](https://octelium.com/docs/octelium/latest/management/core/identity-providers.md)). There are currently 3 types:

- GitHub OAuth *IdentityProvider* as shown in detail [here](https://octelium.com/docs/octelium/latest/management/core/identity-providers.md#github-oauth2)
- OpenID Connect *IdentityProviders* (e.g. Okta, Auth0, etc...) as shown [here](https://octelium.com/docs/octelium/latest/management/core/identity-providers.md#openid-connect).
- SAML 2.0 *IdentityProviders* (e.g. Okta, Entra ID, etc...) as shown [here](https://octelium.com/docs/octelium/latest/management/core/identity-providers.md#saml-20).

Furthermore, you can register a FIDO2 *Authenticator* (e.g. Yubikeys) in order to directly login later via Passkey (read more [here](https://octelium.com/docs/octelium/latest/management/core/authenticator.md#passkey-login)) without having to use an *IdentityProvider*.

Once authenticated to the *Cluster*, the *User* then logs in to Immich itself with their Immich account.

> **Note:**
>
> Immich also supports OAuth/OpenID Connect login (read more [here](https://docs.immich.app/administration/oauth)). You can use the same self-hosted identity provider that you use to login to the *Cluster*, such as Authentik (read more [here](https://octelium.com/docs/octelium/latest/management/guide/service/homelab/authentik-helm-self-host.md)) or Keycloak (read more [here](https://octelium.com/docs/octelium/latest/management/guide/service/homelab/keycloak-helm-self-host.md)), to login to Immich as well.

### Mobile App

The Immich mobile app is a native app rather than a web browser, so it cannot go through the *Cluster*'s browser-based login. Instead, every phone can use its own access token *Credential* (read more [here](https://octelium.com/docs/octelium/latest/management/core/credential.md#access-tokens)) that the app sends with each request via a custom header. Access token *Credentials* are issued to `WORKLOAD` *Users* (read more about *User* types [here](https://octelium.com/docs/octelium/latest/management/core/user.md#types)), so we create a dedicated *User* for every phone as follows:

```yaml
kind: User
metadata:
  name: john-phone
spec:
  type: WORKLOAD
  groups: ["family"]
  # !mark(1:5)
  session:
    clientlessDuration:
      days: 90
    accessTokenDuration:
      days: 90
  authorization:
    inlinePolicies:
      - spec:
          rules:
            - effect: DENY
              condition:
                match: ctx.service.metadata.name != "immich.default"
```

By default, an access token is only valid for 4 hours, which is why we explicitly set the *Session* and access token durations of the *User* (read more [here](https://octelium.com/docs/octelium/latest/management/core/user.md#session)). The inline *Policy* makes sure that the phone's identity can only be used to access the `immich` *Service* and nothing else in the *Cluster*, even if the phone is lost or stolen. After applying the *User* via `octeliumctl apply`, create the access token *Credential* as follows:

```bash
octeliumctl create cred --type access-token --user john-phone john-phone
```

Now configure the Immich app on the phone as follows:

1. On the login screen of the app, tap on **Settings**, then go to **Advanced** > **Custom proxy headers**.
2. Add a header whose name is `X-Octelium-Auth` and whose value is the access token.
3. Go back to the login screen, set the server URL to `https://immich.<DOMAIN>` and then login with your Immich account.

The app sends the custom header with every request, including the automatic background backups. The *Cluster* authenticates the request via the `X-Octelium-Auth` header and then removes it before the request reaches Immich, while Immich keeps authenticating the app via its own session cookie.

> **Note:**
>
> We use the `X-Octelium-Auth` header (read more [here](https://octelium.com/docs/octelium/latest/management/core/service/clientless.md#oauth2-client-credentials)) rather than the `Authorization` header in order to keep the *Cluster*'s credentials completely separate from Immich's own. All `X-Octelium-*` headers as well as the *Cluster*'s own cookies are always removed before the request is forwarded to the upstream.

Once the access token expires, or whenever you suspect that it has leaked, you can rotate the *Credential* (read more [here](https://octelium.com/docs/octelium/latest/management/core/credential.md)) and update the header value in the app as follows:

```bash
octeliumctl create cred --rotate john-phone
```

You can also immediately revoke the phone's access by disabling its *User* (read more [here](https://octelium.com/docs/octelium/latest/management/core/user.md#disabling-users)).

## Access Control

Since every request to Immich is authorized by the *Service*, you can also control access at the HTTP layer on top of Immich's own permissions (read more about *Policies* and access control [here](https://octelium.com/docs/octelium/latest/management/core/policy.md)). Here is an example where the members of the `guests` *Group* (e.g. relatives that you share some albums with) can browse Immich, but are not allowed to upload or delete assets via the Immich API:

```yaml
kind: Group
metadata:
  name: guests
spec: {}
---
kind: Service
metadata:
  name: immich
spec:
  mode: WEB
  isPublic: true
  config:
    upstream:
      url: http://immich-server.immich.svc:2283
  authorization:
    inlinePolicies:
      - spec:
          rules:
            - effect: ALLOW
              condition:
                any:
                  of:
                    - match: '"family" in ctx.user.spec.groups'
                    - match: '"guests" in ctx.user.spec.groups'
            # !mark(1:7)
            - effect: DENY
              condition:
                all:
                  of:
                    - match: '"guests" in ctx.user.spec.groups'
                    - match: ctx.request.http.path == "/api/assets"
                    - match: ctx.request.http.method in ["POST", "DELETE"]
```

Uploading an asset is a `POST` request and deleting assets is a `DELETE` request to the `/api/assets` path. Since `DENY` rules override `ALLOW` rules of the same priority (read more [here](https://octelium.com/docs/octelium/latest/management/core/policy.md#policy-priority)), such requests of the `guests` *Group* members are denied before they reach Immich. You can read more about HTTP-specific *Policies* [here](https://octelium.com/docs/octelium/latest/management/core/service/http.md#access-control).

## Visibility

Octelium also provides OpenTelemetry-ready, application-layer L7 aware visibility and access logging in real time (read more about visibility [here](https://octelium.com/docs/octelium/latest/management/core/visibility.md)). Here is an example of an access *Log* of a photo uploaded by the background backup of the `john-phone` *User*:

```yaml
{
  "apiVersion": "core/v1",
  "entry": {
    "common": {
      // Omitted for the sake of brevity of the example
    },
    # !mark(1:16)
    "info": {
      "http": {
        "request": {
          "path": "/api/assets",
          "userAgent": "immich-android/3.2.0",
          "method": "POST",
          "uri": "/api/assets"
        },
        "response": {
          "code": 201,
          "bodyBytes": "64",
          "contentType": "application/json; charset=utf-8"
        },
        "httpVersion": "HTTP11"
      }
    }
  },
  "kind": "AccessLog",
  "metadata": {
    // Omitted for the sake of brevity of the example
  }
}
```

Here are a few more features that you might be interested in:

- Using Octelium as a self-hosted exit node and VPN for your devices (read more [here](https://octelium.com/docs/octelium/latest/management/guide/service/homelab/self-hosted-vpn-exit-node-socks5.md)).
- Deploying your own containers as *Services* (read more about managed containers [here](https://octelium.com/docs/octelium/latest/management/core/service/managed-containers.md)).
- Request/response header manipulation (read more [here](https://octelium.com/docs/octelium/latest/management/core/service/http.md#header-manipulation)).
- Dynamic routing to different upstreams according to identity and context (read more [here](https://octelium.com/docs/octelium/latest/management/core/service/dynamic-config.md)).
- Exposing your homelab's DNS server, such as Pi-hole, to all of your connected *Users* (read more [here](https://octelium.com/docs/octelium/latest/management/guide/service/homelab/pihole.md)).
