Zum Inhalt springen
SebastianERB.
StartBlogRaceday
SebastianERB.

CTO by day. Athlete by dawn. Tech & Sports. Development, Cloud, AI, Sport-Gadgets, Running, Triathlon, Bikes, Crossfit und Wettkämpfe.

© 2026 Sebastian Erb · sbstnerb.de · Built with ❤️ · Drupal 11
DatenschutzImpressum
Meine
Home/Blog/Meine Website: Headless Drupal, vier Server, sechs Cache-Layer
Blog

Meine Website: Headless Drupal, vier Server, sechs Cache-Layer

und warum das keine Überreaktion ist

A
Admin
9. April 2026 · 14 Min
Sebastian Erb Karikatur (Mit ChatGPT erstellt)
Inhalt · 18 Abschnitte
  • Vorwort
  • Warum erzähle ich euch das?
  • Drupal. Keine Diskussion.
  • Im Dunkeln entwickeln
  • Hosting
  • Multi-Site
  • Performance
  • Suche
  • CI/CD
  • Testing
  • Monitoring
  • Sicherheit
  • Races
  • Branding mit KI
  • Migration
  • Die Downsides
  • Was noch fehlt?
  • Fazit

Vorwort

Ich bin bzw. war ca. fünfzehn Jahre Softwareentwickler & Solution Architect. Seit ein paar Jahren CTO bei einer fast neunzigköpfigen Digitalagentur. Ich entscheide über Technologiestacks für Kundenprojekte, baue Architekturkonzepte, berate und baue Teams... Naja, und kaufe gefühlt jedes zweite Gadget, das auf Kickstarter landet.

Wer mich von Instagram kennt, kennt mich hauptsächlich als Läufer. Marathons, Trailruns, Brooks Running Collective, Strava, Kudos, Triathlons, Gravelbiken, Hyrox, Crossfit, ... All das bin ich auch. Aber das ist nicht alles.

Mein alter Blog (und auch Social Media) hat den anderen Teil von mir komplett ausgeblendet. Und da mir eh gerade langweilig war und ich mal wieder Lust hatte, neue Technologien auszuprobieren, hat mich der Impuls getrieben, meinen alten Blog von Grund auf neu zu bauen. Und weil ich so bin, wie ich bin, war "neu bauen" natürlich nicht nur ein Theme-Wechsel.

Rausgekommen sind vier Server, vier Websites und ein System, das ich mit gutem Gewissen auch für andere betreibe. Drupal 11 als Headless-CMS, Next.js als Frontend, Docker auf Hetzner, Elasticsearch, Redis, sechs Cache-Layer, Gitflow mit GitLab CI/CD, automatisierte Tests, Monitoring mit Alarmen aufs Handy und ein Branding, das ich zusammen mit KI entwickelt habe.

Warum ich das gemacht habe, wie das System funktioniert und was dabei schiefgelaufen ist: darum geht es hier.

Warum erzähle ich euch das?

Auf Social Media bin ich der Läufer und Triathlet. Das stimmt ja auch: Ich laufe seit Jahren, viel und ernsthaft. Aber es ist eben nur ein Teil der Wahrheit.

Der andere Teil: Ich bin ein Tech-Nerd. Durch und durch. Nicht "Techniker, weil Job", sondern Techie, weil ich es liebe. Ich entwickle mir eigene Trainings-Tools, weil die existierenden nicht die Zahlen liefern, die ich haben will. Ich kaufe Gadgets, um zu verstehen, wie sie gebaut sind. Und ich denke über Systemarchitektur nach, bevor ich morgens meinen ersten Kaffee getrunken habe.

Dieser Blog hat sich bisher auf den Läufer konzentriert. Das ändert sich jetzt. Nicht weil ich etwas korrigieren will, sondern weil man älter wird, Interessen reifen und der Fokus sich verschiebt. Oder klarer wird.

Wenn du der Nerd unter den Läufern bist oder der Sportler unter den Nerds, dann wirst du hier künftig sicher den ein oder anderen lesenswerten Post finden.

