Immich einrichten: Google Photos selbst hosten
10. August 2026

Diesen Beitrag habe ich zum Großteil von KI schreiben lassen. Dies ermöglicht es mir, ältere Projekte aufzuarbeiten und zu dokumentieren.
Kurzfassung: Immich auf einem Netcup-VPS, Mediendaten auf externem Block Storage, alles hinter Traefik. Import aus Google Takeout per immich-go, Backup mit Restic nach Infomaniak Swiss Backup — und ein Restore, den ich tatsächlich durchgespielt habe.
Das ist der zweite Teil meiner Degoogle-Serie. Anders als beim Mailserver ist Immich unspektakulär: Es läuft seit März ohne Zwischenfall. Der interessante Teil steckt nicht in der Installation, sondern im Import und im Backup.
Der Import läuft: 12.664 Bilder und 3.469 Videos aus dem Google-Takeout-Export, zusammen 112,6 GB. immich-go zeigt Fortschritt, gestapelte Aufnahmen und übersprungene Duplikate live an.
1. Warum weg von Google Photos
Der offensichtliche Grund ist Kontrolle über die eigenen Bilder. Der weniger offensichtliche ist mir erst beim Export aufgefallen: Ich wusste nicht, wie viele Fotos ich eigentlich habe. Ein Dienst, der alles automatisch schluckt, nimmt einem auch den Überblick darüber ab, was er schluckt.
Dazu kommt der Preis pro Gigabyte, der sich still erhöht, und die Tatsache, dass Google Photos die einzige Kopie ist, wenn man nichts anderes tut. Ein Konto-Ausfall — gesperrt, gehackt, versehentlich gelöscht — und die Bilder von zehn Jahren sind weg.
Was am Ende steht: Fotos und Videos auf eigenem Speicher, erreichbar über fotos.tariwiencke.de, mit einer verschlüsselten Off-Site-Kopie in der Schweiz. Und einer Wiederherstellung, die ich getestet habe, statt sie zu hoffen.
2. Was ist Immich?
Immich ist ein selbst gehosteter Ersatz für Google Photos: Apps für iOS und Android mit automatischem Upload, Weboberfläche, Alben, Gesichtserkennung, Objekterkennung und Volltextsuche über Bildinhalte. Die Funktionen, für die man Google Photos benutzt, sind fast vollständig da.
Zwei Dinge, die man vorher wissen sollte:
Immich verändert deine Originale nicht. Es erzeugt zusätzlich Thumbnails und transkodierte Videos für die Vorschau, aber die Originaldatei bleibt unangetastet. Das ist beruhigend — kostet aber Speicher, weil du effektiv mehrere Fassungen desselben Videos liegen hast.
Die Objekt- und Gesichtserkennung läuft lokal, in einem eigenen Container. Kein Bild verlässt den Server für die Analyse. Dafür braucht dieser Container spürbar RAM.
3. Architektur: VPS, Docker, Block Storage
Internet
│ 443
▼
┌─────────────────┐
│ Traefik │ TLS-Terminierung, Let's Encrypt
└─────────────────┘
│ traefik-net → Port 2283
▼
┌──────────────────────────────────────────────┐
│ immich-server │
│ immich-machine-learning │
│ redis │
│ database (PostgreSQL mit Vektor-Erweiterung)│
└──────────────────────────────────────────────┘
│
▼
/mnt/storage/immich ← Netcup Local Block Storage
Die eine Entscheidung, die ich empfehlen würde: Die Mediendaten liegen nicht auf der System-Disk des VPS, sondern auf separat gebuchtem Block Storage. In der .env:
UPLOAD_LOCATION=/mnt/storage/immich
Der Grund ist banal und wichtig: Fotos wachsen. Ein VPS-Tarif lässt sich nicht beliebig in der Disk-Größe ändern, Block Storage schon. Wer die Bibliothek erst auf der Systemplatte anlegt und später umziehen muss, macht das mit einem gestoppten Dienst und einem mulmigen Gefühl.
Ein Nebeneffekt, der mich später eingeholt hat: Verschiebt man den Pfad, sieht Restic das als neuen Speicherort und beginnt eine eigene Snapshot-Gruppe. Die alten Snapshots unter dem alten Pfad bleiben als getrennte Reihe bestehen; restic rewrite kann den aufgezeichneten Pfad nicht nachträglich ändern. Dank Deduplizierung kostet das kaum Speicher, aber die Snapshot-Liste sieht für immer etwas unordentlich aus.
4. Installation und Traefik-Anbindung
Grundlage ist die offizielle Compose-Datei. Ich habe daran genau zwei Dinge geändert: Netzwerke und Labels für Traefik.
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
depends_on:
- redis
- database
restart: always
networks:
- traefik-net
- immich-net
labels:
- "traefik.enable=true"
- "traefik.http.routers.immich.rule=Host(`fotos.tariwiencke.de`)"
- "traefik.http.routers.immich.entrypoints=websecure"
- "traefik.http.routers.immich.tls=true"
- "traefik.http.routers.immich.tls.certresolver=myresolver"
- "traefik.http.services.immich.loadbalancer.server.port=2283"
- "traefik.docker.network=traefik-net"
Kein ports:-Block. Der Immich-Server ist ausschließlich über Traefik erreichbar, Port 2283 wird nie auf den Host publiziert. Das gilt in meinem Setup für alle Dienste — was nicht in Traefiks Entrypoint-Liste steht, ist von außen nicht ansprechbar, unabhängig davon, was ein Container intern anbietet.
Zwei Netzwerke. traefik-net ist die öffentliche Kante, immich-net verbindet die Immich-Container untereinander. Datenbank und Redis hängen nur im internen Netz.
Die .env
# Mediendaten auf externem Block Storage statt auf der System-Disk
UPLOAD_LOCATION=/mnt/storage/immich
# Datenbank bewusst lokal — Netzwerkfreigaben werden nicht unterstützt
DB_DATA_LOCATION=./postgres
# Auf Major-Version gepinnt statt :release
IMMICH_VERSION=v2
DB_PASSWORD=XXXXXXXX
Zum Version-Pin: Immich entwickelt sich schnell, und release bedeutet, dass ein beiläufiges docker compose pull eine neue Hauptversion einspielen kann. Bei einem Dienst, dessen Datenbank Migrationen mitbringt, will man diesen Moment selbst wählen. Der Pin ist die eine Zeile, die dich davor bewahrt, ein Upgrade zu machen, während du eigentlich etwas anderes vorhattest.
Zum Datenbankpfad: DB_DATA_LOCATION gehört bewusst nicht auf eine Netzwerkfreigabe — das steht so in Immichs eigener Vorlage und ist kein Vorschlag, sondern eine Einschränkung. Postgres auf einem Netzwerk-Dateisystem ist ein zuverlässiger Weg zu Datenkorruption.
Danach:
docker compose up -d
5. Google-Takeout-Export vorbereiten
Über takeout.google.com einen Export von Google Fotos anfordern. Zwei Punkte, die den Unterschied machen:
Große Archivgröße wählen (50 GB statt der Voreinstellung). Sonst zerlegt Google den Export in Dutzende Teile, und jedes einzelne muss heruntergeladen werden, bevor der Download-Link abläuft.
Der Export ist zeitlich begrenzt. Die Links laufen nach wenigen Tagen ab, und Google deckelt die Anzahl der Exporte pro Zeitraum. Plane den Download für einen Tag ein, an dem du wirklich Zeit hast.
Die Zip-Dateien lädst du am besten direkt auf den Server, nicht auf den eigenen Rechner und dann weiter — bei mehreren hundert Gigabyte spart das einen kompletten Übertragungsweg.
Warum Takeout und nicht die Google-Photos-API: Der Takeout-Export enthält die JSON-Metadaten mit den originalen Aufnahmedaten, Alben und Geokoordinaten. Über die API bekommt man die nicht vollständig. Genau diese Metadaten braucht der Importer im nächsten Schritt, damit die Bilder nicht alle auf das Importdatum rutschen.
6. Import mit immich-go
immich-go ist das Werkzeug für den Massenimport. Es versteht die Takeout-Struktur samt Metadaten-JSONs und kann direkt aus den Zips lesen — kein Entpacken nötig.
Zuerst einen API-Key in Immich anlegen: Account Settings → API Keys.
Aus Google Takeout
immich-go upload from-google-photos \
--server=http://localhost:2283 \
--api-key=<dein-key> \
--client-timeout=120m \
--manage-raw-jpeg=StackCoverRaw \
--manage-burst=Stack \
--manage-heic-jpeg=StackCoverJPG \
--verbose \
takeout-*.zip
Die drei --manage-*-Flags sind der Grund, warum sich der Aufwand lohnt:
| Flag | Was es löst |
|---|---|
--manage-raw-jpeg=StackCoverRaw | RAW und JPEG desselben Fotos werden zu einem Eintrag gestapelt, RAW als Titelbild |
--manage-burst=Stack | Serienaufnahmen werden zu einem Eintrag zusammengefasst, statt die Timeline zu fluten |
--manage-heic-jpeg=StackCoverJPG | iPhone-Doppelablagen (HEIC + JPEG) werden gestapelt, JPEG als Titelbild |
Ohne sie landen alle Varianten als eigenständige Bilder in der Bibliothek. Bei einer Serienaufnahme von dreißig Fotos ist das der Unterschied zwischen einer benutzbaren und einer unbrauchbaren Timeline.
--client-timeout=120m ist bei großen Zips nötig — der Default reicht nicht, und der Import bricht sonst mittendrin ab.
Wichtig: --server=http://localhost:2283, nicht die öffentliche HTTPS-Adresse. Der Import läuft auf dem Server selbst, und der Weg über Traefik bringt hier nichts außer einer weiteren Fehlerquelle bei stundenlangen Uploads.
Aus lokalen Ordnern
Fotos, die nicht bei Google lagen — alte Festplatten, Kameraexporte — habe ich zuerst auf den Server übertragen und dann importiert.
Für die Übertragung rclone, und zwar iterativ: Es kamen laufend Bilder dazu, und ich habe unterwegs Ordner umbenannt.
rclone sync "/Volumes/tariwiencke/fotos-export" netcup:/mnt/onedrive/ssd \
-P --track-renames --checksum
--track-renames und --checksum sind hier entscheidend. Ohne sie lädt rclone eine umbenannte Datei komplett neu hoch, statt sie serverseitig umzubenennen. Bei einem halben Terabyte ist das der Unterschied zwischen Minuten und einem Tag.
Dann der Import:
immich-go upload from-folder \
--server=https://fotos.tariwiencke.de \
--api-key=<dein-key> \
--folder-as-album=FOLDER \
--manage-raw-jpeg=StackCoverRaw \
--client-timeout=120m \
--verbose \
/mnt/onedrive/ssd
--folder-as-album=FOLDER macht aus jedem Ordner ein Album. Wenn deine Ordnerstruktur eine Bedeutung hat — nach Reise, nach Jahr, nach Anlass — überträgt sich diese Struktur direkt in Immich, statt sie von Hand nachzubauen.
⚠️ Missverständnis, das ich klarstellen will:
rcloneist hier ein reines Migrationswerkzeug, kein Bestandteil der Backup-Strategie. Es hat Dateien einmalig auf den Server geschoben. Das laufende Backup macht ausschließlich Restic (Kapitel 8). Diese Trennung ist wichtig, weil eine Kopie auf demselben Server kein Backup ist.
7. Videos komprimieren
Immich komprimiert Originale nicht. Bei Handyvideos aus mehreren Jahren summiert sich das erheblich — und ein 4K-Video vom Familienessen braucht keine 4K.
Vor dem Import einmal durch ffmpeg:
for f in *.MOV; do
ffmpeg -i "$f" -movflags use_metadata_tags -map_metadata 0 \
-vf "scale=-2:1080" -c:v h264_videotoolbox -preset slow -crf 23 \
-c:a copy "out/$f"
done
Die beiden Teile, auf die es ankommt:
-map_metadata 0 und -movflags use_metadata_tags übernehmen die Metadaten aus der Quelldatei. Ohne sie verlierst du Aufnahmedatum und Geokoordinaten — und dann sortiert Immich alle komprimierten Videos auf das Konvertierungsdatum. Das merkt man erst nach dem Import, und dann ist es mühsam.
-c:a copy lässt die Tonspur unangetastet. Audio ist klein, neu zu kodieren bringt kaum etwas und kostet Qualität.
h264_videotoolbox ist Hardware-Beschleunigung auf Apple Silicon. Auf anderen Systemen stattdessen libx264 (langsamer, etwas bessere Kompression) oder das passende Pendant der jeweiligen Grafikhardware.
Vorher an einer Datei testen und das Ergebnis ansehen. Kompression ist verlustbehaftet und nicht umkehrbar. Ich habe die Originale erst gelöscht, nachdem die komprimierten Fassungen importiert und in Immich sichtbar waren.
8. Backup mit Restic
Hier wird es ernst. Ein selbst gehosteter Fotoserver ohne Backup ist eine schlechtere Lösung als Google Photos, nicht eine bessere.
Zwei Teile, die zusammengehören
Ein Immich-Backup besteht aus Mediendateien und Datenbank. Die Dateien allein nützen wenig: Alben, Gesichtszuordnungen, Stapel und Metadaten liegen in Postgres. Wer nur UPLOAD_LOCATION sichert, hat am Ende einen Ordner voller Bilder ohne jede Struktur.
Den Datenbank-Dump macht Immich selbst. Unter System Settings → Database Dump Settings:
- Enable database dumps: an
- Cron expression:
0 02 * * * - Amount of previous dumps to keep: 14
Die Dumps landen im backups-Unterordner von UPLOAD_LOCATION — und damit automatisch im selben Restic-Lauf wie die Bilder. Das ist der Grund, warum dieses Setup ohne eigenen pg_dump-Schritt auskommt.
Das Backup-Skript
#!/bin/bash
source /opt/immich/.env
source /root/.restic-env
if find "$UPLOAD_LOCATION/backups" -name "immich-db-backup-*.sql.gz" -mtime -1 | grep -q .; then
echo "DB backup is fresh"
restic backup "$UPLOAD_LOCATION" --host "netcup" --tag "immich" \
--exclude "$UPLOAD_LOCATION/thumbs" \
--exclude "$UPLOAD_LOCATION/encoded-video"
curl -fsS -m 10 --retry 5 https://hc-ping.com/<deine-uuid>
else
echo "No recent DB backup. Skipping restic."
exit 1
fi
0 3 * * * /opt/restic/immich-backup.sh >> /opt/restic/immich-backup.log 2>&1
Vier Entscheidungen darin, die ich für die wichtigsten halte:
Die find-Prüfung. Existiert kein Datenbank-Dump jünger als 24 Stunden, läuft Restic gar nicht erst. Ohne diese Bedingung würdest du über Wochen brav Bilder sichern — mit einer Datenbank vom letzten Monat. Ein stilles, halb kaputtes Backup ist schlimmer als gar keins, weil du dich in Sicherheit wiegst.
Der zeitliche Abstand. Immich dumpt um 02:00, Restic läuft um 03:00. Eine Stunde Puffer, damit der Dump garantiert fertig ist.
Die Ausschlüsse. thumbs und encoded-video sind aus den Originalen wiederherstellbar — Immich generiert sie neu. Sie zu sichern kostet Speicher und Upload-Zeit für Daten, die reproduzierbar sind.
Der Ping nur im Erfolgsfall. healthchecks.io erwartet täglich ein Lebenszeichen. Bleibt es aus — weil der Dump fehlte, Restic scheiterte oder der Server tot ist — kommt eine Meldung. Das ist ein Dead-Man's-Switch: Er meldet Abwesenheit, nicht Fehler. Genau deshalb funktioniert er auch, wenn der Server komplett weg ist.
Wohin gesichert wird
Ziel ist Infomaniak Swiss Backup als Swift-Objektspeicher. Auswahlkriterien: europäischer Anbieter, aber geografisch getrennt vom Server. Ein Backup im selben Rechenzentrum ist eine Kopie, kein Backup.
Die Zugangsdaten liegen in /root/.restic-env, bewusst außerhalb des Git-Checkouts:
export OS_AUTH_URL=https://swiss-backup04.infomaniak.com/identity/v3
export OS_PROJECT_NAME=sb_project_XXXXXXXX
export OS_USERNAME=SBI-XXXXXXXX
export OS_PASSWORD=XXXXXXXX
export RESTIC_REPOSITORY=swift:sb_project_XXXXXXXX:/netcup
export RESTIC_PASSWORD_FILE=/root/.restic-password
Restic verschlüsselt vor dem Upload. Der Anbieter sieht nur verschlüsselte Blobs. Selbst bei kompromittiertem Speicherzugang bleiben die Bilder unlesbar.
⚠️ Der wichtigste Satz dieses Artikels:
.restic-passwordist der einzige Schlüssel zu allen Backups. Restic hat keine Wiederherstellungsfunktion dafür — kein Support, kein Reset, keine Hintertür. Ohne dieses Passwort sind die Daten mathematisch verloren.Es gehört an einen zweiten, unabhängigen Ort. Und zwar an einen, der nicht selbst vom Server abhängt: Liegt es nur im Passwortmanager, und der Passwortmanager läuft auf demselben Server, hast du eine Ringabhängigkeit gebaut, die genau im Ernstfall zuschnappt.
9. Restore — der Teil, den die meisten überspringen
Ich habe die Wiederherstellung durchgespielt, bevor ich mich auf das Backup verlassen habe. Das ist der Unterschied zwischen einer Absicherung und einer Vermutung.
# 1. Immich stoppen
docker compose down
# 2. In einen Testpfad restaurieren, NICHT über die Produktivdaten
restic restore <snapshot-id> --target /tmp/immich-restore
# 3. .env auf den Testpfad umbiegen, Container starten
docker compose up -d
4. Datenbank importieren über die Weboberfläche: Administration → Maintenance → Restore database backup, dort den wiederhergestellten .sql.gz-Dump auswählen. Das ist eine offizielle, dokumentierte Immich-Funktion — kein manuelles psql-Gefummel.
Drei Dinge, die dabei wichtig sind:
In einen Testpfad restaurieren, nie über die Produktivdaten. Ein Restore-Test, der die funktionierende Installation zerstört, ist ein selbst gebauter Notfall.
Der DB-Import überschreibt die komplette Datenbank. Immich legt vorher automatisch einen Rollback-Punkt an, falls der Import scheitert — beruhigend, aber kein Grund, sorglos zu werden.
Kein Versionssprung parallel zum Restore. Ein Backup aus einer älteren Immich-Version kann zusätzliche Datenbankmigrationen brauchen. Im Ernstfall stellt man zuerst auf derselben Version wieder her und aktualisiert danach — zwei Probleme gleichzeitig zu lösen geht selten gut.
Was mein Test nicht abdeckt, und das sage ich bewusst dazu: Die Verifikation war ein Sichtcheck in der Weboberfläche. Ich habe nicht automatisiert abgeglichen, ob die Asset-Anzahl stimmt, alle Alben da sind und keine einzelne Datei fehlt. Grobe Fehler hätte ich gesehen, feine nicht. Das ist der nächste Ausbauschritt, kein erledigter Punkt.
10. Wartung: Prune und Integritätsprüfung
Ohne Aufräumen wächst ein Restic-Repository unbegrenzt. Ein monatlicher Job erledigt beides — alte Snapshots verwerfen und die Daten prüfen:
#!/bin/bash
set -euo pipefail
source /root/.restic-env
restic forget \
--host "netcup" \
--keep-daily 30 \
--keep-weekly 12 \
--keep-monthly 60 \
--keep-yearly 100 \
--prune
restic check --read-data
curl -fsS -m 10 --retry 5 https://hc-ping.com/<eigene-uuid>
0 5 1-7 * 0 /opt/restic/restic-prune.sh >> /opt/restic/restic-prune.log 2>&1
--prune sperrt das Repository exklusiv, deshalb bewusst außerhalb des täglichen 03:00-Fensters: erster Sonntag im Monat, 05:00 Uhr.
restic check --read-data prüft den kompletten Datenbestand, nicht nur eine Stichprobe. Bei rund 318 GiB dauert das gemessen 20 Minuten — deutlich weniger, als ich erwartet hatte. Bei Infomaniak ist es zudem kostenneutral, weil dort eine Kapazitäts-Flatrate ohne Egress-Gebühren gilt. Bei Anbietern, die ausgehenden Traffic berechnen, wäre ein monatlicher Volldurchlauf eine teure Gewohnheit.
set -euo pipefail sorgt dafür, dass das Skript bei jedem Fehler sofort abbricht — der Erfolgs-Ping wird dann nie erreicht, und healthchecks.io schlägt an. Dasselbe Prinzip wie beim Backup-Skript: Fehler dürfen nicht still durchrutschen.
Ein eigener healthchecks-Check für diesen Job, nicht derselbe wie fürs tägliche Backup. Sonst maskiert ein grüner Backup-Lauf einen fehlgeschlagenen Prune.
Zwei Erfahrungen aus dem ersten Lauf
Ein falsch gesetzter Cron produziert erstaunlich wenig Schaden. Bei mir lief das Backup zwei Tage lang versehentlich alle vier Minuten statt täglich — rund 860 zusätzliche Snapshots. Beim Aufräumen wurden davon 965 Snapshots entfernt und dabei nur etwa 216 MiB frei. Restics Deduplizierung hatte die fast identischen Läufe längst zusammengefasst; teuer war nur die aufgeblähte Snapshot-Liste, nicht der Speicher.
Lange Läufe nicht direkt im SSH-Terminal starten. Ein Verbindungsabbruch während --read-data hinterlässt einen verwaisten Lock im Repository, den man danach von Hand lösen muss. Für den manuellen Erstlauf lieber nohup … &.
11. Fazit
Was gut funktioniert: Immich ist der unauffälligste Dienst in meinem Setup. Die Apps laden zuverlässig hoch, die Suche über Bildinhalte funktioniert erstaunlich gut, und seit März gab es keinen Zwischenfall. Von allen Google-Ersetzungen war das die leichteste.
Was Arbeit macht: Der Import. Nicht technisch — immich-go funktioniert — sondern weil man vorher entscheiden muss, wie RAW/JPEG-Paare, Serienaufnahmen und iPhone-Doppelablagen behandelt werden. Diese Entscheidung nachträglich zu ändern bedeutet, von vorne anzufangen.
Was ich anders machen würde: Die Kompression vor dem ersten Import erledigen, nicht danach. Ich habe erst importiert und dann festgestellt, wie viel Platz die Videos brauchen — und dann Dateien ersetzt, die schon in der Bibliothek waren.
Der eigentliche Aufwand liegt nicht bei Immich, sondern beim Backup. Die Installation ist ein Nachmittag. Ein Backup mit getestetem Restore, Off-Site-Kopie, Verschlüsselung und Überwachung ist ein zweiter. Wer den zweiten überspringt, hat Google Photos durch etwas Schlechteres ersetzt — bequem, bis es das einzige Mal darauf ankommt.
Weiterführende Artikel
- Ein VPS, sieben Dienste, ein Repo — der Gesamtaufbau, in den dieser Dienst eingebettet ist.
- Mein Johnny-Decimal-System für Dateien und Ordner — was mit dem Rest des Google-Exports passiert ist.
- Stalwart Mail Server einrichten — der aufwendigste Dienst im selben Stack.
Quellen
- Immich-Dokumentation — insbesondere Docker Compose, Umgebungsvariablen und Backup and Restore
- immich-go — Importwerkzeug für Takeout und lokale Ordner
- Restic-Dokumentation
- rclone —
--track-renamesund--checksum - Infomaniak Swiss Backup: Preise