Ein Server, vier Umgebungen, kein Cookie-Banner — die Infrastruktur hinter einem Solo-App-Projekt

Die Frage nach dem Cookie-Banner stellt sich bei den meisten Web-Anwendungen zu spät. Die Anwendung steht, ein Analyse-Skript ist eingebaut, die Login-Seite kommt von einem Cloud-Dienst, und plötzlich braucht der erste Seitenaufruf einen Einwilligungs-Dialog, den niemand geplant hat.

Im DI²-Projekt wurde die Frage umgedreht. DSGVO ohne Cookie-Banner war ein Designziel vom ersten Tag an, und die Infrastruktur wurde so gebaut, dass die Anwendung keine Einwilligung braucht. Sie läuft auf einem Server in der EU, auf dem vier Umgebungen nebeneinander leben. Die Anmeldung übernimmt ein selbst betriebener Identity Provider, davor steht ein Reverse Proxy, und im Browser landen ausschließlich Cookies, ohne die der Dienst nicht funktionieren würde. Dieser Artikel beschreibt diesen Aufbau, die Entscheidungen dahinter und den Preis, den ein Datenbank-Entwickler ohne Betriebs-Hintergrund dafür bezahlt hat. Im Versprechen, dass KI-gestützte Entwicklung alles einfach macht, kommt dieser Teil nicht vor. Der Agent hat die Skripte geschrieben. Die Entscheidungen, der Betrieb, die Verantwortung und die Monatsrechnung sind beim Maintainer geblieben.

Das Wichtigste vorab:

  • Das Designziel kommt zuerst: Keine Einwilligung für Cookies heißt keine Tracker, keine Drittanbieter-Skripte im Browser, ein eigener Identity Provider statt eines gehosteten und ein Server in der EU. Das ist eine Architektur-Entscheidung am Anfang, keine Rechtsfrage am Ende.
  • Ein VPS, vier Umgebungen: Entwicklung, Integration, Test und Produktion laufen auf einer Maschine, jede mit eigenem Checkout, eigener Datenbank, eigenem Realm und eigenem lokalen Port. Öffentlich erreichbar ist nur der Proxy.
  • Identity Provider selbst betreiben: Keycloak liefert pro Umgebung einen Realm. Ein idempotentes Skript von 477 Zeilen legt ihn an, ein zweiter Lauf ändert nichts. Der Maintainer hat davon keine Zeile geschrieben.
  • Der Rand ist Handarbeit: nginx terminiert TLS und routet per Hostname, der bisherige Hoster hält nur DNS und Postfächer, der VPS-Anbieter sperrt ausgehende Mail-Ports, und etwa monatlich kommt ein Update-Lauf über sieben Schichten vom Kernel bis zu den npm-Abhängigkeiten.
  • Vier Cookies über den Login hinaus, kein Tracking: zwei für die Anmeldung, zwei für ausdrücklich gewählte Präferenzen. Dazu kommen kurzlebige Hilfs-Cookies, die nur während des Anmelde-Umwegs leben. Die Datenschutzerklärung benennt sie einzeln und ordnet sie als unbedingt erforderlich im Sinne von § 25 Abs. 2 Nr. 2 TDDDG ein.
  • Der Preis ist Betrieb und Geld: 8 GB RAM für alles, Secrets in zwei Speicherorten, Doku, die hinter der Realität herläuft, und eine Monatsrechnung aus Server und KI-Abo. Das erste trägt sonst ein Cloud-Anbieter, das zweite steht in keinem Vibe-Coding-Versprechen.

Voraussetzung: Der Fall ist eine Next.js-Anwendung mit Auth.js und Keycloak auf einem Hetzner-Cloud-Server. Betriebs-Vorwissen ist nicht nötig, Begriffe wie Reverse Proxy, Realm und OIDC werden beim ersten Auftreten kurz erklärt. Eine Rechtsberatung ist der Artikel nicht.

Inhalt

Das Designziel zuerst: keine Einwilligungspflicht

Ein Cookie-Banner ist kein Bauteil, das man am Ende dazustellt. Er ist die Folge von Entscheidungen, die vorher gefallen sind. Ein Web-Analytics-Dienst setzt Cookies, also braucht er eine Einwilligung. Eine Login-Seite, die ein Cloud-Anbieter hostet, setzt ihre Cookies unter seiner Domain, und welche das sind, entscheidet der Anbieter. Ein eingebettetes Video lädt Skripte eines Drittanbieters, bevor der Besucher etwas angeklickt hat. Jede dieser Entscheidungen zieht den Dialog nach sich, und jede lässt sich später nur mit Aufwand zurücknehmen.

Im DI²-Projekt, einem ETL-Generator auf Next.js und PostgreSQL, hat der Maintainer die Reihenfolge deshalb umgekehrt. Das Ziel „keine Einwilligungspflicht“ stand fest, bevor die erste Komponente ausgewählt wurde, und es hat vier Entscheidungen erzwungen. Die erste betrifft den Browser: Dort laufen keine Tracker und keine Analyse-Dienste. Die zweite betrifft die Seiten selbst: Sie laden keine Skripte von Drittanbietern. Die dritte betrifft die Anmeldung: Sie läuft über einen selbst betriebenen Identity Provider, damit die Login-Seite und die Cookies, die sie setzt, unter eigener Kontrolle bleiben. Und die vierte betrifft den Standort: Der Server steht bei einem deutschen Anbieter in einem Rechenzentrum in der EU. Für diesen Baustein, das Hosting von Anwendung, Datenbank und Anmeldung, entfällt damit die Übermittlung personenbezogener Daten an einen US-Cloud-Anbieter. Andere Datenflüsse, etwa zu einem Mail-Relay oder zu Diensten, die ein späteres Feature einbindet, sind davon nicht abgedeckt und jeweils eigens zu prüfen.

