Immich zu Hause: Sicherer Zugriff für die Familie, ohne den Server offenzulegen

Immich zu Hause zu betreiben ist immer ein Kompromiss zwischen Sicherheit, Komfort und Zuverlässigkeit beim Hochladen. In der Praxis gibt es drei gängige Möglichkeiten, den Server freizugeben: direkter Zugriff, VPN-Zugriff oder Cloudflare vor einem Reverse-Proxy. Jede Option verändert sowohl die Angriffsfläche als auch die Funktionsweise bei großen Uploads.

Drei Zugriffsmodelle

Direkter Zugriff

Die einfachste Konfiguration ist die reine Portweiterleitung. Das funktioniert zwar, schafft aber auch die größte öffentliche Angriffsfläche. Für ein privates Fotoarchiv ist das in der Regel die riskanteste Option.

VPN-Zugriff

Ein VPN wie WireGuard ist das sicherste Modell. Das Smartphone verbindet sich zuerst mit dem Heimnetzwerk und greift dann auf Immich zu, als wäre es ein interner Dienst. Dadurch bleibt die öffentliche Angriffsfläche klein, da die App nicht wie eine normale öffentliche Website nach außen hin sichtbar ist.

Für einen einzelnen Nutzer oder einen technisch versierten Haushalt ist dies oft die sicherste Option. Der Nachteil ist die Benutzerfreundlichkeit. Jedes Smartphone benötigt einen VPN-Client, der installiert, konfiguriert und aktiv sein muss. Bei Familienmitgliedern bricht das saubere Design oft genau an dieser Stelle zusammen.

Cloudflare davor

Cloudflare ist der praktische Kompromiss. Es bietet dem Dienst einen öffentlichen Zugangspunkt, ohne den Immich-Container direkt dem Internet auszusetzen. In den eigenen Fernzugriffs-Dokumenten von Immich wird erwähnt, dass ein Remote-Reverse-Proxy wie Cloudflare die Sicherheit verbessern kann, indem er die Server-IP verbirgt und gezielte Angriffe erschwert.

Das macht Cloudflare attraktiv, wenn das Ziel ein einfacher Zugriff für die Familie ist, ohne dass auf jedem Gerät ein VPN erforderlich ist. Der Nachteil ist, dass Cloudflare Teil des Anfragepfads wird, und das spielt bei großen Uploads eine Rolle.

Warum Cloudflare der richtige Ausgangspunkt war

Bei einem gemeinsamen Familienfoto-Dienst ist Komfort fast genauso wichtig wie Sicherheit. Eine reine VPN-Konfiguration ist aus Sicherheitssicht sauberer, lässt sich aber bei technisch weniger versierten Nutzern schwerer umsetzen. Cloudflare bietet ein normales App- und URL-Erlebnis, ohne den Immich-Dienst direkt zu veröffentlichen.

Daher ist Cloudflare hier eine gute erste Schutzebene. Es ist nicht sicherer als ein reines VPN-Design, aber deutlich besser als eine direkte Exposition. Und es löst das Problem des Familienzugangs viel besser als WireGuard allein.

Der Haken: große Uploads

Der größte Nachteil zeigt sich bei großen Dateien. Der Cloudflare-Weg wird oft zum limitierenden Faktor für Uploads, und die häufig gemeldete Obergrenze liegt bei etwa 100 MB bei Proxy-Konfigurationen der unteren Preisklasse. Das macht sich bei größeren Videos sehr schnell bemerkbar.

Das eigentliche Problem ist also nicht, dass MP4-Dateien etwas Besonderes sind. Das Problem ist, dass der Cloudflare-Weg zwar bequem und sicherer für den öffentlichen Zugriff ist, aber nicht der beste Weg für große Uploads von vertrauenswürdigen Geräten darstellt.

Die praktische Lösung