Drupal. Keine Diskussion.

Tags
DrupalClaude CodeNext.jsDockernerdStravaHeadless CMSGraphQLElasticsearchRedisPostgreSQLTraefikHetznerGitLabCI/CDAnsibleGrafanaStorybookVitestPlaywrightRenovateCachingMonitoringKIWordPress

Ich arbeite im beruflichen Kontext seit über acht Jahren mit Drupal und habe hier eine große Expertise. Ich habe Contrib-Module geschrieben, war bei Community-Events als Organizer dabei, habe mitgeholfen, eine Drupal-Developer-Akademie aufzubauen, und war Teil des Drupal Business Deutschland e.V.

Ob ich andere Systeme evaluiert habe? Nein. Die Frage hat sich nicht gestellt.

Was Drupal für diesen Use-Case konkret mitbringt: ein flexibles Feldsystem, ein sauberes Content-Modell mit Paragraphs, Taxonomien, Config-Management über YAML und GraphQL über das Contrib-Modul graphql_compose. Ich weiß, wie Drupal reagiert, wenn man es an seine Grenzen drückt. Und ich weiß, wie man es nicht dahin drückt.

Das Frontend läuft in Next.js. Drupal hält die Daten, Next.js rendert sie. Dazwischen liegt GraphQL als API-Layer. Das Frontend weiß nichts von der Datenbank. Es fragt, bekommt strukturierte Daten zurück und baut daraus Seiten. Das macht das System flexibler und zwingt mich dazu, das Content-Modell sauber zu halten.

Im Dunkeln entwickeln

Ich mag ein Kellerkind sein, das meine ich damit aber nicht. (Tageslichtlampe sei Dank)

Ich meine: Den größten Teil dieses Projekts habe ich entwickelt, ohne zu sehen, wie das Ergebnis aussieht.

Das klingt absurder, als es war. Ich habe zuerst Drupal aufgebaut: das Content-Modell definiert, GraphQL konfiguriert, die Infrastruktur vorbereitet. Das Frontend kam danach. Lange Zeit gab es keine sichtbare Website. Nur Drupal, API-Responses in Postman und das Wissen, dass da irgendwann etwas rauskommt. Mit Storybook konnte ich die Frontend-Komponenten aber zum Glück schon vor der "Hochzeit" zwischen Backend und Frontend testen.

Irgendwann habe ich das Frontend dann zum ersten Mal mit Drupal verbunden. Auf der einen Seite YAML-Konfigurationen und GraphQL-Fragmente ohne sichtbares Ergebnis, auf der anderen Seite ein generisches Storybook... und dann: eine echte Seite, aus echten Drupal-Daten, im Browser. Das klingt technisch banal. War es nicht.

Ein Server pro Website

Gehostet wird bei Hetzner in Deutschland. Kein Kubernetes, kein Cloud-Auto-Scaling. Für meine Größenordnung ist das die richtige Entscheidung: fester monatlicher Betrag, volle Kontrolle, klare Übersicht, was läuft.

Jede Website hat ihre eigene VM. sbstnerb.de läuft auf der größten Maschine, die anderen Seiten auf kleineren Instanzen ohne Elasticsearch. Dazu kommt ein kleiner Ops-Server für die Alarme. Wenn eine Seite Mist baut, merken die anderen davon nichts. Production und Staging teilen sich pro Website trotzdem einen Server, mit getrennten Datenbanken, Redis-DBs und Suchindizes. Fällt der Server aus, ist beides weg. Das nehme ich in Kauf.

Von Hand klicke ich keinen Server zusammen. Ansible richtet sie ein: SSH-Härtung, Firewall, fail2ban, automatische Sicherheitsupdates, Backups. Reboots passieren nur, wenn ein Update sie braucht, in einem festen Wartungsfenster und mit Health-Check danach. Regelmäßig kommt ein Report, ob ein Server vom Soll-Zustand abweicht. Auch DNS ist Code: zwölf Domains bei Cloudflare, verwaltet mit OpenTofu, und ein Job prüft regelmäßig, ob jemand von Hand etwas geändert hat. (Spoiler: Wenn, dann war ich das.)