Ein Wort zur Reichweite dieses Ziels. „Ohne Cookie-Banner“ heißt, dass die Anwendung für ihre Cookies keine Einwilligung braucht. Diese Pflicht steht nicht in der DSGVO, sondern in § 25 TDDDG, das den Zugriff auf das Endgerät regelt, und zwar unabhängig von der Technik: Auch der Local Storage des Browsers fällt darunter, nicht nur das Cookie. Die DSGVO kommt danach ins Spiel, für die Verarbeitung der personenbezogenen Daten. Kein Banner heißt also nicht, dass die DSGVO erledigt wäre. Die Verarbeitung der Daten hinter dem Login braucht weiterhin ihre Rechtsgrundlage, eine Datenschutzerklärung und, wo ein Dienstleister beteiligt ist, einen Vertrag mit ihm. Der Banner ist nur das sichtbarste Symptom, und genau deshalb taugt er als Designziel: Wer ihn vermeiden will, muss die Entscheidungen dahinter früh treffen.

Der Rest dieses Artikels ist die Konsequenz aus diesen vier Entscheidungen. Sie sind der Grund, warum die Anwendung auf einem einzelnen VPS läuft, warum Keycloak selbst betrieben wird und warum der Rand aus Proxy, DNS und Mail so viel Handarbeit war.

Ein VPS, vier Umgebungen

Die gesamte Anwendung läuft auf einem einzelnen virtuellen Server bei Hetzner. Auf dieser Maschine leben vier Umgebungen: Entwicklung, Integration, Test und Produktion. Jede Umgebung hat einen eigenen Checkout des Repositories, eine eigene Datenbank, einen eigenen Realm im Identity Provider und einen eigenen Container, der auf einem eigenen lokalen Port lauscht. Die vier Datenbanken der Anwendung liegen in einer gemeinsamen PostgreSQL-Instanz, die selbst als Container läuft. Keycloak hat seit dem Umzug im April 2026 eine eigene Instanz daneben. Von außen erreichbar ist keiner dieser Container. Öffentlich ist nur der Reverse Proxy, der die Anfragen nach Hostname verteilt.

Stark vereinfacht und mit Stand vom September 2026 sieht der Aufbau so aus:

Architektur-Diagramm: links Browser und DNS bei All-Inkl, rechts der Hetzner-VPS mit nginx als einziger öffentlicher Komponente, darunter vier App-Container dev, int, test und prod auf einer PostgreSQL-Instanz mit vier Datenbanken, daneben Keycloak mit eigener Datenbank, unten GitHub Actions per SSH und ein SMTP-Relay auf Port 587.

Die Umgebungen sind Stufen, keine Kopien. Der Code wandert von Hand von einer Stufe zur nächsten. Entwicklung, Integration und Test bauen aus demselben Entwicklungs-Branch, die Produktion baut aus dem Haupt-Branch. Jede Beförderung ist ein manuell ausgelöster GitHub-Actions-Workflow. Er verbindet sich per SSH mit dem Server, setzt den Checkout auf den gewünschten Stand und baut den Container neu. Wie der Lebenszyklus von der Objekt-Datei bis zum Deploy insgesamt aussieht, beschreibt der Hub-Artikel Datenbank-CI/CD mit PostgreSQL. Das Qualitäts-Gate mit einer Wegwerf-Datenbank vor jedem Datenbank-Deploy zeigt GitHub Actions für Postgres-Deploys, und die Verzeichnis-Konvention der Datenbank-Schicht erklärt SQL-Schema deployen ohne Migrations-Tool. Dieser Artikel wiederholt davon nichts.

Ein einzelner Server erzwingt drei Dinge, die bei vier Servern von allein gelöst wären. Erstens verlangt er Port-Disziplin. Jede Umgebung braucht ihren eigenen lokalen Port, und wenn die Konfigurationsdatei einer Umgebung fehlt, fällt ihr Container auf den Standard-Port zurück, den schon eine andere belegt. Zweitens teilen sich alle Dienste ein einziges RAM-Budget, auf das die Kosten-Sektion zurückkommt. Drittens verlangt er eine saubere Trennung der Ebenen von Anfang an, weil jeder nachträgliche Umzug auf dem Live-Server stattfindet. Genau so ein Umzug ist im April 2026 passiert. Die Verzeichnisse wanderten in ein Layout, das mehr als eine App auf demselben Server erlaubt. Die bis dahin geteilte PostgreSQL-Instanz wurde in eine App-Datenbank und eine Keycloak-Datenbank getrennt, und Keycloak bekam einen eigenen Infrastruktur-Ort. Erledigt hat das ein Skript von 412 Zeilen in acht Phasen. Es fragt vor jedem destruktiven Schritt nach und legt in der zweiten Phase Backups an.

Das Skript stammt aus einer Agenten-Session. Der Auftrag dazu, rekonstruiert aus der Feature-Beschreibung, sah in etwa so aus:

Der Server soll später mehr als eine App tragen. Dafür ziehen die vier
App-Verzeichnisse in ein Layout mit App-Ebene um, die gemeinsam genutzte
Postgres-Instanz wird in App-Datenbank und Keycloak-Datenbank getrennt,
und Keycloak bekommt einen eigenen Infrastruktur-Ort.
Das läuft einmalig auf dem Live-Server, alle vier Umgebungen sind betroffen.
Vorgaben: Backup vor dem ersten destruktiven Schritt, jede Phase fragt
vorher nach und lässt sich einzeln überspringen, am Ende antwortet jede
Umgebung wieder mit ihrer Versions-Auskunft.
Dokumentiere den Rollback-Weg, bevor irgendetwas läuft.

Der Prompt beschreibt das Ziel, die Rahmenbedingungen und das Abnahme-Kriterium. Die Aufteilung in acht Phasen, die Backup-Logik und die Rückfrage vor jedem Schritt waren Vorschläge des Agenten. Der Maintainer hat sie Phase für Phase am Live-Server freigegeben.

