Auf der Suche nach dem richtigen Blog und warum es Grav wurde
Ich versuche es, meinen Ubuntu-VPS bis aufs Blut zu optimieren. Mein Ziel für letzes Wochenende war eigentlich simpel: Ein knackiges, schnelles, ressourcenschonendes Flat-File-CMS in Docker zu gießen, um hier meine Tech-Notizen und Anleitungen zu veröffentlichen.
Keine schwere MySQL-Datenbank im Hintergrund, einfach nur Markdown-Dateien, die fliegen. Klingt in der Theorie nach einem gemütlichen Samstagnachmittag mit einer Tasse Kaffee, oder? Tja, Pustekuchen. Es wurde eine epische Odyssee durch die Abgründe des Docker-Rechtemanagements.
Hier ist die ungeschminkte Geschichte, wie ich fast verzweifelt bin, warum andere CMS in der Docker-Visualisierung kläglich versagt haben und wie ich am Ende doch noch meinen Frieden mit Grav CMS gefunden habe.
Mein VPS läuft extrem strikt. Ich isoliere alles in Docker-Containern, die über ein gemeinsames webproxy-Netzwerk hinter meinem Nginx Proxy Manager (NPM) hängen. Da läuft meine Blazor-App, mein Forgejo für den Code und mein heiß geliebtes WikiJS.
WikiJS ist für mich als Wissensdatenbank der absolute Hammer. Es ist modern, sieht verdammt gut aus und läuft in Docker wie ein Uhrwerk. Aber für einen schnellen, öffentlichen Blog, bei dem ich einfach nur mal eben einen Artikel raushauen will, war es mir eine Spur zu wuchtig und "wiki-artig". Ich wollte ein echtes, schlankes CMS.
Weil ich absolut keine Lust habe, dass ein gehackter Container mein ganzes Host-System übernimmt, gilt bei mir die eiserne Regel: Jeder Dienst bekommt seinen eigenen Systemuser auf dem Host ohne Login-Rechte und mit einer strikten UID (User-ID). Keine Root-Rechte im Container, keine 777-Berechtigungen auf dem Server. Und genau diese Paranoia wurde mir bei den ersten Kandidaten zum Verhängnis.
(Wenn das Image schlauer sein will als der Admin) Mein erster Blick fiel auf Bludit. Schön minimalistisch, speichert alles in JSON. Ich legte also auf dem Host einen feinen bludit-user an und nagelte den Container mit user: "2001:2001" fest.
Das Problem: Das offizielle Bludit-Docker-Image ist eine Diva. Beim Starten läuft im Container ein internes Skript Amok, das versucht, via chown die Rechte im Web-Verzeichnis blind auf den internen Nginx-User umzubiegen. Da mein Container aber als kastrierter User ohne Root-Rechte startete, hieß es in den Logs nur: Operation not permitted. Ergebnis? Endlosschleife, Nginx stirbt, total 0 im Datenordner. Selbst der Trick, die originalen Flat-Files manuell via GitHub in den Ordner zu prügeln, endete in einem frustrierenden Berechtigungs-Dilemma. Bludit flog hochkant vom Server.
"Gut", dachte ich, "nimmst du halt das hochgelobte Statamic." Doch hier scheiterte ich schon an der Haustür: Das Community-Image, das ich im Auge hatte, wurde vom Entwickler offenbar kurz zuvor auf „privat“ gestellt oder gelöscht. pull access denied. Autsch.
Dann eben HTMLy. Ein ultra-minimalistischer Traum. Doch das offizielle Image basiert auf einem nackten PHP-FPM-Stack, der intern auf Port 9000 lauscht. Mein Nginx Proxy Manager quittierte den ersten Versuch prompt mit einem hässlichen 502 Bad Gateway. Nach der Port-Umstellung fing dann das gleiche Rechte-Drama an wie bei Bludit. Das Image wollte einfach nicht fressen, dass ich die Ordnerstruktur von außen kontrollieren wollte.
Ich baute das Setup zwar kurzzeitig auf ein robustes php:8.2-apache-Image um, was den Port-Fehler löschte, aber der bittere Beigeschmack blieb: Warum muss das bei Flat-File-Systemen so kompliziert sein?
Zwischendurch schmiss ich noch Typemill in den Ring (uberty/typemill-docker). Ein tolles System mit einem schicken Block-Editor. Nach dem Starten die Überraschung im Browser: Forbidden - You don't have permission to access this resource. Apache/2.4.38 Server.
Warte mal... Apache? Ich dachte, das Image läuft mit Nginx! Typemill hatte klammheimlich einen Apache-Webserver im Container verbaut, der sofort die Schotten dichtmachte, weil meine restriktiven Host-Rechte die interne .htaccess blockierten. Ich musste erst meinen kompletten Host-User auf die Webserver-Standard-ID 33 umprogrammieren, damit Apache überhaupt mit mir redete. Das war mir zu viel Bastelbude für ein eigentlich simples CMS.
Frustriert, müde und kurz davor, doch wieder WordPress zu installieren, stieß ich auf das Grav-Image der Jungs von Linuxserver.io (lscr.io/linuxserver/grav). Und Leute, ich sage euch: Das war, als würde man nach einer Fahrt im kaputten Trabant in einen Tesla steigen.
Die Entwickler von Linuxserver haben verstanden, wie Docker im echten Leben funktioniert. Sie nutzen die genialen Umgebungsvariablen PUID und PGID.
Mein Setup sah am Ende so einfach aus:
Ich legte einen sauberen User auf dem VPS an:
Bash
sudo useradd -u 2002 -g 2002 -m -s /bin/false grav-user
In der docker-compose.yml sagte ich Grav einfach: "Hey, du ziehst jetzt die Identität von User 2002 an!"
Container hochgefahren (docker compose up -d).
Und was passierte? Ein Wunder! Kein Operation not permitted. Kein 502 Bad Gateway. Ein Blick in mein Host-Verzeichnis /data/grav-data zeigte mir eine wunderschön strukturierte Ordnerlandschaft (www, nginx, pages), die ausnahmslos und von Geisterhand korrekt meinem grav-user gehörte.
Im Nginx Proxy Manager kurz die Subdomain auf Port 80 geschaltet, SSL-Zertifikat von Let's Encrypt gezogen – und BÄM! Der Grav-Installationsassistent strahlte mich an.
Mein Fazit Warum erzähle ich euch das? Viele Flat-File-CMS werben mit "Einfachheit". In der Docker-Realität schmeißen sie euch aber oft Knüppel zwischen die Beine, weil die Images unsauber gebaut sind und Root-Rechte erzwingen wollen.
Wenn ihr auf eurem VPS ein absolut sicheres, performantes Blog-System ohne Datenbank-Ballast sucht, das eure Host-User-Rechte respektiert, spart euch die Nerven, die ich verloren habe. Nehmt Grav CMS im Linuxserver-Image. Es läuft wie geschmiert, verbraucht im Leerlauf fast keinen RAM und der Markdown-Editor macht einfach nur Bock. Die aktuelle Version ist zwar schon etwas älter, aber es wird fleissig an der V2.0 gearbeitet.
Hier noch das docker-compose.yml
services:
myblog:
image: lscr.io/linuxserver/grav:latest
container_name: grav-cms
restart: unless-stopped
environment:
- PUID=2002 # ID des grav-user auf dem Host
- PGID=2002 # Gruppen-ID auf dem Host
- TZ=Europe/Vienna
volumes:
# Spiegelt alle Konfigurationen, Themes und Markdown-Inhalte auf den Host
- /data/grav-data:/config
networks:
- webproxy
networks:
webproxy:
external: true
Habt ihr ähnliche Erfahrungen mit Berechtigungen in Docker gemacht? Schreibt es mir unbedingt in die Kommentare! Bis zum nächsten Mal auf rolandbrunner.me!