Nextcloud Preview Generation in Umgebungen mit begrenzten Ressourcen
Ich betreibe schon seit einigen Jahren meine eigene Nextcloud Instanz und mir hat die Leistung meiner Synology DS1019+ eigentlich immer für den Betrieb gereicht, lediglich die preview generation für Fotos, Videos und andere Medientypen hat nie besonders gut funktioniert. Ursache dafür war schlicht, dass der verbaute Intel Celeron J3455 mit seinen vier Kernen die reguläre on-demand preview generation von Nextcloud nicht schnell genug geschafft hat und seit die Spectre und Meltdown Mitigations aktiviert sind, wurde das Problem noch schlimmer. Abgesehen davon hat mich auch gestört, dass keine Previews für Videos und Audiodateien generiert wurden.
In diesem Beitrag möchte ich daher kurz erklären wie ich dieses Problem mit einem eigenen Docker Image, FFmpeg und der Preview Generator App aus dem Nextcloud App Store gelöst habe.
Das Problem: On-Demand Preview Generation
Nextcloud generiert Previews standardmäßig on-demand, also beim ersten Zugriff auf eine Datei. Bei leistungsstarker Hardware ist das kein Problem. Auf meiner Synology führte das jedoch zu CPU Lastspitzen nahe 100 % und Timeouts der Requests, sobald mehrere Bilder gleichzeitig angefordert wurden. Das trat typischerweise beim Scrollen in der Fotos App auf.
Die Lösung besteht darin, Previews zu generieren, bevor sie tatsächlich benötigt werden. Dazu nutze ich die Preview Generator App, die direkt über den Nextcloud App Store verfügbar ist. Die App überwacht Datei-Events (neu hochgeladen, modifiziert) und speichert diese in einer Warteschlange. Ein Cron-Job arbeitet diese Warteschlange dann im Hintergrund ab, z.B. zu Zeiten, in denen das System eh im Leerlauf ist.
Welches Format braucht welche Abhängigkeit?
Nicht jeder Preview-Provider benötigt dieselben System-Bibliotheken. Bevor wir zum Dockerfile kommen, lohnt sich ein
Blick auf die konfigurierten Formate in der Nextcloud config.php (hier exemplarisch die, welche ich in meiner Instanz
aktiviert habe):
'enabledPreviewProviders' =>
array (
0 => 'OC\\Preview\\BMP',
1 => 'OC\\Preview\\GIF',
2 => 'OC\\Preview\\JPEG',
3 => 'OC\\Preview\\Krita',
4 => 'OC\\Preview\\MarkDown',
5 => 'OC\\Preview\\MP3',
6 => 'OC\\Preview\\OpenDocument',
7 => 'OC\\Preview\\PNG',
8 => 'OC\\Preview\\TXT',
9 => 'OC\\Preview\\XBitmap',
10 => 'OC\\Preview\\HEIC',
11 => 'OC\\Preview\\Movie',
),
Die folgende Übersicht zeigt, welche Abhängigkeiten jeweils erforderlich sind:
| Format | Benötigt FFmpeg? | Anmerkung |
|---|---|---|
| BMP, GIF, JPEG, PNG, XBitmap | Nein | Verarbeitung über die PHP-Erweiterung Imagick, die im Community Nextcloud-Image bereits enthalten ist |
| HEIC | Nein | Ebenfalls über Imagick. Erfordert libheif-Unterstützung, die im aktuellen Community Nextcloud-Image vorhanden ist. Bei älteren Images kann ein separates Paket nötig sein |
| Krita | Nein | Rendering über Imagick |
| MarkDown, TXT, OpenDocument | Nein | Textbasierte Renderung, keine externen Medien-Bibliotheken |
| MP3 | Ja | FFmpeg extrahiert ggf. eingebettete Cover-Art oder generiert ein Platzhalter-Bild |
| Movie | Ja | FFmpeg extrahiert ein Einzelbild aus der Videodatei als Thumbnail |
FFmpeg ist ausschließlich für die Formate MP3 und Movie zuständig. Alle Bildformate werden über die bereits im
Basis-Image enthaltene Imagick-Erweiterung verarbeitet. Wer nur Fotos nutzt und keine MP3- oder Video-Previews benötigt,
kann sich die Installation von FFmpeg komplett sparen.
Ich verwende dieses Setup seit Nextcloud 29. In dieser und allen neueren Versionen war Imagick mit libheif
-Unterstützung bereits im Community Image enthalten, sodass HEIC-Vorschaubilder out-of-the-box funktionierten.
Eigenes Docker-Image bauen
Da das Community Nextcloud Docker Image FFmpeg nicht enthält, baue ich ein eigenes Image. Das Dockerfile ist bewusst minimal:
ARG NEXTCLOUD_VERSION
FROM nextcloud:${NEXTCLOUD_VERSION}-apache AS apache
RUN apt-get update && apt-get upgrade -y && apt-get clean
RUN apt-get install -y --no-install-recommends ffmpeg && apt-get clean
Zwei Anmerkungen dazu:
Warum apt-get upgrade -y? Ich möchte alle verfügbaren Security-Patches einspielen, nicht nur die für Nextcloud
selbst. Das schließt auch das Basissystem und alle transitiven Abhängigkeiten ein. Der Preis dafür ist ein etwas
längerer Build und ein größer ausgefallenes Image, aber der Sicherheitsgewinn rechtfertigt das für mich.
Warum --no-install-recommends? Damit werden nur die strikt notwendigen Abhängigkeiten von FFmpeg installiert,
keine empfohlenen, aber nicht zwingend erforderlichen Pakete. Das hält das Image schlanker.
Das Image pushe ich anschließend in ein privates Docker-Repository. Beim Pull auf dem NAS ist ein vorheriger
docker login bei der Registry erforderlich:
docker login registry.example.com -u <USER> -p <PASSWORD>
docker pull registry.example.com/nextcloud:34
In meinem Setup verwende ich Portainer, hier kann man private Registries direkt auf dem NAS konfigurieren.
CI/CD und Versionierung
Der Build wird in meinem Setup über eine CI/CD-Pipeline gesteuert, die das Git-Tag als Build-Argument übergibt.
Natürlich kann man das Image auch lokal bauen und pushen:
docker build --build-arg NEXTCLOUD_VERSION=34.0.3 \
-t registry.example.com/nextcloud:34.0.3 \
-t registry.example.com/nextcloud:34 \
.
Das erlaubt zwei Strategien:
- Patch-Version-Tag (z. B.
34.0.3): Gezielter Build für eine bestimmte Version. - Major-Version-Tag (z. B.
34): Wird bei jedem Re-Build überschrieben. So kann ich auf dem NAS einfachregistry.example.com/nextcloud:34pullen und immer die neueste Patch-Version innerhalb der Major-Version verwenden.
Ein Major-Update von beispielsweise 34 auf 35 bricht das Image nicht, da Nextcloud beim Start des Containers automatisch eventuell notwendige Datenbank-Migrationen durchführt. Es reicht aus, den Build mit dem neuen Tag anzustoßen und das aktualisierte Image zu deployen, solange das Upstream Image die Versionsnummer bereits bereitstellt.
Ich aktualisiere das Image in der Regel wöchentlich manuell. Ich prüfe vorher die Changelogs und Release Notes, um zu sehen, ob Breaking Changes oder wichtige Sicherheitsfixes enthalten sind. Eine automatisierte CI/CD-Pipeline, die das Image regelmäßig baut, wäre denkbar, ich bevorzuge aber aktuell die manuelle Kontrolle und führe die Pipeline on-demand aus.
Preview Generator App einrichten
Mit folgendem Befehl kann man die Preview Generator App im Container installieren:
docker exec --user www-data <NEXTCLOUD_CONTAINER> php occ app:install previewgenerator
Anschließend kann die initiale Generierung der Previews für alle vorhandenen Dateien angestoßen werden:
docker exec --user www-data <NEXTCLOUD_CONTAINER> php occ preview:generate-all
Je nach Anzahl der Dateien, für die eine Preview generiert werden muss, dauert dieser Schritt entsprechend lange.
Für den laufenden Betrieb kommt preview:pre-generate zum Einsatz. Dieser Befehl verarbeitet nur Dateien, die neu
hinzugekommen sind oder modifiziert wurden.
Laut Dokumentation der Preview Generator App wird empfohlen, den internen Background-Job zu deaktivieren, wenn man einen eigenen System-Cron-Job nutzt:
docker exec --user www-data <NEXTCLOUD_CONTAINER> php occ config:app:set \
--value=true --type=boolean previewgenerator job_disabled
Der System-Cron-Job auf dem NAS sieht wie folgt aus:
*/5 * * * * docker exec --user www-data <NEXTCLOUD_CONTAINER> php occ preview:pre-generate
Alle 5 Minuten wird geprüft, ob neue oder geänderte Dateien vorliegen und deren Previews werden generiert. Das Intervall ist bewusst gewählt, um sicherzustellen, dass neue Previews zügig erstellt werden. Da neue Dateien meist über die mobile App oder den Desktop-Client hochgeladen werden möchte ich, dass die Previews schon kurz nach dem Upload generiert werden damit sie bereitstehen, wenn ich die Web-Oberfläche aufrufe oder die Fotogalerie in der mobile App verwende.
Der Status der Warteschlange lässt sich jederzeit einsehen:
docker exec --user www-data <NEXTCLOUD_CONTAINER> php occ preview:queue-stats
Da der Job alle 5 Minuten läuft, aber die Generierung bei vielen neuen Dateien auch länger dauern kann, bietet es sich an den Timeout für die Generierung entsprechend anzupassen damit der Job nicht mehrmals parallel läuft, wenn der System-Cron ihn wieder ausführt.
docker exec --user www-data <NEXTCLOUD_CONTAINER> php occ config:app:set \
--value=300 --type=integer previewgenerator job_max_execution_time
Je nachdem wie und von wie vielen Usern die Instanz genutzt wird kann auch eine nächtliche Ausführung des Cron-Jobs sinnvoll sein, für meine Instanz mit mir als einzigem User hat sich das beschriebene Setup bewährt.
Fazit
Seit ich dieses Setup betreibe, funktionieren Previews in meiner Nextcloud-Instanz wesentlich flüssiger. Die Fotos-App lädt sofort, weil die Vorschaubilder bereits vorhanden sind. Video-Previews, die vorher komplett fehlten, werden dank FFmpeg ebenfalls generiert.
Der Aufwand beschränkt sich auf ein minimales Dockerfile, eine optionale CI/CD-Pipeline für den Build und einen Cron-Eintrag. Der laufende Aufwand ist eine regelmäßige manuelle Aktualisierung des Images, die sich aber auch automatisieren lässt, wenn man möchte.
Wer seine Nextcloud auch auf schwächerer Hardware betreibt oder viele parallele User hat und mit langsamen oder fehlenden Previews kämpft, für den lohnt sich dieser Ansatz.