Identität selbst betreiben: Keycloak

Die erste Fassung der Anwendung im März 2026 meldete Benutzer mit E-Mail und Passwort direkt in der App an. Ab April 2026 übernahm Keycloak diese Aufgabe als einziger Identity Provider. Die App selbst kennt seither keine Passwörter mehr. Sie leitet den Benutzer per OpenID Connect zu Keycloak weiter. Nach der Anmeldung dort kommt der Browser mit einem einmaligen Code zurück, den die Anwendung serverseitig gegen die Tokens von Keycloak eintauscht. Aus dem Ergebnis baut Auth.js die eigene Sitzung der Anwendung.

Zwei Begriffe braucht man dafür. OpenID Connect, kurz OIDC, ist das Protokoll, über das eine Anwendung die Anmeldung an einen Identity Provider delegiert und von ihm eine beglaubigte Identität zurückbekommt. Ein Realm ist in Keycloak der Behälter, der eine Menge von Benutzern, Anmeldedaten, Rollen und Gruppen verwaltet. So definiert es die Keycloak-Dokumentation. Im DI²-Projekt gibt es einen Realm je Umgebung. Ein Testbenutzer in der Integrations-Umgebung existiert in der Produktion nicht, und eine Rollen-Änderung im Test-Realm berührt die Produktion nicht.

Vier Realms mit identischem Aufbau von Hand zu klicken wäre die naheliegende Lösung gewesen und der sichere Weg in die Drift. Stattdessen legt ein Bash-Skript von 477 Zeilen jeden Realm über die Admin-API von Keycloak an. Es erzeugt pro Realm einen OIDC-Client für die App, mit Authorization Code Flow und PKCE. Es legt die vier Realm-Rollen an, schaltet den Brute-Force-Schutz ein und macht die E-Mail-Adresse zum Benutzernamen. Es aktiviert den Link zum Zurücksetzen des Passworts, hängt das eigene Login- und E-Mail-Theme ein und setzt Deutsch als Standardsprache, mit Englisch als zweiter. Das Skript ist idempotent. Ein zweiter Lauf auf einem fertigen Realm ändert nichts und meldet für jedes Objekt, dass es bereits vorhanden ist und übersprungen wird. Nach jeder Änderung am Skript laufen alle vier Realms in einer Schleife durch, und nur die neuen Einstellungen kommen dazu.

Auch dieser Auftrag ist rekonstruiert, aus der Feature-Beschreibung und dem Kopf des Skripts:

Keycloak läuft im Container hinter nginx, der Admin-Zugang steht.
Ich brauche pro Umgebung (dev, int, test, prod) einen eigenen Realm mit
demselben Aufbau: ein OIDC-Client für die App mit Authorization Code und
PKCE, die Rollen Administrator und Benutzer, E-Mail als Benutzername,
Passwort-Reset erlaubt, unser Login- und E-Mail-Theme, Deutsch als
Standardsprache und Englisch zusätzlich.
Das Ganze als Skript gegen die Admin-API, nicht als Klick-Anleitung.
Abnahme: Ein zweiter Lauf auf einem fertigen Realm ändert nichts und
meldet pro Objekt, dass es übersprungen wurde.
Schlag mir vor, womit du das umsetzt, und begründe es.

Der Maintainer beherrscht weder Bash in dieser Tiefe noch die Admin-API von Keycloak. Der Prompt legt deshalb nur fest, was am Ende stehen muss, und delegiert das Wie ausdrücklich. Bash mit curl und jq war der Vorschlag des Agenten. Das Abnahme-Kriterium des zweiten Laufs war die Vorgabe des Maintainers, und es ist der Teil, der das Skript von einer Klick-Anleitung unterscheidet.

Zwei Migrationen kamen dazu, beide auf laufenden Realms. Die erste war der Wechsel vom Passwort-Login in der App zum externen Identity Provider, nach dem sich jeder Benutzer einmal neu anmelden musste. Die zweite war der Umbau vom Zwei-Rollen-Modell mit Administrator und Benutzer auf ein Vier-Rollen-Modell am 22. April 2026. Das Migrations-Skript hebt bestehende Administratoren auf die höchste Rolle und setzt alle bisherigen Benutzer zunächst auf die niedrigste. Die alten Rollen entfernt es erst, wenn niemand sie mehr trägt, und es warnt, wenn ein Realm danach weniger als zwei aktive Administratoren hat. Wer mehr als Leserechte braucht, wird anschließend von Hand hochgestuft.

Drei Dinge hat der Maintainer an dieser Sektion selbst entschieden. Es gibt einen Realm je Umgebung statt eines gemeinsamen. Der Identity Provider wird selbst betrieben statt als Cloud-Dienst bezogen. Und jede Änderung an einem Realm geht durch das Skript, nicht durch die Admin-Oberfläche. Alles andere, vom Skript bis zum Theme, stammt aus Agenten-Sessions und wurde per Login-Test abgenommen.

Reverse Proxy, DNS und Mail: der Rand der Infrastruktur

Ein Reverse Proxy ist ein Webserver, der Anfragen aus dem Internet entgegennimmt und intern an den passenden Dienst weiterreicht. Im DI²-Projekt übernimmt nginx diese Rolle, und er ist die einzige Komponente, die von außen erreichbar ist. Er terminiert TLS mit Zertifikaten von Let’s Encrypt. Jede Subdomain hat ihr eigenes Zertifikat, die vier App-Umgebungen ebenso wie der Identity Provider. Die Erneuerung läuft über einen Cron-Job. Dass dieser Weg tatsächlich funktioniert, hat sich im Juli 2026 gezeigt, als drei Zertifikate ohne Zutun verlängert wurden.

