Skip to content
Draft
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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
152 changes: 152 additions & 0 deletions content/docs/guides/immich.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
---
title: Secure Immich with Pomerium
sidebar_label: Immich
lang: en-US
keywords:
[pomerium, immich, sso, oidc, identity aware proxy, self-hosted, photos]
description: Put self-hosted Immich behind Pomerium so every browser request is authenticated and authorized at the front door before it reaches your photo library.
# cSpell:ignore immich vectorchord valkey
---

import TabItem from '@theme/TabItem';
import Tabs from '@theme/Tabs';

import Config from '/content/examples/guides/immich/config.yaml.md';
import Compose from '/content/examples/guides/immich/docker-compose.yaml.md';

# Secure Immich with Pomerium

## What this guide does

Put a self-hosted [Immich](https://immich.app/) instance behind Pomerium so every browser request to its web interface is authenticated against your identity provider (IdP) and checked against your route policy before it reaches Immich. You get centralized single sign-on (SSO), one place to express group and domain policy, and an audit trail of who reached the service. Immich keeps its own login and accounts on top; Pomerium decides who can reach them at all.

```mermaid
flowchart LR
Browser --> Pomerium["Pomerium<br/>SSO + route policy"]
Pomerium -.->|"sign in"| IdP[Identity provider]
Pomerium --> Immich["Immich<br/>web + API"]
Mobile["Mobile / API client"] -->|"access token"| Immich
```

## When to use this guide

Use it when you run self-hosted Immich and don't want its web interface exposed directly to the internet. Pomerium makes the access decision; Immich continues to manage albums, sharing, and its own user sessions behind it.

This guide covers the web interface. Immich's mobile apps and API clients authenticate with Immich's own access tokens and never perform a browser SSO redirect, so a route that requires Pomerium SSO blocks them. Either serve the API or mobile hostname on a separate [public access](/docs/reference/routes/public-access) route (not identity-protected, so Immich's token auth is then the only control), or keep that host off Pomerium and reach it over a virtual private network (VPN). [Security considerations](#security-considerations) walks through the trade-offs.

## Prerequisites

- [Docker](https://docs.docker.com/install/) and [Docker Compose](https://docs.docker.com/compose/install/)
- For the Pomerium Zero path: a [Pomerium Zero](https://console.pomerium.app) account with its Pomerium instance running locally via the [Quickstart](/docs/get-started/quickstart) Compose file; the route uses the starter domain that comes with it
- For the Pomerium Core path: a domain you control for the Immich route (this guide uses `immich.yourdomain.com`), with DNS pointed at the host running Pomerium and ports 80 and 443 reachable so `autocert` can provision certificates; the Compose file below runs Pomerium itself

:::tip Prefer to self-host the identity provider?

This guide uses the hosted authenticate service so you don't have to run an IdP. To run your own instead, follow [Keycloak + Pomerium](/docs/integrations/user-identity/oidc) and swap the `authenticate_service_url` / `idp_*` settings into the config below.

:::

## Configure Pomerium

<Tabs queryString="type">
<TabItem value="zero" label="Pomerium Zero" default>

In the [Zero Console](https://console.pomerium.app):

1. Create a **Route**. In **From**, enter `https://immich.<your-starter-domain>`; in **To**, enter `http://immich-server:2283`.
2. Set the policy to scope access to who should reach Immich (for example, **Any Authenticated User** or a specific group or domain).
3. On the route's settings, enable **Allow WebSockets** and **Preserve Host Header**. Immich's reverse-proxy guidance expects WebSocket upgrades and the original host to reach the server.
4. Raise the request **Timeout** (or set it to 0 for no limit), and set a generous **Idle Timeout** such as `600s`, so large photo and video uploads and original-quality downloads are not cut off mid-transfer.

Zero manages the route's TLS certificate behind your starter domain, so there's nothing else to configure on the Pomerium side.

</TabItem>
<TabItem value="core" label="Pomerium Core">

Create a `config.yaml`. It routes `immich.yourdomain.com` to the Immich server container, preserves the public host header, allows WebSockets, and lifts the request time limit so large uploads and downloads are not cut off.

<Config />

Replace `immich.yourdomain.com` with your domain and `you@example.com` with the email (or switch to a group or domain match) that should be allowed through.

</TabItem>
</Tabs>

## Configure Immich

Immich keeps its own login and account database. The first time you reach it through Pomerium, Immich serves its own onboarding screen where you create the initial admin account; after that it enforces its own sessions on top of Pomerium's SSO.

This minimal stack includes what the Immich web/API server needs to boot behind Pomerium:

- A connection to its database. Immich ships a specific PostgreSQL image that bundles the vector-search extension it depends on. Set `DB_HOSTNAME`, `DB_USERNAME`, `DB_PASSWORD`, and `DB_DATABASE_NAME` to match the database service, and set the matching `POSTGRES_USER` / `POSTGRES_PASSWORD` / `POSTGRES_DB` on the database.
- A connection to Redis (Valkey) for its job queue, via `REDIS_HOSTNAME`.
- A media volume for uploaded photos and videos.

Generate a unique database password and use the same value for `DB_PASSWORD` and `POSTGRES_PASSWORD`.

It does not include Immich's `immich-machine-learning` service; add it from the official Immich release Compose file if you need smart search or face recognition.

:::caution Use Immich's database image, not stock Postgres

Immich requires the [VectorChord](https://docs.immich.app/administration/postgres-standalone/) PostgreSQL image it ships (`ghcr.io/immich-app/postgres:...` in the Compose file). A stock `postgres` image crashes `immich-server` on first boot because the vector-search extension is missing. Pin the exact image Immich publishes for your release.

:::

## Run the stack

The Compose file runs Pomerium Core together with the Immich server, its VectorChord PostgreSQL database, and Redis (Valkey). Pomerium publishes ports 80 and 443 for the protected route; the Immich server publishes no host ports, and Pomerium reaches it by service name on the Compose project network. For Zero, drop the `pomerium` service and use the `compose.yaml` from the [Quickstart](/docs/get-started/quickstart) with your `POMERIUM_ZERO_TOKEN`, keeping the Immich services below; put those services on the same Docker network as the Quickstart's `pomerium` service (the Quickstart names it `main`) so Pomerium can resolve `immich-server` by name.

<Compose />

Start it:

```bash
docker compose up -d
```

On first boot the Immich server runs database migrations against the VectorChord database, which can take a minute or two before it answers requests. Watch `docker compose ps` until `immich-server` reports `healthy`, or follow `docker compose logs -f immich-server`.

To stop the stack, run `docker compose down`. Add `-v` only if you mean it: that flag deletes the named volumes, which wipes the Immich database and your entire photo library.

This guide was last tested with Immich v2.7.5 and Pomerium v0.32.7. Newer Immich releases pin a different VectorChord PostgreSQL image, so take the database image from the [release matching your Immich version](https://github.com/immich-app/immich/releases) when you upgrade.

## Verify the setup

1. **The route requires authentication.** In a fresh browser, open `https://immich.yourdomain.com`. You should be redirected to sign in through Pomerium, not straight to Immich.
2. **An allowed user gets in.** Sign in with a user your policy allows. Pomerium redirects you back and Immich's own onboarding or login screen loads.

![Immich's web login reached through Pomerium](./img/immich/immich-login.png)

3. **Immich answers through Pomerium.** In the same browser where you signed in, open `https://immich.yourdomain.com/api/server/ping`. The browser carries your Pomerium session, so the request reaches Immich and returns `{"res":"pong"}`. A plain `curl` with no Pomerium session is redirected to sign in instead. On Pomerium Zero or Enterprise, you can script the check by attaching a [Pomerium service account](/docs/capabilities/service-accounts) token that your route policy allows.

Pomerium decides who reaches Immich; Immich runs its own login on top. Immich's admin onboarding and first-run setup are Immich's concern, not Pomerium's.

## Common failure modes

- **`502` or `503` right after `docker compose up`.** The Immich server is still running first-boot database migrations. Wait for the container to report `healthy`; watch `docker compose logs -f immich-server`.
- **`immich-server` restarts in a crash loop with a missing-extension error.** The database is a stock Postgres image instead of Immich's VectorChord image. Switch the database service to the `ghcr.io/immich-app/postgres:...` image Immich ships. If this happened on a fresh test stack, remove the database volume and reinitialize it; for an existing library, back up the database and follow Immich's migration or recovery docs before deleting any volume.
- **Uploads of large videos fail or time out.** The proxy is cutting the request short. Set the route `timeout` to `0s` (and a generous `idle_timeout`) as shown in the config so long transfers aren't capped.
- **Redirect loop or certificate errors.** Make sure DNS for `immich.yourdomain.com` points at Pomerium and that Pomerium can obtain a TLS certificate. On the Core path, `autocert` needs ports 80 and 443 reachable for Let's Encrypt; Zero manages certificates for you.

## Security considerations

Immich runs its own authentication, so this guide puts Pomerium in front of that login as the front door. (Immich can also sign users in itself with native OIDC against your identity provider; see [Next steps](#next-steps).) The trust boundary depends on Pomerium being the only path to the web interface, and on understanding which clients the route actually protects.

- **Don't expose Immich directly.** Only Pomerium should reach `immich-server:2283`. Keep the Immich server off published host ports and on the Docker network Pomerium uses to reach it, so the policy can't be bypassed by hitting Immich's port directly.
- **Scope the route policy** (group or domain) to who should have any access to Immich at all. Immich's own per-user albums and sharing still apply on top of that.
- **Pomerium's SSO covers the web browser, not token-based clients.** The Immich mobile apps and API or command-line clients authenticate with their own Immich access tokens and never perform a browser SSO redirect. If the host the mobile app talks to requires Pomerium SSO, the app breaks: it has no way to satisfy the interactive redirect. Make a deliberate choice for that host. Either serve the API or mobile hostname on a separate route with [public access](/docs/reference/routes/public-access), or keep that host off Pomerium entirely and reach it over a VPN. On a public-access route, Pomerium will not authenticate or authorize matching requests, so Immich's own token auth is the only control; scope the host or path as narrowly as Immich supports.

Each access channel is protected by a different control:

| Access channel | What gates it | Credential the client presents |
| --- | --- | --- |
| Web interface in a browser | Pomerium route policy, then Immich's own login | Pomerium SSO session, then an Immich session |
| Mobile apps | Immich's own authentication (reach it over a public access route or a VPN) | Immich access token from the app's login |
| API and CLI clients | Immich's own authentication (reach it over a public access route or a VPN) | Immich API key |

## Next steps

- **Go further: single sign-on into Immich.** Immich supports [native OIDC](https://docs.immich.app/administration/oauth/). Point it at the same identity provider Pomerium uses, turn on Auto Register, and users land already signed in instead of meeting a second Immich login. This is Immich doing its own OIDC, not something Pomerium forwards, so it also covers the mobile app's login (which Pomerium's browser-based SSO cannot).
- [Build policies](/docs/get-started/fundamentals/zero/zero-build-policies)
- [Set route timeouts](/docs/reference/routes/timeouts)
- [Custom domains](/docs/capabilities/custom-domains)
23 changes: 23 additions & 0 deletions content/examples/guides/immich/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Pomerium Core configuration for Immich. Uses the hosted authenticate service, so
# you don't run your own identity provider. To self-host the IdP, see the Keycloak
# guide: https://www.pomerium.com/docs/integrations/user-identity/oidc
authenticate_service_url: https://authenticate.pomerium.app

# Obtain TLS certificates automatically from Let's Encrypt.
autocert: true

routes:
- from: https://immich.yourdomain.com
to: http://immich-server:2283
allow_websockets: true
preserve_host_header: true
# Photo and video uploads (and original-quality downloads) can be large and
# long-running, so don't cap total request time at the proxy.
timeout: 0s
idle_timeout: 600s
policy:
- allow:
or:
- email:
# Replace with the users or domain you want to allow
is: you@example.com
25 changes: 25 additions & 0 deletions content/examples/guides/immich/config.yaml.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
```yaml title="config.yaml"
# Pomerium Core configuration for Immich. Uses the hosted authenticate service, so
# you don't run your own identity provider. To self-host the IdP, see the Keycloak
# guide: https://www.pomerium.com/docs/integrations/user-identity/oidc
authenticate_service_url: https://authenticate.pomerium.app

# Obtain TLS certificates automatically from Let's Encrypt.
autocert: true

routes:
- from: https://immich.yourdomain.com
to: http://immich-server:2283
allow_websockets: true
preserve_host_header: true
# Photo and video uploads (and original-quality downloads) can be large and
# long-running, so don't cap total request time at the proxy.
timeout: 0s
idle_timeout: 600s
policy:
- allow:
or:
- email:
# Replace with the users or domain you want to allow
is: you@example.com
```
49 changes: 49 additions & 0 deletions content/examples/guides/immich/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
services:
pomerium:
image: pomerium/pomerium:v0.32.7@sha256:e10d1d267af24f581157f485d9b0bc08469e2428675b696a08e42ceb09b2279c
volumes:
- ./config.yaml:/pomerium/config.yaml:ro
- pomerium-cache:/data
ports:
- 443:443
- 80:80
restart: always

immich-server:
image: ghcr.io/immich-app/immich-server:v2.7.5@sha256:c15bff75068effb03f4355997d03dc7e0fc58720c2b54ad6f7f10d1bc57efaa5
environment:
DB_HOSTNAME: database
DB_USERNAME: postgres
DB_PASSWORD: change-this-database-password
DB_DATABASE_NAME: immich
REDIS_HOSTNAME: redis
volumes:
- immich-library:/data
depends_on:
- database
- redis
restart: always

# Immich requires this VectorChord/pgvecto-rs Postgres image. A stock
# postgres image crashes immich-server on first boot because the vector
# search extension is missing, so pin the exact image Immich ships.
database:
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:bcf63357191b76a916ae5eb93464d65c07511da41e3bf7a8416db519b40b1c23
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: change-this-database-password
POSTGRES_DB: immich
POSTGRES_INITDB_ARGS: '--data-checksums'
volumes:
- immich-db:/var/lib/postgresql/data
shm_size: 128mb
restart: always

redis:
image: docker.io/valkey/valkey:9.0.3@sha256:3b55fbaa0cd93cf0d9d961f405e4dfcc70efe325e2d84da207a0a8e6d8fde4f9
restart: always

volumes:
pomerium-cache:
immich-library:
immich-db:
51 changes: 51 additions & 0 deletions content/examples/guides/immich/docker-compose.yaml.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
```yaml title="docker-compose.yaml"
services:
pomerium:
image: pomerium/pomerium:v0.32.7@sha256:e10d1d267af24f581157f485d9b0bc08469e2428675b696a08e42ceb09b2279c
volumes:
- ./config.yaml:/pomerium/config.yaml:ro
- pomerium-cache:/data
ports:
- 443:443
- 80:80
restart: always

immich-server:
image: ghcr.io/immich-app/immich-server:v2.7.5@sha256:c15bff75068effb03f4355997d03dc7e0fc58720c2b54ad6f7f10d1bc57efaa5
environment:
DB_HOSTNAME: database
DB_USERNAME: postgres
DB_PASSWORD: change-this-database-password
DB_DATABASE_NAME: immich
REDIS_HOSTNAME: redis
volumes:
- immich-library:/data
depends_on:
- database
- redis
restart: always

# Immich requires this VectorChord/pgvecto-rs Postgres image. A stock
# postgres image crashes immich-server on first boot because the vector
# search extension is missing, so pin the exact image Immich ships.
database:
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:bcf63357191b76a916ae5eb93464d65c07511da41e3bf7a8416db519b40b1c23
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: change-this-database-password
POSTGRES_DB: immich
POSTGRES_INITDB_ARGS: '--data-checksums'
volumes:
- immich-db:/var/lib/postgresql/data
shm_size: 128mb
restart: always

redis:
image: docker.io/valkey/valkey:9.0.3@sha256:3b55fbaa0cd93cf0d9d961f405e4dfcc70efe325e2d84da207a0a8e6d8fde4f9
restart: always

volumes:
pomerium-cache:
immich-library:
immich-db:
```
Loading