Immich behind Caddy
Config + gotchas checked against the official Immich docs and the issues people actually hit · 2026-06-07
A Caddy reverse-proxy setup for Immich, with automatic HTTPS on immich.example.com. Copy it, swap the domain — the gotchas that trip people up are solved below.
New to reverse proxies? Start with the Caddy + Docker guide.
# Caddyfile for Immich
# Replace immich.example.com with your real domain. Caddy issues TLS automatically.
immich.example.com {
encode zstd gzip
reverse_proxy immich-server:2283
}
Replace immich.example.com with your domain. Your DNS A record must already point at the server.
Gotchas
What trips people up
Immich loads at the domain root but 404s on a sub-path like /immich.
Immich must be served at the root of a (sub)domain — it does not support a sub-path. Give it its own subdomain (immich.example.com), not example.com/immich.
Large video uploads fail or time out through the proxy.
nginx needs `client_max_body_size` raised; Caddy streams request bodies with no size cap by default, so the block here needs no tuning. If uploads still fail it is an upstream limit — most often the Cloudflare Tunnel 100MB cap, not Caddy.
Deploy
How to deploy
- Create the shared proxy network once: `docker network create proxy`.
- Start the stack: `docker compose up -d`.
- Point an A record for immich.example.com at the server; Caddy issues the TLS cert on first request (ports 80/443 must be reachable).
Runnable
Full runnable stack
Immich + Caddy + the shared network, in one compose file — so it runs on the first try, not after you stitch the proxy in yourself. Save the Caddyfile above next to it.
# docker-compose.yml — Immich + Caddy, ready to run.
# Pin caddy to a digest for production: caddy:2.8-alpine@sha256:<digest>
# 1. Save the Caddyfile above as ./Caddyfile
# 2. docker network create proxy
# 3. docker compose up -d
services:
immich-server:
image: ghcr.io/immich-app/immich-server:v1.119.0@sha256:24df1172544370826349159692d177ba22ca773c81857d36996a254c08422b95
restart: unless-stopped
depends_on:
- immich-redis
- immich-postgres
environment:
DB_HOSTNAME: immich-postgres
DB_USERNAME: postgres
DB_PASSWORD: ${DB_PASSWORD}
DB_DATABASE_NAME: immich
REDIS_HOSTNAME: immich-redis
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
- /etc/localtime:/etc/localtime:ro
healthcheck:
test:
- CMD
- /bin/sh
- -c
- curl -fSs http://127.0.0.1:2283/api/server-info/ping
interval: 30s
timeout: 5s
retries: 3
start_period: 20s
networks:
- default
- proxy
immich-machine-learning:
image: ghcr.io/immich-app/immich-machine-learning:v1.119.0@sha256:fa558ae8752eae335c5bfafeb5716dc60d0882f026350adb45e18ab28fbd36ae
restart: unless-stopped
volumes:
- model-cache:/cache
immich-redis:
image: docker.io/redis:6.2-alpine@sha256:eaba718fecd1196d88533de7ba49bf903ad33664a92debb24660a922ecd9cac8
restart: unless-stopped
healthcheck:
test: redis-cli ping || exit 1
interval: 30s
timeout: 5s
retries: 3
immich-postgres:
image: docker.io/tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0
restart: unless-stopped
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_USER: postgres
POSTGRES_DB: immich
POSTGRES_INITDB_ARGS: --data-checksums
volumes:
- ${DB_DATA_LOCATION}:/var/lib/postgresql/data
healthcheck:
test:
- CMD-SHELL
- pg_isready --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" || exit 1; Chksum="$$(psql --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" --tuples-only --no-align --command='SELECT COALESCE(SUM(checksum_failures), 0) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1
interval: 5m
timeout: 10s
retries: 5
start_period: 5m
caddy:
image: caddy:2.8-alpine
restart: unless-stopped
ports:
- 80:80
- 443:443
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy-data:/data
- caddy-config:/config
networks:
- proxy
volumes:
model-cache: {}
caddy-data: {}
caddy-config: {}
networks:
proxy:
external: true