Die Verteilung geschieht per Hostname. Jede Umgebung hat eine eigene Subdomain, alle zeigen auf dieselbe Server-IP, und nginx entscheidet anhand des angefragten Namens, an welchen lokalen Port die Anfrage geht. Zwei Details sind dabei mehr als Routing. Die Container lauschen nur auf localhost, das ist die eigentliche Abschottung. nginx setzt zusätzlich bei jeder weitergereichten Anfrage einen geheimen Header, und die App verwirft Anfragen ohne diesen Header. Das ist eine zweite Schicht für den Fall, dass die erste einmal nicht greift. Und der Rückweg des Logins, also die Adresse, auf die der Browser nach der Anmeldung bei Keycloak zurückgeleitet wird, ist auf zehn Versuche pro Minute und IP-Adresse gedrosselt. Das ist eine zweite Schutzschicht neben dem Brute-Force-Schutz im Realm.

All-Inkl, der Hoster dieses Blogs, spielt in diesem Aufbau nur zwei Nebenrollen. Er verwaltet die DNS-Einträge, also die fünf A-Records, die auf den Server zeigen, und er stellt die Postfächer bereit. Alles andere, auch das Hosting der Anwendung, findet dort nicht statt.

Die Mail-Rolle brachte die erste echte Falle des Projekts. Keycloak verschickt Mails zum Zurücksetzen von Passwörtern, und die naheliegende Lösung war, dafür den SMTP-Server des bisherigen Hosters zu nutzen. Der erste Versand am 20. April 2026 lief in einen Timeout. Es gab keine Fehlermeldung und keine abgelehnte Verbindung, die Pakete verschwanden einfach. Der Grund liegt beim Anbieter. Hetzner sperrt auf neuen Cloud-Servern die ausgehenden Ports 25 und 465 standardmäßig, um Spam-Versand zu verhindern. So beschreibt es die Cloud-FAQ von Hetzner mit Stand vom 7. September 2026. Dass sich die Sperre als Timeout und nicht als abgelehnte Verbindung zeigt, ist die Beobachtung aus dem Projekt, keine Aussage der Dokumentation. Die Freischaltung lässt sich in der Cloud-Konsole beantragen, allerdings erst nach der ersten bezahlten Rechnung und mindestens einem Monat als Kunde. Der andere Weg ist ein Mail-Relay, das auf Port 587 oder 2525 angesprochen wird. Wer die Sperre nicht kennt, sucht den Fehler zuerst in Keycloak, dann in den Zugangsdaten und zuletzt beim Anbieter.

Updates jeden Monat: Betriebssystem, Docker, PostgreSQL, Keycloak

Ein Server, den man selbst betreibt, hat keinen Anbieter, der ihn nachts patcht. Im DI²-Projekt läuft deshalb seit Mai 2026 etwa monatlich ein Update-Check, vier Läufe sind dokumentiert: am 2. Mai, am 30. Mai, am 19. Juni und am 3. August. Jeder Lauf prüft zwei Seiten. Auf der Entwicklungs-Seite prüft er die Abhängigkeiten der Anwendung, die deklarierten Container-Versionen und die Versionen der GitHub-Actions. Auf der Server-Seite prüft er, was dort tatsächlich läuft, vom Kernel bis zu den Container-Images. Die Tabelle zeigt, welche Schichten dabei anfallen, was im Projekt konkret passiert ist, und wer die Schicht bei einer verwalteten Alternative aus Vercel und gehosteter Datenbank übernehmen würde. Die Zuordnung gilt für genau diese Kombination, bei anderen Diensten verschiebt sie sich:

SchichtBeispiel aus dem Projekt, 2026Bei Vercel und gehosteter Datenbank
Betriebssystem und KernelBatches von 14, 8 und 22 Paketen mit Reboot, im Mai, Juni und AugustAnbieter
Docker-Engine, nginx, Certbotim Projekt kommen Docker und nginx über die Paketverwaltung des Betriebssystems, Certbot als Snap mit Auto-Updateentfällt, es gibt keinen eigenen Server
Container-ImagesPostgres 17.9 auf 17.10 mit elf Security-Fixes laut Release-Ankündigung, ein Keycloak-Patch, ein kritischer Patch der MySQL-Beispiel-DatenbankDatenbank-Patches beim Anbieter, Major-Upgrades beim Kunden, ein selbst betriebener Identity Provider ebenfalls
App-AbhängigkeitenNode 20 auf 24 im Mai, sechs npm-Findings im August, fünf davon hoch, mit einem Update-Lauf geschlossenEntwickler, in beiden Modellen
GitHub ActionsCheckout- und Setup-Actions im Mai auf Version 5 gehobenEntwickler
ZertifikateErneuerung per Cron, im Juli 2026 ohne ZutunAnbieter
SecretsRotation über fünf Klassen mal vier Umgebungen nach einem Advisory des Next.js-Herstellers im MaiEntwickler

Die Mechanik ist dieselbe Arbeitsteilung wie beim Rest der Infrastruktur. Ein Agenten-Skill liest die deklarierten Versionen aus den Projektdateien, nie aus dem Gedächtnis, und vergleicht sie mit dem Upstream-Stand. Auf dem Server läuft ein Skript, das nur liest und nichts verändert, und seine Ausgabe gibt der Maintainer an die Session zurück. Der Agent sortiert die Befunde nach kritisch, empfohlen, klein und groß. Angewendet wird von Hand, und jeder Lauf hinterlässt eine Zeile in der Historie des Berichts. Dazu kommt ein Security-Audit als eigener Skill. Er läuft quartalsweise und vor jedem Produktions-Deploy, zuletzt am 5. September 2026 als vollständiger Audit über zehn Bereiche.

Die Disziplin ist eine Warteschlange, kein Nullstand. Am 3. August standen die Postgres-Minor-Version vom 14. Mai mit den elf Security-Fixes und der Keycloak-Patch noch offen, und beide stehen mit Datum im Bericht. Für dieses Projekt ist der Identity Provider die Stelle, an der ein Patch-Rückstand am schwersten wiegt, weil an ihm jede Anmeldung aller vier Umgebungen hängt. Genau deshalb steht er in jedem Lauf in der Ist-Tabelle. Eine verwaltete Alternative nimmt einem die ersten beiden Zeilen der Tabelle ab, die Datenbank-Patches und die Zertifikate dazu. Die übrigen Zeilen bleiben in jedem Modell beim Entwickler.