Architektur von sbstnerb.de: Browser, Traefik, Next.js-Frontend, Drupal-Backend, Redis, PostgreSQL und Elasticsearch in vier getrennten Docker-Netzwerken

Der Aufbau pro Website: Traefik vorne, Frontend und Drupal in der Mitte, Datenbank und Suche in Netzwerken, an die von außen niemand drankommt.

Auf jedem Server läuft Docker Compose. Traefik ist der Reverse Proxy: Er liest die Container-Labels, konfiguriert sich selbst und holt die TLS-Zertifikate über Let's Encrypt. Ich schreibe keine einzige nginx.conf für das Routing.

Das Netzwerk ist in vier isolierte Docker-Netzwerke aufgeteilt. PostgreSQL und Elasticsearch leben in einem Netzwerk mit internal: true, da kommt von außen nichts rein. Das Frontend darf mit Redis und Drupal sprechen, aber nicht mit der Datenbank. Klingt nach Overkill für eine persönliche Website. Sind ein paar Zeilen YAML und sorgen dafür, dass ich nachts ruhig schlafe.

Ein System, vier Websites

Die Trennung in einen generischen Core und ein projektspezifisches Theme war am Anfang mehr Aufwand. Das wollte ich aber so, weil ich das System auch für weitere Seiten nutzen wollte, die ich betreue.

Genau das ist passiert. Vier Websites hängen daran: meine eigene und drei Seiten für Freunde, Familie und Bekannte. Der Core (@se-frontend/core) enthält alles Technische: GraphQL-Client, Routing, Cache-Logik, Suche, SEO. Er wird als Package versioniert und von jedem Theme eingebunden. Das Theme enthält nur noch, was die Seite ausmacht: Komponenten, Design-Tokens, Queries.

Auf der Drupal-Seite das gleiche Spiel. Alles, was mehrere Seiten brauchen, liegt in eigenen Composer-Packages: GraphQL-Erweiterungen, Cache-Revalidation, SEO, Lesezeit, KI-Teaser, Migration, Content-Audit. Dreizehn Module sind es. Zusammen mit Backends, Themes, Infrastruktur, DNS und Monitoring komme ich auf 27 GitLab-Repos. Aus einer Website ist eine kleine Plattform geworden.

Performance: sechs Cache-Layer

Das Problem bei Headless-Setups ohne Caching ist simpel: Wenn jemand eine Seite aufruft, fragt das Frontend Drupal per GraphQL, und das dauert. Wenn gleichzeitig fünfzig Leute die gleiche Seite aufrufen, fragt das Frontend fünfzigmal. Das skaliert nicht und macht langsam.

Die Lösung ist Caching auf mehreren Ebenen, weil jede Ebene ein anderes Problem löst. Aktuell sind es sechs.

Die sechs Cache-Layer von sbstnerb.de vom Browser bis zur Datenbank

Sechs Ebenen, ein Ziel: Drupal so selten wie möglich fragen müssen.

Die Cache-Layer im Detail

Drupal nutzt Redis als Cache-Backend für nahezu alles: Entity-Cache, Config-Cache, Render-Cache, Container-Cache. Ohne Redis würde Drupal bei jedem Request die Datenbank fragen. Mit Redis fragt es Redis. Das ist erheblich schneller.

Ausgeliefert wird direkt aus Deutschland. Für meine Zugriffszahlen reicht das locker.

Und wie schnell ist das jetzt? Sehr schnell. Die Pipeline misst nach jedem Production-Deploy automatisch mit Lighthouse nach, und die Werte sind sehr gut.

Die Schriften liegen übrigens auf meinem eigenen Server, nicht bei Google. Geladen werden nur die Schnitte, die auch wirklich genutzt werden.

