Reading time: ~12 minutes
Audience: Self-hosters running Immich behind a reverse proxy


Why Immich Needs a Reverse Proxy

Immich runs on port 2283 by default. A reverse proxy provides:

  • SSL termination (HTTPS)
  • Domain-based routing (photos.mydomain.com)
  • Rate limiting (protect uploads)
  • Security headers (HSTS, CSP, X-Frame-Options)
  • Load balancing (if running multiple instances)
Feature Without Proxy With Proxy
HTTPS Manual cert management Automatic (Let’s Encrypt)
Domain IP:2283 photos.mydomain.com
Rate limiting None Configurable
Security headers None Full HSTS/CSP
Path-based routing N/A /api, /share, etc.

Option 1: Nginx Reverse Proxy

Nginx Config (HTTPHTTPS redirect)

server {
    listen 80;
    server_name photos.mydomain.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name photos.mydomain.com;

    # SSL certificates (Let's Encrypt via certbot)
    ssl_certificate /etc/letsencrypt/live/mydomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/mydomain.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256';
    ssl_prefer_server_ciphers off;

    # Security headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;

    # Rate limiting
    limit_req_zone $binary_remote_addr zone=immich:10m rate=10r/s;
    limit_req zone=immich burst=20 nodelay;

    # Client max body size for uploads
    client_max_body_size 50000M;

    location / {
        proxy_pass http://immich-server:2283;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 600s;
        proxy_send_timeout 600s;
    }
}

Docker Compose (Nginx + Immich)

version: "3.8"

services:
  immich-server:
    image: ghcr.io/immich-app/immich-server:release
    container_name: immich_server
    volumes:
      - ${UPLOAD_LOCATION}:/usr/src/app/upload
    env_file:
      - .env
    ports:
      - 2283:3001
    restart: always

  immich-machine-learning:
    image: ghcr.io/immich-app/immich-machine-learning:release
    container_name: immich_machine_learning
    volumes:
      - model-cache:/cache
    env_file:
      - .env
    restart: always

  nginx:
    image: nginx:alpine
    container_name: immich_nginx
    ports:
      - 80:80
      - 443:443
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
      - /etc/letsencrypt:/etc/letsencrypt:ro
    depends_on:
      - immich-server
    restart: always

  redis:
    image: redis:6-alpine
    container_name: immich_redis
    restart: always

  database:
    image: postgres:14-alpine
    container_name: immich_postgres
    env_file:
      - .env
    volumes:
      - pgdata:/var/lib/postgresql/data
    restart: always

volumes:
  model-cache:
  pgdata:

Option 2: Traefik (Docker-native)

Traefik Docker Compose

version: "3.8"

services:
  traefik:
    image: traefik:v2.10
    container_name: traefik
    command:
      - "--api.insecure=true"
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
      - "--certificatesresolvers.letsencrypt.acme.email=admin@mydomain.com"
      - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
      - "--entrypoints.web.http.redirections.entryPoint.to=websecure"
      - "--entrypoints.web.http.redirections.entryPoint.scheme=https"
    ports:
      - "80:80"
      - "443:443"
      - "8080:8080"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./letsencrypt:/letsencrypt
    restart: always

  immich-server:
    image: ghcr.io/immich-app/immich-server:release
    container_name: immich_server
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.immich.rule=Host(`photos.mydomain.com`)"
      - "traefik.http.routers.immich.entrypoints=websecure"
      - "traefik.http.routers.immich.tls.certresolver=letsencrypt"
      - "traefik.http.services.immich.loadbalancer.server.port=3001"
      - "traefik.http.middlewares.immich-headers.headers.customFrameOptionsValue=SAMEORIGIN"
      - "traefik.http.middlewares.immich-headers.headers.contentTypeNosniff=true"
      - "traefik.http.routers.immich.middlewares=immich-headers"
    volumes:
      - ${UPLOAD_LOCATION}:/usr/src/app/upload
    env_file:
      - .env
    restart: always

  immich-machine-learning:
    image: ghcr.io/immich-app/immich-machine-learning:release
    container_name: immich_machine_learning
    volumes:
      - model-cache:/cache
    env_file:
      - .env
    restart: always

  redis:
    image: redis:6-alpine
    container_name: immich_redis
    restart: always

  database:
    image: postgres:14-alpine
    container_name: immich_postgres
    env_file:
      - .env
    volumes:
      - pgdata:/var/lib/postgresql/data
    restart: always