Das Designziel lässt sich am Ende an einer Liste prüfen. Vier Cookies bleiben über den Anmelde-Ablauf hinaus im Browser, und die Datenschutzerklärung mit Stand vom September 2026 benennt jedes einzeln mit Zweck, Lebensdauer und Rechtsgrundlage:

Cookie, über den Login hinausZweckLebensdauerWann gesetzt
__Secure-authjs.session-tokenSession-Token als JWT, hält die Anmeldung, enthält Nutzer-ID, E-Mail und Name8 Stundennach dem Login
__Host-authjs.csrf-tokenCSRF-Token, schützt den Anmelde-Fluss vor gefälschten AnfragenSitzungbeim Login
sidebar_statemerkt, ob die Navigation auf- oder zugeklappt ist7 Tageim eingeloggten Bereich, beim Klick
di2_public_langmerkt die gewählte Sprache der öffentlichen Seiten12 Monatebei der Sprachwahl

Die beiden Login-Cookies stammen von Auth.js 5 und tragen dessen Standard-Namen. Die Konfiguration des Projekts folgt bei den Cookies den Vorgaben der Bibliothek, und die setzt HttpOnly, Secure und SameSite=Lax. HttpOnly verwehrt Skripten im Browser den Zugriff auf das Cookie, Secure beschränkt seine Übertragung auf HTTPS-Verbindungen, und SameSite=Lax hält es bei Anfragen zurück, die eine fremde Seite auslöst. Während des Anmelde-Umwegs über Keycloak setzt Auth.js außerdem kurzlebige Hilfs-Cookies, etwa für die Rücksprung-Adresse, den PKCE-Prüfwert und den Status des Ablaufs. Sie sind in der Liste oben nicht enthalten, tragen dieselben Attribute, laufen nach spätestens 15 Minuten ab und gehören zum Login selbst. Die beiden Präferenz-Cookies speichern eine ausdrücklich getroffene Wahl des Nutzers und nichts sonst. Kein Cookie enthält eine Kennung, die einen Besucher über Seiten hinweg verfolgt, und keines wird von einem Drittanbieter gesetzt.

Die Rechtsgrundlage dafür ist § 25 Abs. 2 Nr. 2 TDDDG, dem Telekommunikation-Digitale-Dienste-Datenschutz-Gesetz, das bis Mai 2024 TTDSG hieß. Die Einwilligung ist danach nicht erforderlich, wenn die Speicherung von Informationen in der Endeinrichtung des Endnutzers „unbedingt erforderlich ist, damit der Anbieter eines digitalen Dienstes einen vom Nutzer ausdrücklich gewünschten digitalen Dienst zur Verfügung stellen kann“. Für die Login-Cookies spricht viel dafür: Eine Anmeldung ohne Session-Cookie gibt es nicht, ein sicherer Login ohne CSRF-Schutz auch nicht. Die konkrete Einordnung hängt trotzdem an der Implementierung und am Dienst, nicht am Cookie-Typ. Bei den beiden Präferenz-Cookies ist es eine Einordnung, die das Projekt in der Datenschutzerklärung begründet. Der Nutzer hat die Sprache und den Zustand der Navigation ausdrücklich gewählt, und das Cookie speichert genau diese Wahl und nichts sonst. Wer diese Einordnung nicht teilen will, kann die Sprachwahl in der URL führen und den Navigationszustand nicht merken. Das kostet Komfort, keinen Banner.

Wichtig ist die Grenze dieser Aussage. Sie gilt für die Infrastruktur- und Anmelde-Schicht, gemäß Datenschutzerklärung und dem genannten Paragrafen. Sie ist kein Gesamturteil über die Anwendung und keine Rechtsberatung. Und sie gilt nur, solange keiner der Auslöser dazukommt, die eine Einwilligung erzwingen würden. Die Liste dieser Auslöser ist im Projekt als Regel hinterlegt: Web-Analytics mit Cookies, Marketing-Pixel, eingebettete Inhalte von Drittanbietern mit eigenen Cookies, Live-Chat-Widgets, A/B-Testing-Werkzeuge, Session-Recording. Sobald eines davon eingebaut werden soll, hält das Projekt die Umsetzung an. Dann kommt ein Banner dazu, das fremde Skript wird erst nach der Einwilligung geladen, und die Datenschutzerklärung wird um den Anbieter ergänzt. Dieselbe Regel gilt für diesen Blog.

Die Kosten-Seite

Was ein Cloud-Anbieter sonst unsichtbar trägt, liegt bei diesem Aufbau offen auf dem Tisch. Drei Posten waren im ersten halben Jahr die teuersten, ein vierter kommt in jeder Variante vor, und ein fünfter trifft nur die verwaltete.

Der geteilte Arbeitsspeicher: Der Server hat 8 GB RAM. Darauf laufen die vier App-Umgebungen, zwei PostgreSQL-Instanzen, eine mit den vier App-Datenbanken und eine für Keycloak, dazu Keycloak selbst, drei Beispiel-Datenbanken für die Produkt-Demo und ein privater Nextcloud-Stack mit Office-Anbindung, der mit dem Projekt nichts zu tun hat. Im August 2026 waren das gut ein Dutzend Container, zehn davon für DI². Die vier App-Container selbst sind mit zusammen rund 300 MB die kleinsten Posten. Die großen sind die Office-Anbindung mit etwa 1,6 GB, die SQL-Server-Beispiel-Datenbank mit 824 MB und Keycloak mit 754 MB. Wenn nach mehreren Deploys hintereinander der Speicher knapp wird, lagert der Kernel Speicherseiten aus, die länger nicht angefasst wurden. Bei einem Keycloak, der zwischen zwei Anmeldungen nichts zu tun hat, sind das fast alle seine Seiten. Am 28. August 2026 lagen 754 von 802 MB des Keycloak-Prozesses im Swap. Der erste Login-Aufruf dauerte 9,6 Sekunden, jeder weitere 0,19 Sekunden. Wie dieser Befund gefunden wurde, und warum der erste Verdacht auf die neue Version falsch war, erzählt der Diagnose-Artikel dieses Astes. Hier zählt nur die Kosten-Seite: Ein Identity Provider auf einem geteilten Server braucht ein RAM-Budget, das man ihm lassen muss.