Die sinnvolle Lösung ist ein Hybridmodell. Behalte Cloudflare als öffentlichen Zugang für den normalen und familienfreundlichen Zugriff bei, lass aber vertrauenswürdige Geräte über WireGuard und lokales DNS einen direkten Weg zum Heimserver nutzen.

So bleiben der Komfort und die geringere Sicherheitsrisiken von Cloudflare erhalten, während der Upload-Engpass für dein eigenes Smartphone oder andere vertrauenswürdige Clients vermieden wird. Die gleiche Immich-URL funktioniert weiterhin überall. Nur der Netzwerkpfad ändert sich.

So funktioniert die Einrichtung

Der öffentliche Pfad ist mehrschichtig aufgebaut, sodass Immich nicht direkt von Docker aus ins Internet exponiert ist. Der Internetverkehr geht zuerst an Cloudflare, Cloudflare leitet ihn über den benannten Tunnel weiter fyp, der Tunnel sendet Anfragen an Nginx auf localhost:8283, und Nginx leitet den Haupt-Immich-Hostnamen an den Immich-Container auf 127.0.0.1:2283.

Parallel dazu gibt es auch einen direkten lokalen HTTPS-Pfad für vertrauenswürdige Clients. Im Heimnetzwerk lauscht Nginx ebenfalls auf Port 443 mit TLS, sodass LAN-Geräte denselben Immich-Dienst erreichen können, ohne die Cloudflare-Route zu nutzen.

Beispiel für den Datenfluss

Internet
  └─► Cloudflare
        └─► cloudflared tunnel "fyp"
              └─► localhost:8283 (nginx)
                    └─► http://127.0.0.1:2283 (Immich Docker)

Local network
  └─► nginx :443 SSL
        └─► http://127.0.0.1:2283 (Immich Docker)

Hostnamen-Struktur

Eine übersichtliche Version dieser Konfiguration kann zwei Hostnamen verwenden:

  • photos.example-family.net für die vollständige Immich-App
  • share-photos.example-family.net nur für öffentliche Freigabelinks

Diese Trennung ist sinnvoll, da Nginx unterschiedliche Verhaltensweisen anwenden kann, server_name selbst wenn Cloudflare beide über denselben lokalen Listener weiterleitet. Die Haupt-App-Domain stellt die vollständige Benutzeroberfläche bereit. Die Freigabe-Domain kann auf freigabebezogene Pfade beschränkt werden und 403 für alles andere.

Nginx-Design

Der Haupt-Nginx-Block nutzt ein Dual-Listen-Design. Er lauscht auf 127.0.0.1:8283 auf cloudflaredund außerdem auf :443 ssl für den direkten lokalen HTTPS-Zugriff von LAN- und VPN-Clients.

Die Proxy-Konfiguration benötigt die von Immich erwarteten Header: Host, X-Real-IP, X-Forwarded-Proto, und X-Forwarded-For. In einer Cloudflare-basierten Konfiguration real_ip_header CF-Connecting-IP ist wichtig, damit Nginx die ursprüngliche Client-IP wiederherstellen kann, anstatt nur die Tunnelquelle zu sehen. X-Forwarded-Proto https und X-Forwarded-Port 443 hilft Immich dabei, hinter dem Proxy korrekte Weiterleitungs-URLs zu generieren.

Außerdem braucht es WebSocket-Unterstützung, ausreichend große Limits für den Request-Body und Timeouts, die verhindern, dass der Reverse-Proxy beim Hochladen zum ersten Ausfallpunkt wird.

Cloudflare-Tunnel-Rolle

Der Tunnel ordnet die öffentlichen Immich-Hostnamen localhost:8283, und Nginx trennt sie durch server_name. So kann ein Tunnel mehrere interne virtuelle Hosts bedienen, ohne den Anwendungscontainer direkt offenzulegen.