volumes:
  model-cache:
  pgdata:

Traefik Middleware for Rate Limiting

labels:
  - "traefik.http.middlewares.immich-ratelimit.ratelimit.average=100"
  - "traefik.http.middlewares.immich-ratelimit.ratelimit.burst=200"
  - "traefik.http.routers.immich.middlewares=immich-headers,immich-ratelimit"

Option 3: Caddy (Simplest Config)

Caddyfile

photos.mydomain.com {
    reverse_proxy immich-server:3001 {
        header_up Host {host}
        header_up X-Real-IP {remote}
        header_up X-Forwarded-For {remote}
        header_up X-Forwarded-Proto {scheme}
    }

    tls admin@mydomain.com

    # Security headers
    header {
        X-Frame-Options "SAMEORIGIN"
        X-Content-Type-Options "nosniff"
        X-XSS-Protection "1; mode=block"
        Referrer-Policy "strict-origin-when-cross-origin"
        Permissions-Policy "geolocation=(), microphone=(), camera=()"
    }

    # Rate limiting (requires caddy-ratelimit plugin)
    rate_limit {
        zone static {
            key static
            events 100
            window 1m
        }
    }

    # Upload size
    request_body {
        max_size 50GB
    }
}

Docker Compose (Caddy)

services:
  caddy:
    image: caddy:2-alpine
    container_name: caddy
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config
    restart: always

  immich-server:
    image: ghcr.io/immich-app/immich-server:release
    container_name: immich_server
    volumes:
      - ${UPLOAD_LOCATION}:/usr/src/app/upload
    env_file:
      - .env
    restart: always

  # ... (redis, postgres, ML same as above)

volumes:
  caddy_data:
  caddy_config:

Performance Tuning

Nginx Buffer Sizes

proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
proxy_temp_file_write_size 256k;

Immich .env for Large Libraries

# .env
UPLOAD_LOCATION=/mnt/photos
DB_HOSTNAME=immich_postgres
DB_USERNAME=immich
DB_PASSWORD=your-secure-password
DB_DATABASE_NAME=immich
REDIS_HOSTNAME=immich_redis

# Increase for large libraries
MACHINE_LEARNING_WORKERS=2
MACHINE_LEARNING_WORKER_TIMEOUT=120

Common Mistakes

Mistake 1: Missing WebSocket Support

Immich uses WebSockets for live updates. Ensure your proxy passes Upgrade and Connection headers.

Mistake 2: Too Small client_max_body_size

Default Nginx limit is 1MB. Immich uploads need 50GB+ for video. Set:

client_max_body_size 50000M;

Mistake 3: Forgetting X-Forwarded-Proto

Immich needs this header to generate correct URLs. All configs above include it.


Conclusion

Summary

Any reverse proxy works with Immich. Caddy is simplest for beginners, Traefik is best for Docker-native stacks, and Nginx offers maximum control for advanced users.

Next Steps

  1. Choose your proxy (Caddy for speed, Nginx for control)
  2. Set DNS A record to your server IP
  3. Deploy the Docker Compose stack
  4. Test with curl -I https://photos.mydomain.com

Affiliate Opportunities

  • Domain registrars: Namecheap, Cloudflare Registrar
  • VPS/cloud: Hetzner, DigitalOcean, Linode
  • Hardware: Mini PCs for home hosting (Minisforum, Beelink)
  • Storage: NAS drives, USB enclosures

Internal Linking

  • immich-setupimmich-photo-server.md
  • docker-composedocker-compose-yml-examples.md
  • traefiktraefik-docker-setup.md
  • ssllets-encrypt-homelab.md

CTA

  • Which reverse proxy do you use? Nginx, Traefik, or Caddy? Vote in the comments.
  • Subscribe for more self-hosted app deployment guides.