Secrets in zwei Speicherorten: Mehrere Geheimnisse existieren zwangsläufig doppelt. Das Datenbank-Passwort steht in der Umgebungsdatei des Containers und in der Rolle in PostgreSQL. Das Client-Secret für Keycloak steht in derselben Umgebungsdatei und im Realm. Ein Token für einen Cron-Job steht in der Umgebungsdatei und in der Crontab. Dazu kommen die Secrets in GitHub, einmal je Umgebung, damit der Deploy sie setzen kann. Wenn ein Wert an einer Stelle rotiert wird und an der anderen nicht, scheitert der Login mit einer Meldung, die auch drei andere Ursachen haben kann. Die Antwort darauf war zweiteilig. Eine Tabelle in der Betriebs-Regel nennt pro Secret beide Speicherorte, das Drift-Symptom und den Abgleich-Weg. Ein Prüf-Skript gibt für jede Pflicht-Variable nur aus, ob sie gesetzt ist, und nie ihren Wert.

Doku, die hinter der Realität herläuft: Der Setup-Guide des Projekts beschreibt PostgreSQL an einer Stelle noch als direkt auf dem Server installiert. Tatsächlich läuft es als Container. Die Betriebs-Regel, die per Container-Liste verifiziert wurde, sagt das richtig. Beide Dokumente existieren nebeneinander, und die Regel im Projekt lautet seither, dass bei Widerspruch die verifizierte Betriebs-Regel gilt und dass jede Diagnose-Session mit der Container-Liste beginnt, statt aus dem Gedächtnis zu raten. Das ist keine Nachlässigkeit eines einzelnen Autors. Es ist die normale Alterung von Dokumentation in einem Projekt, in dem die Infrastruktur von Agenten-Sessions gebaut wird, die den Guide nicht bei jeder Änderung mitziehen.

Das Geld: Das Versprechen, dass KI-Entwicklung alles einfach macht, ist unter dem Schlagwort Vibe Coding verbreitet, und es klingt nach einem einzigen Abo. Tatsächlich sind es mehrere Posten, und die kostenlosen Stufen der verwalteten Dienste sind nur so lange kostenlos, wie man ihre Einschränkungen hinnimmt. Die Tabelle stellt den gebauten Aufbau einer verwalteten Alternative gegenüber, mit Listenpreisen je Monat und Stand vom 8. September 2026. Außer Hetzner rechnen alle genannten Anbieter in US-Dollar ab. Die Euro-Beträge sind deshalb zum EZB-Referenzkurs vom 7. September 2026 umgerechnet und gerundet, der Listenpreis steht jeweils in Klammern:

PostenSo gebautVerwaltete Alternative
HostingHetzner-Cloud-Server der Klasse 4 vCPU, 8 GB RAM, 80 GB: 8,49 € ohne Umsatzsteuer bei Neubestellung seit dem 15. Juni 2026, davor 6,49 €Vercel Pro: rund 17 € (20 USD) je Team-Mitglied, darüber hinaus nutzungsabhängig. Die Gratis-Stufe ist auf persönliche, nicht-kommerzielle Nutzung beschränkt
Datenbankim Server enthalten, als ContainerSupabase Pro: rund 22 € (25 USD), jedes weitere Projekt ab rund 9 € (10 USD) Rechenleistung. Die Gratis-Stufe erlaubt zwei Projekte mit 500 MB und pausiert sie nach einer Woche ohne Zugriff
Identity ProviderKeycloak, im Server enthalten, bezahlt in Arbeitsspeichergehosteter Identity Provider mit Gratis-Stufe bis einige tausend Nutzer und einer Login-Seite unter fremder Domain
DNS und MailAll-Inkl, im bestehenden Hosting-Paket enthaltenebenso
DeployGitHub in der kostenlosen StufeGitHub in der kostenlosen Stufe
KI-WerkzeugClaude mit Claude Code: Pro rund 17 € (20 USD), Max ab rund 86 € (100 USD)ebenso

Ohne das KI-Abo liegt der eigene Aufbau unter zehn Euro im Monat. Die verwaltete Alternative liegt bei rund 39 Euro, und das ist der Stand mit einem einzigen Supabase-Projekt und ohne Identity Provider. Für den Vergleich sind bei Supabase vier getrennte Projekte angesetzt, eines je Umgebung. Das ist eine Annahme für strikt getrennte Datenbanken, keine Vorgabe des Anbieters. Die Rechnung dafür lautet 20 USD für Vercel, 25 USD für Supabase Pro mit dem ersten Projekt und dreimal 10 USD für die weiteren Projekte, zusammen 75 USD oder rund 65 Euro, immer noch ohne Identity Provider. Das KI-Abo kommt in beiden Spalten obendrauf und ist der größte Posten. Welche Stufe nötig ist, entscheidet die tägliche Nutzungsdauer, denn beide Stufen deckeln die Nutzung in Fünf-Stunden-Fenstern und zusätzlich pro Woche, und wie weit man damit kommt, hängt von Modell und Gesprächslänge ab. Die Gratis-Stufe kennt Claude Code nicht. Und die verwaltete Spalte bringt genau die Frage zurück, die dieser Artikel vermeidet.