Das ist hier der eigentliche Vorteil der Cloudflare-Ebene. Sie bietet einen einfachen öffentlichen Einstiegspunkt für den privaten Gebrauch und reduziert die direkte Exposition des Heim-Servers. Aber es ist immer noch nicht der ideale Upload-Pfad für vertrauenswürdige Clients und große Dateien.

Lokales DNS und Heimzugang

Um zu vermeiden, dass vertrauenswürdiger Heimverkehr über Cloudflare geleitet wird, nutzt das Heimnetzwerk eine lokale DNS-Überschreibung für den Haupt-Immich-Hostnamen. Die FRITZ!Box unterstützt die Zuweisung eines lokalen DNS-Servers an LAN-Clients im Bereich „Heimnetzwerk → Netzwerk → Netzwerkeinstellungen → IPv4“, sodass Geräte im WLAN den Immich-Hostnamen zur lokalen Server-IP auflösen können, anstatt den öffentlichen Cloudflare-Pfad zu nutzen.

Das bedeutet, dass dieselbe URL sowohl öffentlich als auch privat funktioniert. Außerhalb des Hauses erfolgt die Auflösung über Cloudflare. Innerhalb des Hauses erfolgt die Auflösung direkt zum lokalen Nginx-Endpunkt und nutzt den lokalen HTTPS-Pfad.

WireGuard als Pfad für vertrauenswürdige Geräte

WireGuard erweitert dieses Konzept auf mobile Geräte. Bei diesem Ansatz verbindet sich das Smartphone zuerst mit dem Heimnetzwerk, erhält die privaten DNS-Einstellungen und erreicht denselben Immich-Hostnamen über den direkten Pfad statt über Cloudflare.

Das ist die elegante Lösung für große Uploads. Familiengeräte können weiterhin den einfachen öffentlichen Weg nutzen. Vertrauenswürdige Geräte wie dein eigenes Smartphone können WireGuard und den lokalen DNS-Weg für direkte, zuverlässigere Uploads nutzen.

Sicherheitsbewertung

Dieses Design ist sicherer, als Immich direkt dem Internet auszusetzen. Immich bleibt hinter Docker und Nginx verborgen, und Cloudflare reduziert die direkte Sichtbarkeit der Heim-IP.

Gleichzeitig ist es immer noch nicht sicherer als ein reines VPN-Modell. Eine reine VPN-Konfiguration sorgt für die kleinste öffentliche Angriffsfläche, da die Anwendung selbst überhaupt nicht öffentlich zugänglich ist.

Deshalb funktioniert der hybride Ansatz in der Praxis gut. Cloudflare ist die familienfreundliche „Haustür“. WireGuard und das lokale DNS bieten den besseren Weg für vertrauenswürdige Geräte, besonders wenn es um große Uploads geht.

Nach all der Theorie – hier ist die detaillierte Einrichtung

Schritt 1 – Docker / Immich

Erstelle /home/<user>/immich-app/.env:

UPLOAD_LOCATION=/data/photo_archive/immich_upload
DB_DATA_LOCATION=/home/<user>/immich-app/postgres

IMMICH_VERSION=release

DB_PASSWORD=<strong-random-password>
DB_USERNAME=postgres
DB_DATABASE_NAME=immich

Erstelle docker-compose.yml:

name: immich

services:
  immich-server:
    container_name: immich_server
    image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
    volumes:
      - ${UPLOAD_LOCATION}:/usr/src/app/upload
      - /etc/localtime:/etc/localtime:ro
    env_file:
      - .env
    ports:
      - '2283:2283'
    depends_on:
      - redis
      - database
    restart: always

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

  redis:
    container_name: immich_redis
    image: docker.io/redis:6.2-alpine
    restart: always

  database:
    container_name: immich_postgres
    image: ghcr.io/immich-app/postgres:14-vectorchord0.3.0-pgvectors0.2.0
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_USER: ${DB_USERNAME}
      POSTGRES_DB: ${DB_DATABASE_NAME}
      POSTGRES_INITDB_ARGS: '--data-checksums'
    volumes:
      - ${DB_DATA_LOCATION}:/var/lib/postgresql/data
    restart: always

