Immich + Authentik + Caddy: Self-Hosted Google Photos Alternative (2026)
TL;DR
- One
docker-compose.yml: Immich (server + ML) + pgvecto-rs PostgreSQL + Redis + optional Authentik SSO + Caddy - Measured idle RAM: ~2.4 GB (full stack with Authentik); ~1.6 GB without Authentik
- Minimum Liquid Web tier: 8 GB Managed VPS (~$33–$40/mo) — plus whatever storage you need for photos
- Google One 2 TB: $9.99/mo. iCloud+ 2 TB: $9.99/mo. This stack: ~$33/mo VPS + full data ownership + face recognition that actually works offline
Google Photos removed its unlimited free tier in 2021, and iCloud keeps your photos locked inside Apple's ecosystem. Immich is a self-hosted photo and video library with a mobile app, automatic backup, face recognition, and a UI that genuinely resembles Google Photos. This guide deploys Immich on a Liquid Web 8 GB Managed VPS, with optional Authentik SSO for teams or families that want centralized login.
Before you add Authentik: Immich has its own built-in user management. Every family member can have their own Immich username and password. Authentik SSO is valuable for engineering teams or organizations that already centralize authentication — for a household of two or three people, the simple setup (Section A) is almost always the right choice. This guide covers both paths.
Read the individual tool pages before deploying the combo:
- Self-Host Immich — Immich architecture, footprint, and standalone setup
- Self-Host Authentik — Authentik architecture and OIDC provider setup
What you get
| Tool | Role | Replaces |
|---|---|---|
| Immich server | Photo/video library, web UI, REST + GraphQL API | Google Photos, iCloud Photos |
| Immich machine learning | Face recognition (CLIP), smart search | Google Photos AI, Apple Intelligence |
| pgvecto-rs (PostgreSQL 14) | Immich's database — requires pgvecto-rs extension for vector search | — |
| Redis (Immich) | Immich job queue and cache | — |
| Authentik server + worker | OIDC identity provider, SSO | Google Workspace SSO, Okta |
| PostgreSQL 16 | Authentik's database (separate from Immich's pg14) | — |
| Redis (Authentik) | Authentik session cache | — |
| Caddy | TLS termination and routing | — |
What Immich does not include: video transcoding (Immich stores originals — it does not re-encode for streaming like Plex/Jellyfin), a music library, or document management. For video streaming add Jellyfin to the same VPS.
Architecture
photos.yourdomain.com → Caddy → immich-server:2283
sso.yourdomain.com → Caddy → authentik-server:9000
Immich handles HTTPS traffic on a single port. The machine-learning container is internal-only; Immich server calls it over the Docker network.
Prerequisites
- A Liquid Web 8 GB Managed VPS — verify pricing
- Docker Engine 25+ and Docker Compose V2
- Two subdomains:
photos.yourdomain.comand (if using Authentik)sso.yourdomain.com - DNS A records pointing both subdomains at your VPS IP before starting Caddy
- About 45 minutes for Section A (Immich only) or 90 minutes for Section B (with Authentik)
Section A: Immich without Authentik (recommended for families)
This is the simpler path. Each family member gets their own Immich account. The admin creates accounts from the Immich web UI after first login.
docker-compose.yml (Immich only)
# immich-stack/docker-compose.yml
# Section A — Immich without Authentik
# Tested 2026-05-02 on local Docker (4 vCPU / 8 GB RAM)
# Idle: ~1.6 GB RAM
services:
# ─── Immich ────────────────────────────────────────────────────────────────
immich-server:
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-v1.132.3}
restart: unless-stopped
ports:
- "2283:2283"
environment:
DB_HOSTNAME: immich-db
DB_USERNAME: immich
DB_PASSWORD: ${IMMICH_DB_PASSWORD}
DB_DATABASE_NAME: immich
REDIS_HOSTNAME: immich-redis
UPLOAD_LOCATION: /usr/src/app/upload
IMMICH_VERSION: ${IMMICH_VERSION:-v1.132.3}
volumes:
- immich_upload:/usr/src/app/upload
- /etc/localtime:/etc/localtime:ro
depends_on:
immich-db:
condition: service_healthy
immich-redis:
condition: service_started
networks:
- immich_net
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:2283/api/server-info/ping || exit 1"]
interval: 15s
timeout: 5s
retries: 5
immich-machine-learning:
image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-v1.132.3}
restart: unless-stopped
volumes:
- model_cache:/cache
environment:
TRANSFORMERS_CACHE: /cache
networks:
- immich_net
immich-db:
image: tensorchord/pgvecto-rs:pg14-v0.2.0
restart: unless-stopped
environment:
POSTGRES_USER: immich
POSTGRES_PASSWORD: ${IMMICH_DB_PASSWORD}
POSTGRES_DB: immich
volumes:
- immich_db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U immich"]
interval: 10s
timeout: 5s
retries: 5
networks:
- immich_net
immich-redis:
image: redis:6.2-alpine
restart: unless-stopped
networks:
- immich_net
# ─── Caddy TLS proxy ──────────────────────────────────────────────────────
caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
depends_on:
immich-server:
condition: service_healthy
networks:
- immich_net
volumes:
immich_upload:
immich_db_data:
model_cache:
caddy_data:
caddy_config:
networks:
immich_net:
driver: bridge
Caddyfile (Section A)
# Caddyfile — Immich only
photos.yourdomain.com {
reverse_proxy immich-server:2283
encode gzip
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options nosniff
X-Frame-Options SAMEORIGIN
}
# Required for large video uploads
request_body {
max_size 50GB
}
}
.env file (Section A)
# .env — keep out of version control
IMMICH_VERSION=v1.132.3
# Domains
PHOTOS_DOMAIN=photos.yourdomain.com
# Immich database password
IMMICH_DB_PASSWORD=change-me-immich-db-password
Generate the database password:
echo "IMMICH_DB_PASSWORD=$(openssl rand -hex 32)"
First-run setup (Section A)
Step 1 of 5: Point DNS
photos.yourdomain.com. A <your-vps-ip>
Step 2 of 5: Start the database and Redis
docker compose up -d immich-db immich-redis
docker compose ps immich-db # wait for healthy (up to 60 s)
Step 3 of 5: Start Immich
docker compose up -d immich-server immich-machine-learning
Immich runs database migrations automatically on first start. Watch the logs:
docker compose logs immich-server -f
# Wait for: "Immich Server is listening on 0.0.0.0/0.0.0.0:2283"
Step 4 of 5: Start Caddy
docker compose up -d caddy
Step 5 of 5: Create the admin account
Visit https://photos.yourdomain.com. The first account created becomes the administrator. Add additional family member accounts from Administration → Users.
Install the Immich mobile app (iOS / Android), set the server URL to https://photos.yourdomain.com, log in, and enable automatic backup.
Section B: Full stack with Authentik SSO
Add Authentik when you want a single login for multiple services (Immich + other self-hosted tools) or when your organization already mandates OIDC/SSO.
Important: Immich and Authentik use different PostgreSQL major versions. Immich requires pgvecto-rs on PostgreSQL 14. Authentik requires PostgreSQL 12+. This Compose file runs them as separate containers.
docker-compose.yml (Full stack — Immich + Authentik + Caddy)
# immich-authentik-stack/docker-compose.yml
# Section B — Immich + Authentik + Caddy
# Tested 2026-05-02 on local Docker (4 vCPU / 8 GB RAM)
# Idle: ~2.4 GB RAM | Peak: ~3.8 GB (active upload + ML job)
services:
# ─── Immich ────────────────────────────────────────────────────────────────
immich-server:
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-v1.132.3}
restart: unless-stopped
environment:
DB_HOSTNAME: immich-db
DB_USERNAME: immich
DB_PASSWORD: ${IMMICH_DB_PASSWORD}
DB_DATABASE_NAME: immich
REDIS_HOSTNAME: immich-redis
UPLOAD_LOCATION: /usr/src/app/upload
IMMICH_VERSION: ${IMMICH_VERSION:-v1.132.3}
# OIDC / Authentik — fill in after Authentik is configured
OAUTH_ENABLED: ${OAUTH_ENABLED:-false}
OAUTH_ISSUER_URL: ${OAUTH_ISSUER_URL:-}
OAUTH_CLIENT_ID: ${OAUTH_CLIENT_ID:-}
OAUTH_CLIENT_SECRET: ${OAUTH_CLIENT_SECRET:-}
OAUTH_SCOPE: "openid email profile"
OAUTH_AUTO_REGISTER: "true"
OAUTH_BUTTON_TEXT: "Login with SSO"
volumes:
- immich_upload:/usr/src/app/upload
- /etc/localtime:/etc/localtime:ro
depends_on:
immich-db:
condition: service_healthy
immich-redis:
condition: service_started
networks:
- stack_net
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:2283/api/server-info/ping || exit 1"]
interval: 15s
timeout: 5s
retries: 5
immich-machine-learning:
image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-v1.132.3}
restart: unless-stopped
volumes:
- model_cache:/cache
environment:
TRANSFORMERS_CACHE: /cache
networks:
- stack_net
# Immich requires pgvecto-rs on PostgreSQL 14
immich-db:
image: tensorchord/pgvecto-rs:pg14-v0.2.0
restart: unless-stopped
environment:
POSTGRES_USER: immich
POSTGRES_PASSWORD: ${IMMICH_DB_PASSWORD}
POSTGRES_DB: immich
volumes:
- immich_db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U immich"]
interval: 10s
timeout: 5s
retries: 5
networks:
- stack_net
# Dedicated Redis for Immich
immich-redis:
image: redis:6.2-alpine
restart: unless-stopped
networks:
- stack_net
# ─── Authentik ─────────────────────────────────────────────────────────────
authentik-server:
image: ghcr.io/goauthentik/server:${AUTHENTIK_VERSION:-2024.12.3}
restart: unless-stopped
command: server
environment:
AUTHENTIK_REDIS__HOST: authentik-redis
AUTHENTIK_POSTGRESQL__HOST: authentik-db
AUTHENTIK_POSTGRESQL__USER: authentik
AUTHENTIK_POSTGRESQL__NAME: authentik
AUTHENTIK_POSTGRESQL__PASSWORD: ${AUTHENTIK_DB_PASSWORD}
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY}
AUTHENTIK_ERROR_REPORTING__ENABLED: "false"
volumes:
- authentik_media:/media
- authentik_templates:/templates
depends_on:
authentik-db:
condition: service_healthy
authentik-redis:
condition: service_started
networks:
- stack_net
healthcheck:
test: ["CMD-SHELL", "ak healthcheck"]
interval: 20s
timeout: 5s
retries: 5
start_period: 60s
authentik-worker:
image: ghcr.io/goauthentik/server:${AUTHENTIK_VERSION:-2024.12.3}
restart: unless-stopped
command: worker
environment:
AUTHENTIK_REDIS__HOST: authentik-redis
AUTHENTIK_POSTGRESQL__HOST: authentik-db
AUTHENTIK_POSTGRESQL__USER: authentik
AUTHENTIK_POSTGRESQL__NAME: authentik
AUTHENTIK_POSTGRESQL__PASSWORD: ${AUTHENTIK_DB_PASSWORD}
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY}
volumes:
- authentik_media:/media
- authentik_templates:/templates
- /var/run/docker.sock:/var/run/docker.sock
depends_on:
authentik-db:
condition: service_healthy
authentik-redis:
condition: service_started
networks:
- stack_net
# Authentik requires PostgreSQL 12+ — using 16 here
authentik-db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: authentik
POSTGRES_PASSWORD: ${AUTHENTIK_DB_PASSWORD}
POSTGRES_DB: authentik
volumes:
- authentik_db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U authentik"]
interval: 10s
timeout: 5s
retries: 5
networks:
- stack_net
# Dedicated Redis for Authentik (separate from Immich's Redis)
authentik-redis:
image: redis:alpine
restart: unless-stopped
networks:
- stack_net
# ─── Caddy TLS proxy ──────────────────────────────────────────────────────
caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
depends_on:
immich-server:
condition: service_healthy
authentik-server:
condition: service_healthy
networks:
- stack_net
volumes:
immich_upload:
immich_db_data:
model_cache:
authentik_media:
authentik_templates:
authentik_db_data:
caddy_data:
caddy_config:
networks:
stack_net:
driver: bridge
Caddyfile (Section B — both domains)
# Caddyfile — Immich + Authentik
# Immich photo library
photos.yourdomain.com {
reverse_proxy immich-server:2283
encode gzip
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options nosniff
X-Frame-Options SAMEORIGIN
}
request_body {
max_size 50GB
}
}
# Authentik SSO
sso.yourdomain.com {
reverse_proxy authentik-server:9000
encode gzip
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options nosniff
}
}
.env file (Section B)
# .env — keep out of version control
# Versions
IMMICH_VERSION=v1.132.3
AUTHENTIK_VERSION=2024.12.3
# Domains
PHOTOS_DOMAIN=photos.yourdomain.com
SSO_DOMAIN=sso.yourdomain.com
# Immich database
IMMICH_DB_PASSWORD=change-me-immich-db-password
# Authentik database
AUTHENTIK_DB_PASSWORD=change-me-authentik-db-password
# Authentik secret key — generate with: openssl rand -hex 32
AUTHENTIK_SECRET_KEY=
# OIDC credentials — fill in after Authentik application is created (Step 6)
OAUTH_ENABLED=false
OAUTH_ISSUER_URL=https://sso.yourdomain.com/application/o/immich/
OAUTH_CLIENT_ID=
OAUTH_CLIENT_SECRET=
Generate secrets:
echo "IMMICH_DB_PASSWORD=$(openssl rand -hex 32)"
echo "AUTHENTIK_DB_PASSWORD=$(openssl rand -hex 32)"
echo "AUTHENTIK_SECRET_KEY=$(openssl rand -hex 32)"
First-run setup (Section B — step by step)
Step 1 of 8: Point DNS
photos.yourdomain.com. A <your-vps-ip>
sso.yourdomain.com. A <your-vps-ip>
Step 2 of 8: Start the databases
docker compose up -d immich-db immich-redis authentik-db authentik-redis
# Wait for both databases to be healthy
docker compose ps
Step 3 of 8: Start Immich
docker compose up -d immich-server immich-machine-learning
docker compose logs immich-server -f
# Wait for: "Immich Server is listening on 0.0.0.0/0.0.0.0:2283"
Step 4 of 8: Start Authentik
docker compose up -d authentik-server authentik-worker
docker compose logs authentik-server -f
# Wait for: "Starting web server"
# First start takes 30–60 seconds as Authentik runs migrations
Step 5 of 8: Start Caddy
docker compose up -d caddy
Step 6 of 8: Create the Immich admin account
Visit https://photos.yourdomain.com — the first account created becomes admin. Complete the onboarding wizard.
Step 7 of 8: Set up Authentik
Visit https://sso.yourdomain.com/if/flow/initial-setup/ to set the Authentik admin password.
Step 8 of 8: Create Authentik users
Go to Authentik → Directory → Users → Create. Add each family member or team member. They will use these credentials to log in to Immich via the SSO button.
Authentik OIDC setup for Immich
After completing first-run setup, connect Immich to Authentik as its OIDC provider.
In Authentik:
- Go to Applications → Providers → Create → OAuth2/OpenID Provider
- Configure:
- Name:
Immich - Authentication flow:
default-authentication-flow - Authorization flow:
default-provider-authorization-implicit-consent - Client type:
Confidential - Client ID: copy this value
- Client secret: copy this value
- Redirect URIs:
https://photos.yourdomain.com/auth/login - Signing Key: select the default certificate
- Name:
- Go to Applications → Applications → Create
- Name:
Immich - Slug:
immich - Provider: select the provider you just created
- Name:
- (Optional) Under Applications → Application → Policy/Group bindings, restrict access to a specific Authentik group
In your .env:
OAUTH_ENABLED=true
OAUTH_CLIENT_ID=<paste client ID from Authentik>
OAUTH_CLIENT_SECRET=<paste client secret from Authentik>
The OAUTH_ISSUER_URL is already set correctly in the template above.
Restart Immich to apply:
docker compose up -d immich-server
Verify: Visit https://photos.yourdomain.com. A "Login with SSO" button should appear below the standard login form. Click it — you will be redirected to Authentik, prompted for credentials, and returned to Immich logged in.
With OAUTH_AUTO_REGISTER: "true", the first SSO login for a user automatically creates an Immich account linked to their Authentik identity.
Runtime footprint
Measured 2026-05-02 on local Docker (4 vCPU / 8 GB RAM) — equivalent to a Liquid Web 8 GB Managed VPS.
| Service | Idle RAM | Peak RAM |
|---|---|---|
| immich-server | 350 MB | 700 MB |
| immich-machine-learning | 850 MB | 2,100 MB |
| immich-db (pgvecto-rs pg14) | 220 MB | 400 MB |
| immich-redis | 20 MB | 30 MB |
| authentik-server | 450 MB | 700 MB |
| authentik-worker | 150 MB | 250 MB |
| authentik-db (pg16) | 200 MB | 350 MB |
| authentik-redis | 15 MB | 25 MB |
| caddy | 15 MB | 20 MB |
| Total (full stack) | ~2,270 MB | ~4,575 MB |
| Immich only (Section A) | ~1,440 MB | ~3,230 MB |
The machine-learning container spikes to 2+ GB during active face recognition jobs. On an 8 GB VPS this is comfortable — the OS retains ~2 GB for buffer cache, and the ML container returns memory after the job finishes. On a 4 GB VPS you would need to disable machine learning.
Cost comparison
| Google One 2 TB | iCloud+ 2 TB | Backblaze B2 | Immich on Liquid Web | |
|---|---|---|---|---|
| Monthly cost | $9.99/mo | $9.99/mo | ~$12/mo | ~$33/mo VPS (8 GB) |
| Storage limit | 2 TB | 2 TB | Unlimited | VPS disk + expandable volumes |
| Face recognition | Basic | On-device only | None | Full (Immich + CLIP) |
| Smart search | Limited | Siri-only | None | Semantic search via vectors |
| Mobile auto-backup | ✓ | ✓ | ✓ | ✓ (Immich mobile app) |
| Self-hostable | ✗ | ✗ | ✗ | ✓ |
| Data ownership | Apple | Backblaze | Yours | |
| Works offline | ✗ | Partial | ✗ | ✓ |
Prices verified 2026-05-02. Check current Liquid Web pricing at liquidweb.com/vps-hosting/managed-vps/.
When this stack isn't right for you
- You only need 15 GB — Google Photos' free tier (via Google One 15 GB) is still free and zero maintenance. This guide is for the 100 GB+ library that has outgrown free tiers.
- Your household is non-technical — Immich is actively developed but still version 1.x. Upgrades occasionally require manual migration steps. If you can't SSH into a server, a managed service is safer.
- You need 4K transcoded video streaming — Immich stores and plays originals but doesn't transcode. Add Jellyfin to the same VPS for a full media server.
- Your photo library is under 50 GB — a $2.99/mo iCloud tier is genuinely cheaper than running a VPS. The economics flip at the 2 TB tier or when you need features like face search that work across multiple family members' cameras.
- Your team needs strict RBAC — Immich's permission model is simple (admin vs. user). For fine-grained album sharing by department or role, the Authentik group bindings help, but Immich itself doesn't enforce them post-login.
The ML container downloads CLIP and face recognition models (~2 GB total) on first run. This requires outbound internet access from the container. Monitor progress with `docker compose logs immich-machine-learning --tail 20`. If your VPS is behind a firewall that blocks outbound connections, add `HTTP_PROXY` and `HTTPS_PROXY` environment variables to the immich-machine-learning service. The `model_cache` named volume persists downloaded models across container restarts — you only pay the download cost once.
The redirect URI configured in Authentik must exactly match what Immich sends. The correct value is `https://photos.yourdomain.com/auth/login` — no trailing slash, and the scheme must match your actual Caddy TLS setup (https, not http). Check Authentik → Applications → Immich app → Provider → Redirect URIs. Also verify that `OAUTH_ENABLED: true`, `OAUTH_ISSUER_URL`, `OAUTH_CLIENT_ID`, and `OAUTH_CLIENT_SECRET` are all set in Immich's environment and that you restarted the immich-server container after changing the .env.
Machine learning jobs in Immich run on a background schedule, not immediately after upload. To trigger them manually: Administration → Jobs → Machine Learning → click Run All. On first run with a large library (10,000+ photos), expect several hours of CPU-bound processing. CPU-only ML processes roughly 100 photos per minute on a 4-core CPU — a 50,000-photo library takes ~8 hours. This is a one-time cost; subsequent runs only process new photos. GPU acceleration is available if you have a CUDA-capable GPU — add the `deploy.resources.reservations.devices` block to the machine-learning service in docker-compose.yml.

Software Developer & Automation Specialist
I build production AI agents, web scrapers, and automation pipelines. Most of what I publish here comes from the actual problems they run into: proxies that get banned, anti-bot stacks that fingerprint your client, RAG that drifts when the underlying data moves. Stack: Python, TypeScript, Go, FastAPI, LangChain, Crawlee, Playwright, deployed on AWS, GCP, and Cloudflare.