Die Drittland-Frage: Verwaltet heißt nicht verboten. Es heißt, dass die Voraussetzungen nicht von selbst erfüllt sind. Vercel ist ein US-Unternehmen, Supabases Vertragspartner sitzt in Singapur und hostet bei US-Anbietern, beide bieten EU-Regionen und einen Auftragsverarbeitungs-Vertrag, aber die Standard-Einstellung ist nicht die, die ein Betreiber in der EU braucht, bei Vercel etwa laufen Funktionen in den USA, solange man keine andere Region wählt. Region, Vertrag und den Nachweis darüber muss der Nutzer selbst herstellen, und eine EU-Region senkt bestimmte Übermittlungs-Risiken, ohne die übrigen Pflichten zu erledigen. Dazu trägt er zwei Risiken, die kein Vertrag abnimmt. Die heutige Übermittlungs-Grundlage in die USA, das Data Privacy Framework von 2023, gilt. Ein Rechtsmittel dagegen liegt seit Oktober 2025 beim Europäischen Gerichtshof, und die beiden Vorgänger dieser Grundlage sind dort gefallen. Der CLOUD Act kann US-Anbieter unter bestimmten Voraussetzungen zur Herausgabe von Daten verpflichten, auch wenn diese außerhalb der USA gespeichert sind. Warum eine EU-Region deshalb nicht dasselbe ist wie ein EU-Anbieter, steht im eigenen Artikel EU-Region ist nicht EU-Anbieter — Vercel, Supabase und die Drittland-Frage der DSGVO. Wer diese Risiken nicht tragen will, kommt um die Anbieter-Frage nicht herum, und für dieses Projekt lautete die Antwort: kein US-Anbieter in der Kette. Auf dem eigenen Server in der EU stellt sich die Frage für die Infrastruktur nicht, und das ist der eigentliche Unterschied zwischen den beiden Spalten, nicht der Preis.

Die ersten drei Posten haben etwas gemeinsam. Bei einem verwalteten Dienst trägt sie der Anbieter. Hier trägt sie der Maintainer, auch wenn ein Agent die Skripte und Konfigurationen geschrieben hat. Der vierte Posten fällt in jeder Variante an. Der fünfte ist der Preis der verwalteten Variante, und er steht auf keiner Rechnung.

Was ein Cloud-Anbieter sonst trägt

Die Rechnung lässt sich in zwei Spalten führen. Auf der einen Seite steht, was der Aufbau liefert. Er gibt die Kontrolle über jedes Cookie und jede Anfrage, die der Browser eines Besuchers stellt. Er hält Daten, Anmeldung und Mail an einem Standort in der EU. Er liefert eine Login-Seite im eigenen Design und ohne fremde Domain, und er trägt vier Umgebungen zum Preis eines einzelnen Servers. Das Designziel „DSGVO ohne Cookie-Banner“ ist damit nicht nur erreicht, sondern nachprüfbar. Die Cookie-Liste hat vier Einträge, und die Liste der Auslöser, die ein Banner erzwingen würden, ist leer.

Auf der anderen Seite steht der Betrieb, mit allem, was der Update-Abschnitt und die Kosten-Sektion aufgezählt haben, von der Mail-Sperre bis zur Monatsrechnung. Diese Spalte steht in keinem Vibe-Coding-Versprechen. Sie ist der Teil der Entwicklung, den ein Agent beschleunigt, aber nicht übernimmt, und für einen Datenbank-Entwickler ohne Betriebs-Hintergrund war sie der Teil mit dem meisten Lernaufwand. Die Arbeit ist nicht verschwunden, sie ist von der Anwendung in den Betrieb gewandert, und sie ist dort geblieben. Für das ganze Projekt beschreibt das der Hub-Artikel Agentic Coding aus Anwender-Sicht, dessen Infrastruktur-Kapitel dieser Artikel ausführt. Der eigentliche Preis dieses Aufbaus sind nicht 8,49 Euro im Monat. Es ist die Verantwortung für Patch-Stand, Backups, Zertifikate, Mail, Secrets und Wiederherstellung, und die steht in keinem Versprechen. Wer denselben Weg geht, sollte diese Spalte vor der ersten Entscheidung kennen, nicht danach.

Nachbau-Checkliste: vom leeren Server zur ersten Anmeldung

Wer den Aufbau nachbauen will, braucht keines der Skripte aus diesem Artikel, aber die Reihenfolge. Zehn Schritte, und bei jedem steht, woran es im DI²-Projekt gehakt hat oder was sich rückblickend als Pflicht erwiesen hat:

  1. Server bestellen: bei einem EU-Anbieter, mit mindestens 8 GB RAM für vier Umgebungen und einen Identity Provider. Beispiel-Datenbanken und private Dienste zählen beim Budget mit.
  2. DNS setzen: beim bestehenden Hoster ein A-Record je Subdomain, alle auf dieselbe IP, einer davon für den Identity Provider.
  3. nginx und Certbot einrichten: ein Zertifikat je Subdomain, Erneuerung per Cron, und nach dem ersten Lauf einmal den Trockenlauf der Erneuerung ausführen.
  4. Docker und PostgreSQL starten: eine Instanz als Container, eine Datenbank je Umgebung. Von Anfang an als Container, sonst driftet der Setup-Guide von der Realität weg.
  5. Keycloak als eigenen Stack starten: mit eigener Datenbank und eigener Subdomain, dann ein Realm je Umgebung, angelegt von einem Skript, das beim zweiten Lauf nichts ändert.
  6. App-Container je Umgebung binden: nur auf localhost, mit eigener Umgebungsdatei und eigenem Port. Das Prüf-Skript für die Pflicht-Variablen gehört zum ersten Tag, nicht zum ersten Drift-Fehler.
  7. nginx-Vhosts anlegen: je Subdomain auf den passenden Port, mit geheimem Proxy-Header und Rate-Limit auf dem Rückweg des Logins.
  8. Deploy per GitHub Actions einrichten: ein Workflow je Umgebung, per SSH Checkout setzen, Container bauen und starten. Die Produktion baut nur aus dem Haupt-Branch.
  9. Mail-Versand testen, bevor jemand ein Passwort zurücksetzt: bei Hetzner sind die Ports 25 und 465 gesperrt, der Weg führt über ein Relay auf Port 587 oder die beantragte Freischaltung.
  10. Datenschutzerklärung mit Cookie-Liste schreiben, Backup beider Datenbanken einmal zurückspielen und einen Update-Rhythmus festlegen: alles drei, bevor der erste Benutzer kommt. Es sind keine Nachträge, sondern Teil des Aufbaus.