volumes:
  model-cache:

Immich starten:

docker compose up -d

Immich ist nun auf dem Host unter http://127.0.0.1:2283.


Schritt 2 — Cloudflare-Einrichtung

2a. Voraussetzungen

  • Eine in Cloudflare verwaltete Domain (Nameserver verweisen auf Cloudflare)
  • Cloudflare Zero Trust aktiviert (die kostenlose Stufe reicht aus)

2b. Erstelle den Tunnel im Cloudflare-Dashboard

  1. Geh zum Cloudflare-Dashboard → Zero Trust → Netzwerke → Tunnel
  2. Klicke auf „Tunnel erstellen“ → wähle „Cloudflared“
  3. Gib dem Tunnel einen Namen (z. B. my-tunnel)
  4. Befolge die Installationsanweisungen auf dem Bildschirm – dadurch wird das cloudflared Paket bereit und die Datei mit den Tunnel-Anmeldedaten generiert
  5. Öffentliche Hostnamen hinzufügen:
Subdomain Domäne Dienst
<main-subdomain> your-domain.org http://localhost:8283
<share-subdomain> your-domain.org http://localhost:8283

Cloudflare erstellt die DNS-CNAME-Einträge automatisch – es sind keine manuellen DNS-Einträge erforderlich.

2c. SSL/TLS-Einstellung

Geh im Cloudflare-Dashboard für deine Domain zu SSL/TLS → Übersicht und stelle den Verschlüsselungsmodus auf „Vollständig (streng)“ ein.

Das bedeutet, dass Cloudflare das Let’s Encrypt-Zertifikat deines Servers bei der Verbindung überprüft. Dazu ist ein gültiges Zertifikat auf dem Server erforderlich (in Schritt 3 eingerichtet).

2d. Installiere „cloudflared“ auf dem Server

# Debian / Ubuntu
curl -L --output cloudflared.deb \
  https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
sudo dpkg -i cloudflared.deb

2e. Tunnel-Konfigurationsdatei

Erstelle ~/.cloudflared/config.yml:

tunnel: <tunnel-name>
credentials-file: /home/<user>/.cloudflared/<tunnel-id>.json

ingress:
  - hostname: <main-subdomain>.your-domain.org
    service: http://localhost:8283
  - hostname: <share-subdomain>.your-domain.org
    service: http://localhost:8283
  - service: http_status:404   # catch-all: required, rejects unknown hostnames

no-autoupdate: true

Beide Hostnamen verweisen auf Nginx am Port 8283. Nginx leitet sie je nach server_name.

2f. Führe cloudflared als systemd-Dienst aus

sudo cloudflared service install
sudo systemctl enable --now cloudflared

# Verify
sudo systemctl status cloudflared

Schritt 3 – TLS-Zertifikat (Let’s Encrypt)

Das Zertifikat erfüllt zwei Zwecke:

  • Aktiviert HTTPS auf Port 443 für den direkten Zugriff aus dem lokalen Netzwerk
  • Erfüllt die „Full (strict)“-Validierung von Cloudflare, wenn eine Verbindung zu deinem Server hergestellt wird
sudo apt install certbot python3-certbot-nginx
sudo certbot certonly --nginx -d <main-subdomain>.your-domain.org

Mit dem --nginx Flag ermöglicht es Certbot, vorübergehend Nginx zu nutzen, um die ACME-Challenge abzuschließen. Das Zertifikat wird unter /etc/letsencrypt/live/<domain>/.

Automatische Verlängerung

Certbot installiert einen systemd-Timer, der das Zertifikat vor Ablauf automatisch erneuert. Nach der Erneuerung muss nginx neu geladen werden, damit das neue Zertifikat übernommen wird:

