The stack
Five containers:
- immich-server — TypeScript / Nest.js API + web UI
- immich-machine-learning — Python worker running CLIP + face detection + face recognition models
- redis — job queue and short-lived caches
- postgres + pgvector — metadata, faces, embeddings (the embeddings rely on pgvector — see that tutorial)
- (optional) external storage backend if not using a local volume
docker compose
Grab the official template and env file. The current canonical version is published per release on the Immich docs; the structure below is correct as of v1.x in early 2026:
# docker-compose.yml (trimmed)
services:
immich-server:
image: ghcr.io/immich-app/immich-server:release
extends:
file: hwaccel.transcoding.yml
service: cpu # or "nvenc", "qsv", "vaapi" if you have a GPU
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
- /etc/localtime:/etc/localtime:ro
env_file: [ .env ]
ports:
- "127.0.0.1:2283:2283"
depends_on: [ redis, database ]
restart: always
immich-machine-learning:
image: ghcr.io/immich-app/immich-machine-learning:release
volumes:
- model-cache:/cache
env_file: [ .env ]
restart: always
redis:
image: docker.io/redis:6.2-alpine
healthcheck: { test: [ "CMD-SHELL", "redis-cli ping || exit 1" ] }
restart: always
database:
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_USER: ${DB_USERNAME}
POSTGRES_DB: ${DB_DATABASE_NAME}
POSTGRES_INITDB_ARGS: '--data-checksums'
volumes:
- ${DB_DATA_LOCATION}:/var/lib/postgresql/data
healthcheck:
test: [ "CMD-SHELL", "pg_isready -U ${DB_USERNAME} || exit 1" ]
restart: always
volumes:
model-cache:
Critical environment variables in .env:
UPLOAD_LOCATION=/srv/immich/library
DB_DATA_LOCATION=/srv/immich/postgres
TZ=America/Toronto
DB_PASSWORD=<long-random>
DB_USERNAME=postgres
DB_DATABASE_NAME=immich
# Optional: enable CLIP downloads through a proxy / pin model version
# IMMICH_MACHINE_LEARNING_URL=http://immich-machine-learning:3003
docker compose up -d and watch docker compose logs -f immich-server — first start runs DB migrations.
Immich's Postgres image is a forked build with the vectorchord/pgvector extension. Pointing it at a vanilla Postgres data directory or vice-versa corrupts the database. The DB_DATA_LOCATION directory must be Immich-managed only.
Reverse proxy
Immich uploads can be very large (4K video). The proxy needs generous body-size limits:
# Caddy
photos.example.com {
reverse_proxy 127.0.0.1:2283
request_body {
max_size 50000MB
}
}
# nginx
client_max_body_size 50000M;
proxy_read_timeout 600s;
proxy_request_buffering off; # stream uploads instead of buffering
proxy_request_buffering off matters — otherwise nginx tries to spool every multi-GB upload to disk first.
Mobile setup
Install the Immich app on iOS (App Store) or Android (Play Store / F-Droid / direct APK). On first launch:
- Server URL:
https://photos.example.com. The app must hit that exact URL with TLS — no plain HTTP from real iOS/Android. - Log in with the account created at the web UI signup flow.
- Settings → Backup → pick album(s) to back up. "Camera Roll" or specific named albums.
- Settings → Backup → turn on "Foreground" and "Background" backup. Background backup on iOS is best-effort by the OS — expect periodic catch-up rather than real-time uploads.
ML model selection
By default Immich uses the smallest CLIP model (ViT-B-32) for semantic search. Web UI → Administration → Settings → Machine Learning lets you swap to larger models:
- ViT-B-32 — fastest, ~150 MB, OK quality
- ViT-L-14 — better quality, ~1.7 GB, 4–6× slower per image
- SigLIP — current quality leader for many tasks, intermediate size
Switching models triggers a re-embedding job for the whole library. Plan for hours-to-days of background CPU on a large library; queue progress is visible at Administration → Jobs.
External libraries (no copy)
Immich can index files in place from a read-only mount, so an existing photo archive doesn't have to be re-uploaded. Mount the archive into the server container at any path (e.g. /mnt/old-photos), then web UI → Administration → External Libraries → add the path. Immich reads metadata, generates thumbnails into its own storage, and indexes the originals without touching them.
Backups
Three things to back up:
- The library (
UPLOAD_LOCATION) — originals and uploaded media. - The Postgres database — metadata, embeddings, face clusters. A nightly
pg_dumpallthrough a sidecar or systemd job is enough. - The .env — without DB_PASSWORD, restored data is opaque.
For Postgres specifically, Immich's docs recommend a logical dump rather than file-level copies of the data directory, because vectorchord index files don't always survive a raw-file copy across container restarts:
docker exec -t immich-database \
pg_dumpall -c -U postgres | gzip > immich-pg-$(date +%F).sql.gz
Both the library directory and the pg-dump fit comfortably inside a restic job (see restic + S3).
Performance notes
- CPU: ML jobs are the dominant load. On a 4-core x86_64 box, ViT-B-32 indexes ~50–150 photos/minute; ViT-L-14 is roughly 4× slower.
- RAM: 4 GB minimum, 8 GB comfortable for the ML worker. Larger CLIP models want more.
- GPU: NVENC/QSV/VAAPI extends the compose file to use hardware acceleration for transcoding (the
hwaccel.transcoding.ymlreference above). CUDA on the ML worker is a separate opt-in: setimage: ghcr.io/immich-app/immich-machine-learning:release-cudaand adddeploy.resources.reservations.devicesto expose the GPU. - Disk: thumbnails and ML embeddings add roughly 5–10% on top of original media size.
What it isn't (yet)
Immich is a backup-and-browse product, not full photo editing. Cropping, exposure, color, lens-correction — not in scope. Edit in Lightroom / Darktable / a native app, then upload. For an editor-first workflow that also self-hosts, look at PhotoPrism; for a "literally Google Photos clone, full stop" workflow, Immich is the closest thing in the open-source world.