Skip to main content

Immich + Authentik + Caddy: Self-Hosted Google Photos Alternative (2026)

· 14 min read
Yassine El Haddad
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.

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:

What you get

ToolRoleReplaces
Immich serverPhoto/video library, web UI, REST + GraphQL APIGoogle Photos, iCloud Photos
Immich machine learningFace recognition (CLIP), smart searchGoogle 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 + workerOIDC identity provider, SSOGoogle Workspace SSO, Okta
PostgreSQL 16Authentik's database (separate from Immich's pg14)
Redis (Authentik)Authentik session cache
CaddyTLS 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.com and (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)

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:

  1. Go to Applications → Providers → Create → OAuth2/OpenID Provider
  2. 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
  3. Go to Applications → Applications → Create
    • Name: Immich
    • Slug: immich
    • Provider: select the provider you just created
  4. (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.

ServiceIdle RAMPeak RAM
immich-server350 MB700 MB
immich-machine-learning850 MB2,100 MB
immich-db (pgvecto-rs pg14)220 MB400 MB
immich-redis20 MB30 MB
authentik-server450 MB700 MB
authentik-worker150 MB250 MB
authentik-db (pg16)200 MB350 MB
authentik-redis15 MB25 MB
caddy15 MB20 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 TBiCloud+ 2 TBBackblaze B2Immich on Liquid Web
Monthly cost$9.99/mo$9.99/mo~$12/mo~$33/mo VPS (8 GB)
Storage limit2 TB2 TBUnlimitedVPS disk + expandable volumes
Face recognitionBasicOn-device onlyNoneFull (Immich + CLIP)
Smart searchLimitedSiri-onlyNoneSemantic search via vectors
Mobile auto-backup✓ (Immich mobile app)
Self-hostable
Data ownershipGoogleAppleBackblazeYours
Works offlinePartial

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.
Frequently Asked Questions

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.

Photo of Yassine El Haddad

Yassine El Haddad

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.