Suche mit Elasticsearch

Ich hätte die Suche als einfache Datenbanksuche bauen können. Das wäre schneller gegangen.

Volltextsuche mit Relevanz-Scoring, Vorschlägen beim Tippen und Toleranz für Tippfehler bekommt man so aber nicht. Elasticsearch läuft auf dem gleichen Server, kostet nichts extra, und die Daten verlassen mein System nicht. Es hängt im internen Netzwerk, von außen nicht erreichbar, nur Drupal darf es fragen.

Seit dem Frühjahr ist die Suche deutlich erwachsener geworden. Synonyme pflege ich jetzt redaktionell in Drupal, ohne Deploy. Wer "HM" sucht, findet Halbmarathons. Bei Tippfehlern gibt es ein "Meinten Sie...?". Und mit ⌘K bzw. Strg+K geht die Suche als Overlay über jeder Seite auf.

Warum nicht Algolia, Solr oder Typesense? Hätte alles funktioniert. Elastic nutze ich auch im Job häufig. So kann ich hier noch mehr Expertise aufbauen.

Vom Commit bis live

Bei vier Websites reicht ein Pipeline-Job und ein gutes Gefühl nicht. Deshalb gilt Gitflow, auch für mich alleine. Jede Änderung startet als Feature-Branch und geht per Merge-Request nach develop. Ein Bot macht jeden Tag einen Release-Merge-Request auf. Der Release-Branch landet automatisch auf Staging, wo ich mir alles in Ruhe ansehe. Production gibt es nur über einen Tag wie prod-2026-10-16-1 und einen Klick auf "Play". Nichts geht ungewollt live.

Deployt werden fertige Docker-Images, keine Code-Updates auf dem Server. Jedes Image ist eindeutig getaggt, ein Rollback ist das vorherige Image. Für Drupal gibt es einen eigenen Job, der das aktuelle Release installiert, dann mit dem neuen Code die Datenbank-Updates laufen lässt und prüft, ob die Konfiguration danach sauber ist. So fallen kaputte Update-Hooks auf, bevor sie Production treffen.

Weg einer Änderung vom Feature-Branch über develop, Release und Staging bis zum Production-Tag, mit Tests, Scans und Monitoring

Der Weg einer Änderung: vom Feature-Branch bis Production, mit Tests, Scans und Monitoring drumherum.

Um Updates kümmert sich Renovate. Jeden Tag schaut der Bot nach neuen Versionen von npm- und Composer-Paketen und Docker-Images und macht dafür Merge-Requests auf. Patch-Updates werden automatisch gemerged, wenn die Pipeline grün ist, alles Größere schaue ich mir an. Dazu kommen tägliche composer audit und npm audit gegen bekannte Sicherheitslücken und ein Trivy-Scan der Docker-Images. Ehrlicherweise sind das die Merge-Requests, die am meisten nerven. Und die, die am wichtigsten sind.

Testen, was kaputtgehen kann

Ein System, das vier Websites trägt, kann ich nicht nach Gefühl ändern. Also gibt es Tests. Nicht für die Statistik, sondern weil ich jedes Mal ruhiger schlafe, wenn sie grün sind.

  • Vitest: rund 750 Unit-Tests für Core und Themes, von Routing über Cache-Logik bis zur Bildauswahl. Im Core laufen sie zusammen mit dem Linter bei jedem Push.
  • Storybook: über 150 Stories, jede Komponente lässt sich isoliert ansehen und testen, bevor sie auf einer echten Seite landet.
  • PHPUnit: Tests für die Drupal-Module, unter anderem für GraphQL, Cache-Revalidation und SEO.
  • Quality-Check nach jedem Deploy: Lighthouse und axe laufen per Playwright gegen die echte Production-Seite, im Dark Mode, mehrere Durchläufe, mit Budgets für Performance, SEO, Barrierefreiheit, Layout-Shifts und Darstellung auf 320 Pixel Breite.