FAQ

Braucht eine Web-Anwendung mit Login wirklich kein Cookie-Banner?

Nicht für die Cookies, ohne die der Login nicht funktioniert. Session-Token und CSRF-Token sind unbedingt erforderlich für einen Dienst, den der Nutzer ausdrücklich wünscht, und dafür verlangt § 25 Abs. 2 Nr. 2 TDDDG keine Einwilligung. Für Cookies, die eine ausdrücklich getroffene Präferenz speichern, etwa die Sprachwahl, ist das eine Einordnung, die man in der Datenschutzerklärung begründen sollte. Der Banner kommt in dem Moment, in dem ein Tracker, ein Marketing-Pixel oder ein eingebetteter Drittinhalt dazukommt. Die Aussage bezieht sich auf die Infrastruktur-Schicht und ersetzt keine rechtliche Prüfung der gesamten Anwendung.

Warum vier Umgebungen auf einem Server statt vier Server?

Der Grund sind die Kosten und die Einfachheit des Randes. Ein Server bedeutet einen Reverse Proxy, einen Satz Zertifikate, eine PostgreSQL-Instanz und einen Keycloak für alle Stufen. Die Trennung entsteht nicht durch Hardware, sondern durch eigene Checkouts, eigene Datenbanken, eigene Realms und eigene lokale Ports. Der Preis ist ein gemeinsames RAM-Budget, und er wird sichtbar, sobald ein Dienst zwischen zwei Aufrufen ausgelagert wird. Für ein Solo-Projekt mit einer Handvoll Benutzer trägt der Aufbau, für mehr Last wäre die Produktion der erste Kandidat für eine eigene Maschine.

Warum Keycloak und nicht Auth0, Clerk oder ein anderer gehosteter Identity Provider?

Ein gehosteter Identity Provider bringt seine eigene Login-Seite unter seiner eigenen Domain mit, und mit ihr seine eigenen Cookies. Die Anmeldedaten liegen dann bei einem Dritten, oft außerhalb der EU. Mit Keycloak bleiben Login-Seite, Benutzer und Rollen auf dem eigenen Server, das Theme ist frei gestaltbar, und ein Realm je Umgebung ist ein Skript-Lauf. Der Preis sind rund 750 MB Arbeitsspeicher, regelmäßige Updates und die Pflicht, die Realm-Provisionierung als Skript statt als Klick-Folge zu halten.

Was macht All-Inkl noch, wenn der Server bei Hetzner steht?

Er übernimmt zwei Dinge, das DNS und die Postfächer. Die A-Records für alle Subdomains zeigen vom bisherigen Hoster auf den Server bei Hetzner, und die Mail-Adressen laufen weiter über dessen Postfächer. Was nicht funktioniert, ist der Versand vom Server über den SMTP-Server des Hosters auf Port 465, weil Hetzner diesen Port auf neuen Cloud-Servern sperrt, Stand September 2026. Dafür braucht es entweder die beantragte Freischaltung oder ein Relay auf Port 587 oder 2525.

Kann man Vercel und Supabase DSGVO-konform nutzen?

Nicht von selbst, sondern erst durch eigene Arbeit. Beide bieten EU-Regionen und einen Auftragsverarbeitungs-Vertrag. Vercel ist ein US-Unternehmen und zusätzlich unter dem EU-US Data Privacy Framework zertifiziert, Supabases Vertragspartner sitzt in Singapur und hostet bei US-Anbietern. Die Voraussetzungen dafür schafft man, indem man die Region wählt, den Vertrag abschließt, die Übermittlungs-Grundlage dokumentiert und die Datenschutzerklärung erweitert. Ob das im Einzelfall reicht, ist eine rechtliche Bewertung, die dieser Artikel nicht leistet. Ein Restrisiko bleibt, weil die beiden Vorgänger der heutigen Grundlage vor dem Europäischen Gerichtshof gefallen sind, gegen die heutige ein Rechtsmittel beim Europäischen Gerichtshof anhängig ist und der CLOUD Act US-Anbieter unter bestimmten Voraussetzungen zur Herausgabe verpflichten kann, unabhängig von der Region. Wer das nicht tragen will, hostet bei einem EU-Anbieter in der EU, so wie dieses Projekt. Die Einzelheiten zu beiden Risiken stehen im eigenen Artikel EU-Region ist nicht EU-Anbieter — Vercel, Supabase und die Drittland-Frage der DSGVO.

Was wäre beim zweiten Aufbau anders?

Vier Dinge, und alle stammen aus der Kosten-Sektion. PostgreSQL würde von Anfang an als Container laufen, damit Setup-Guide und Realität nicht auseinanderlaufen. Das Prüf-Skript für die Umgebungsdateien käme am ersten Tag, nicht nach dem ersten Drift-Fehler. Das RAM-Budget je Dienst würde die Beispiel-Datenbanken der Produkt-Demo ehrlich einrechnen, weil sie zusammen mehr Speicher brauchen als die Anwendung selbst. Und die Mail-Sperre des Anbieters würde vor dem ersten Passwort-Reset-Test gelesen, nicht danach.

Verwandte Artikel

Hub dieses Astes:

Cluster-Hub:

Geschwister-Artikel:

CI/CD und Deploy:

Einstieg: