Last updated: November 2026 — Immich compose on main can diverge from release; prefer the compose file from the latest GitHub release.
For: A household or small team replacing Google Photos / iCloud with self-hosted Immich on a Linux VPS or mini-PC, with enough RAM for Postgres + ML, behind Caddy HTTPS.
Do not use this guide when:
You only have ≤ 4 GB RAM and refuse to disable machine learning (official floor with ML off is already tight)
Postgres data would live on NFS/SMB (unsupported — use local SSD)
You need a turnkey NAS UI with zero Docker (look at vendor apps instead)
The CPU is ancient x86-64-v1 only (Immich ML on amd64 since v3 wants x86-64-v2)
From Immich’s published requirements (docs, 2026):
Cheap-VPS reality check: A $5 / 1 GB box cannot run Immich. Budget roughly a $12–24/mo tier (4–8 GB) or a home mini-PC. Disable ML only as a last resort on 4 GB.
Official Immich services: immich-server, immich-machine-learning, redis (Valkey), database (Immich Postgres image)
Caddy publishes 80/443; Immich server is not bound to the public interface
Shared Compose network; Caddy reverse-proxies to immich-server:2283
~/immich/
docker-compose.yml
.env
Caddyfile
library/ # UPLOAD_LOCATION
postgres/ # DB_DATA_LOCATION — local SSD
caddy-data/
caddy-config/
Download release compose as a base, then adapt ports (below we show a complete working layout with Caddy).
UPLOAD_LOCATION=./library
DB_DATA_LOCATION=./postgres
TZ=Asia/Shanghai
IMMICH_VERSION=v3
DB_PASSWORD=changeMeUseOnlyA-Za-z0-9
DB_USERNAME=postgres
DB_DATABASE_NAME=immich
# Public hostname for Caddy (no scheme)
DOMAIN=photos.example.com
ACME_EMAIL=you@example.com
{$DOMAIN} {
encode gzip
reverse_proxy immich-server:2283
}
name: immich
services:
immich-server:
container_name: immich_server
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
volumes:
- ${UPLOAD_LOCATION}:/data
- /etc/localtime:/etc/localtime:ro
env_file:
- .env
# Do NOT publish 2283 on 0.0.0.0 when Caddy is the public edge
expose:
- "2283"
depends_on:
- redis
- database
restart: unless-stopped
immich-machine-learning:
container_name: immich_machine_learning
image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release}
volumes:
- model-cache:/cache
env_file:
- .env
environment:
MACHINE_LEARNING_MODEL_REVISION: v2
restart: unless-stopped
# On a 4 GB host: remove this service and disable ML in the Immich admin UI / docs path
redis:
container_name: immich_redis
image: docker.io/valkey/valkey:9
healthcheck:
test: redis-cli ping | grep -q PONG || exit 1
restart: unless-stopped
database:
container_name: immich_postgres
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_USER: ${DB_USERNAME}
POSTGRES_DB: ${DB_DATABASE_NAME}
POSTGRES_INITDB_ARGS: "--data-checksums"
# Uncomment if DB sits on HDD spinning rust:
# DB_STORAGE_TYPE: "HDD"
volumes:
- ${DB_DATA_LOCATION}:/var/lib/postgresql/data
shm_size: 128mb
restart: unless-stopped
caddy:
container_name: immich_caddy
image: caddy:2.11.7
restart: unless-stopped
depends_on:
- immich-server
ports:
- "80:80"
- "443:443"
- "443:443/udp"
environment:
DOMAIN: "${DOMAIN}"
EMAIL: "${ACME_EMAIL}"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./caddy-data:/data
- ./caddy-config:/config
volumes:
model-cache:
Production check before first upload
.env password is alphanumeric only
docker compose (v2 plugin), not legacy docker-compose
Free RAM ≥ 6 GB with ML enabled (free -h)
Same pattern as other guides on this site: only Caddy binds 80/443. Mobile apps should use https://photos.example.com. If you previously published 2283:2283, remove that publish so TLS is not bypassed.
WebSockets / large uploads: Caddy’s default reverse_proxy handles Immich; if uploads fail at reverse proxies with tiny body limits, raise limits there (Caddy defaults are usually fine; nginx is where people hit client_max_body_size).
Immich has two critical pieces: library files + Postgres.
Backup
Prefer Immich’s documented backup approach for DB; minimally:
docker compose stop immich-server immich-machine-learning
Postgres dump:
docker compose exec -T database pg_dumpall -U postgres > immich-db-$(date +%F).sql
File copy / rsync -a of $UPLOAD_LOCATION to off-site storage
Copy .env (secrets) to a secure store — not the photo bucket’s public ACL
docker compose start immich-server immich-machine-learning
Restore
Start database only; restore SQL into it; then start the rest
Paths in .env must match the restored layout
Open the web UI; confirm thumbnails regenerate if needed
Do not treat “volume folder copy while Postgres is writing” as a consistent backup without a dump or filesystem freeze.
[ ] docker compose ps — server, redis, database healthy; ML up if you kept it
[ ] https://photos.example.com loads with a valid certificate
[ ] Create admin user; upload 20 photos from phone over cellular
[ ] Jobs complete (thumbnails); faces/search work if ML enabled
[ ] pg_dumpall backup file is non-empty; test restore on a throwaway folder once
[ ] Reboot VPS; stack returns without manual up
Uncomment Immich’s hwaccel.*.yml extends for NVENC / Quick Sync / etc. That is not required for a first successful deploy and often unavailable on cheap cloud VPS CPUs.
Image refs aligned with Immich release compose patterns (server/ML ghcr.io/immich-app/..., Valkey 9, Immich Postgres 14 + vectorchord). Re-download compose from the latest release before major upgrades.