Der Quality-Check meldet im Moment nur und blockiert noch nichts. Er hat aber schon Kontrastfehler und falsche Labels gefunden, die mir sonst nie aufgefallen wären.

Und weil ich hier ehrlich sein will: Die Tests der Themes existieren zwar, laufen aber noch nicht in deren Pipeline, sondern nur lokal bei mir. Daran arbeite ich gerade.

Monitoring: Mein Handy weiß es zuerst

Bei einer Website für mich alleine hätte ich vielleicht noch gesagt: Merke ich schon, wenn sie down ist. Bei Seiten, die ich für andere betreibe, reicht das nicht.

Auf jedem Server läuft Grafana Alloy und schickt Metriken an Grafana Cloud. Dashboards und Alarme sind, wie alles andere, als Code im Repo. Alarm gibt es, wenn ein Server nicht erreichbar ist, Platte oder RAM knapp werden, ein Systemdienst abstürzt, ein Backup fehlt oder fehlschlägt oder ein Zertifikat bald abläuft. Zusätzlich rufen synthetische Checks in kurzen Abständen von mehreren Standorten aus jede Website auf und prüfen Antwort und Zertifikat.

Die Benachrichtigung kommt per Push aufs Handy, über einen eigenen ntfy-Server, und per Mail. Ich erfahre also von einem Problem, bevor es mir jemand schreibt.

Sicherheit

Eine persönliche Website ist kein lohnendes Ziel? Die Bots sehen das anders. Jeden Tag landen Anfragen nach wp-login.php und Co. auf dem Server, bei einer Seite, die gar kein WordPress mehr ist.

Was läuft: SSH nur mit Schlüssel, Firewall, fail2ban, automatische Sicherheitsupdates. Traefik setzt Security-Header wie HSTS, Referrer-Policy und Permissions-Policy. Staging und Storybook sind per Passwort geschützt, Storybook zusätzlich nur für bestimmte IPs erreichbar. Die GraphQL-API hat Limits für Tiefe und Komplexität von Queries, damit niemand Drupal mit einer einzigen verschachtelten Anfrage beschäftigt.

Dazu kommt eine Content-Security-Policy, die ich Schritt für Schritt weiter verschärfe. Jede Verschärfung prüfe ich vorher, damit sie nichts kaputtmacht.

Races als eigener Content-Typ

Ein Teil, auf den ich mich als Läufer besonders gefreut habe: Wettkämpfe sind kein Blogpost mit Ergebnis im Fließtext, sondern ein eigener Content-Typ mit über 30 Feldern. Zeit, Platzierungen gesamt und in der Altersklasse, Splits pro Disziplin, Link zur Ergebnisliste, Urkunde, Strava-Aktivität, Status wie Finisher, DNF oder DSQ.

Weil die Daten strukturiert sind, kann das Frontend damit rechnen. Die Race-Übersicht zeigt automatisch meine Bestzeiten pro Distanz, filterbar nach Kategorie. Und mit dem Haken "Nicht für Bestzeiten werten" kann ich auch die Läufe eintragen, bei denen ich nur mitgelaufen bin, ohne dass sie mir die Statistik verhageln.

Auch die alten Rennberichte aus WordPress-Zeiten sind mit Ergebnislisten, Strava-Daten und Fotos ergänzt. Da steckte mehr drin, als ich gedacht hätte.

Drupal Content Type "Wettkampf"

Vom Content-Typ in Drupal zur fertigen Race-Seite.

Branding mit KI

Das ist der Teil, bei dem ich am wenigsten erwartet hätte, dass er so gut funktioniert.

Ich bin Techniker. Ich baue Systeme. Ich bezeichne mich nicht als Designer und hätte das auch vor diesem Projekt nicht getan.

Für das Branding habe ich mit Claude gearbeitet. Nicht als Tool, das mir auf Knopfdruck ein fertiges Design hinwirft, sondern als Sparring-Partner. Ich hatte eine konkrete Ausgangsbasis: ein Referenzdokument mit Design-Tokens, Typografie, Farbpaletten für Dark und Light Mode, Abstands-Rhythmus, alles.