# Create a deploy hook so nginx reloads after every renewal
echo "systemctl reload nginx" | sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh

# Verify the timer is active
sudo systemctl status certbot.timer

# Test renewal (dry run)
sudo certbot renew --dry-run

Schritt 4 – Nginx

Nginx verwaltet zwei separate Serverblöcke für Immich. Füge sie /etc/nginx/nginx.conf innerhalb des http {} Block.

map $http_upgrade $connection_upgrade { default upgrade; '' close; }

# --- Main Immich server ---
# Listens on 127.0.0.1:8283 (cloudflared) AND :443 (local network HTTPS)
server {
    listen 127.0.0.1:8283;
    listen 443 ssl http2;
    server_name <main-subdomain>.your-domain.org;

    ssl_certificate     /etc/letsencrypt/live/<main-subdomain>.your-domain.org/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/<main-subdomain>.your-domain.org/privkey.pem;

    client_max_body_size 50000M;
    client_body_timeout 600s;

    # Restore real visitor IP (cloudflared connects from localhost)
    real_ip_header CF-Connecting-IP;
    set_real_ip_from 127.0.0.1;

    # Required for OAuth to generate correct redirect URLs
    proxy_set_header Host              $host;
    proxy_set_header X-Forwarded-Host  $host;
    proxy_set_header X-Forwarded-Proto https;
    proxy_set_header X-Forwarded-Port  443;
    proxy_set_header X-Real-IP         $remote_addr;
    proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;

    # WebSocket support (required by Immich UI)
    proxy_http_version 1.1;
    proxy_set_header Upgrade    $http_upgrade;
    proxy_set_header Connection $connection_upgrade;
    proxy_request_buffering off;

    proxy_read_timeout 600s;
    proxy_send_timeout 600s;
    send_timeout       600s;

    location / {
        proxy_pass http://127.0.0.1:2283;
    }
}

# --- Share-only domain ---
# Serves Immich share links only; all other paths return 403
server {
    listen 8283;
    server_name <share-subdomain>.your-domain.org;

    set $backend "http://127.0.0.1:2283";

    proxy_set_header Host              $http_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_http_version 1.1;
    proxy_set_header Upgrade    $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_redirect off;
    proxy_read_timeout 600s;
    proxy_send_timeout 600s;
    send_timeout       600s;

    # Allow share paths and all static assets Immich needs to render them
    location ~* ^/(share|api|_app|assets/|static/|fonts/|favicon.*\.png|apple-icon-180\.png|favicon\.ico|.*\.js|.*\.css|.*\.woff2?|.*\.ttf) {
        proxy_pass $backend;
    }

    # Block everything else
    location / {
        return 403;
    }
}

Testen und übernehmen:

sudo nginx -t && sudo systemctl reload nginx

Wichtige Hinweise zum Design

  • Port 8283 ist die interne Übergabe zwischen Cloudflared und Nginx – er wird niemals nach außen freigegeben.
  • Port 2283 ist Immich selbst – nur von localhost über Nginx erreichbar.
  • real_ip_header CF-Connecting-IP ist unerlässlich. Ohne ihn werden Immich-Protokolle und jegliche Ratenbegrenzung 127.0.0.1 für jede Anfrage.
  • X-Forwarded-Proto: https und X-Forwarded-Port: 443 sind erforderlich, damit Immich OAuth korrekte Callback-URLs erstellen kann.
  • Die Whitelist für die „Share-only“-Domain muss /api sowie die Dateiendungen für statische Dateien enthalten – Immich lädt Assets aus diesen Pfaden auch für Freigabeseiten.
  • Der map $http_upgrade $connection_upgrade Block muss auf der http {} Ebene deklariert werden, nicht innerhalb eines Server-Blocks.
  • Die Catch-All-Regel - service: http_status:404 am Ende der Cloudflared-Ingress-Regeln ist erforderlich – ohne ihn weigert sich Cloudflared, zu starten.