Immich behind Caddy

Verified · Caddy 2.8

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
# 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

  1. Create the shared proxy network once: `docker network create proxy`.
  2. Start the stack: `docker compose up -d`.
  3. 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 (app + Caddy)
# 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

Full Immich stack (compose + env + docs)