Daraus haben wir iteriert. Version 1 war ein roher Mockup: Die Grundstruktur stimmte, aber es fühlte sich nicht nach mir an. Version 2 hatte das Branding, das ich wollte. Barlow Condensed für Headlines, Space Grotesk für UI-Texte, Inter für Fließtext, JetBrains Mono für Code. Und als Signaturfarbe: #B8FF35, Lime Green auf dunklem Hintergrund.

Das Entscheidende dabei war nicht, dass die KI einen guten Geschmack hat, sondern dass ich schnell korrigieren konnte, was nicht stimmte. Wer schon mal einen Designprozess über E-Mails und Feedback-Schleifen abgewickelt hat, versteht, warum das einen Unterschied macht.

Die WordPress-Migration

Ich wollte die alten Inhalte nicht verlieren. Also habe ich eine Migration gebaut.

Drupal hat ein eigenes Migrate-Framework, das genau dafür gedacht ist. Mein Modul secms_migration liest direkt aus der WordPress-Datenbank, mappt Posts auf Drupal-Nodes, Kategorien auf Taxonomy-Terms und migriert die Bilder mit. Die Migration war beliebig oft wiederholbar. Ich konnte WordPress so lange live lassen, bis in Drupal alles gepasst hat, und erst dann den DNS-Switch setzen.

Kein harter Stichtag, an dem plötzlich die Hälfte fehlt. Kontrollierter Übergang.

Die Downsides

Der Aufwand für eine persönliche Website ist objektiv übertrieben. Wenn ein Kunde zu mir käme und "einfach einen Blog" wollte, würde ich ihm das nicht vorschlagen. Aber ich bin CTO, und manchmal baut man Dinge einfach, weil man sie bauen will. Das ist kein Fehler.

Ehrlicherweise gehört aber noch mehr dazu:

  • Kein Failover. Ein Server pro Website heißt auch: Fällt er aus, ist die Seite weg, bis er wieder läuft.
  • Wartung hört nie auf. Renovate, Audits und Scans finden jede Woche etwas. Ein System dieser Größe ist kein Projekt, das irgendwann fertig ist.

Was noch fehlt?

Fertig ist so ein System nie. Auf meiner Liste stehen gerade:

  • Die Backup-Strategie weiter ausbauen.
  • Die Theme-Tests in die Pipeline holen.
  • Die Content-Security-Policy weiter verschärfen.
  • Strava richtig anbinden, sodass die Daten automatisch kommen und ich die Aktivitäten nicht mehr von Hand verknüpfe.
  • Mein KI-Modul, das heute auf Knopfdruck Teaser-Texte schreibt, für weitere Use-Cases ausbauen.

Fazit

Ich habe eine Website gebaut, die ich in zwei Jahren noch verstehen werde. Das klingt wie eine niedrige Messlatte. Ist es nicht. Die meisten Systeme, die ich kenne, sind nach zwei Jahren so gewachsen, dass nur noch der ursprüngliche Entwickler durchblickt.

Die Architektur ist bewusst simpel, wo es geht: ein Server pro Seite, Docker Compose, klare Grenzen zwischen den Schichten. Und bewusst aufwendig, wo es sein muss: sechs Cache-Layer, Netzwerktrennung, Tests, Monitoring, Deploys nur über Tags.

Was ich eigentlich bauen wollte: ein technisch solides System, das ich komplett selbst verstehe, das mir als Referenz für Kundenprojekte dient und das mein öffentliches Bild um den Teil ergänzt, der bisher gefehlt hat. Was rausgekommen ist: eine kleine Plattform, auf der auch andere Websites laufen.

Das ist mehr, als ich geplant hatte.

Wenn dich die Details interessieren oder du Fragen hast: Schreib mir. Ich rede gerne über Technik. Fast so gerne, wie ich laufe.

LinkedIn