Projekt starten

Willkommen im
Maschinenraum.

Hier dokumentiere ich Code-Snippets, Architektur-Entscheidungen und die ungeschönte Wahrheit beim Bau moderner B2B-Infrastrukturen.

28. April 2026 Server Infra

Cold Snapshots: Wenn das Rescale-Limit erreicht ist

Hetzner verweigert das einfache Server-Upgrade wegen lokaler Hardware-Limits? Ein "Cold Snapshot" und der Umzug auf eine neue Instanz löst das Problem ohne Datenverlust.

Wer komplexe n8n-Workflows oder lokale KI-Modelle hosten will, bringt einen kleinen Server mit 4 GB RAM schnell in einen Out-of-Memory (OOM) Absturz. Wenn ein einfaches Skalieren der Instanz im selben Rechenzentrum physisch nicht möglich ist, bleibt nur der "Snapshot & Switch"-Move auf eine neue Maschine.

Wichtigstes Learning dabei: Ein Abbild im laufenden Betrieb ("Hot Snapshot") birgt das Risiko, dass die PostgreSQL-Datenbank beim Neustart crasht, weil Transaktionen noch im Arbeitsspeicher hingen. Erst das saubere Herunterfahren des Servers ("Cold Snapshot") garantiert eine 100 % konsistente Migration der Architektur.

# Der saubere Migrations-Workflow:
1. Server regulär herunterfahren (Power Off)
2. Snapshot im Cloud-Panel erstellen
3. Neue Instanz (z.B. CX43) aus Snapshot booten
4. Alte Instanz zur Sicherheit 24h pausieren, dann löschen
25. April 2026 Grafana Security

Grafana in WordPress einbetten: Der Kampf gegen X-Frame-Options

"localhost hat die Verbindung verweigert" – der absolute Klassiker beim Versuch, ein selbst gehostetes Grafana-Dashboard als Iframe in einer Web-App anzuzeigen.

Beim Bau des AlpxShot-Frontends (WordPress/Elementor) weigerte sich der Browser strikt, das Grafana-Dashboard im Iframe zu laden. Grafana verbietet das Einbetten standardmäßig aus Sicherheitsgründen (Clickjacking-Schutz).

Die Lösung erfordert Eingriffe an zwei Fronten: Erstens muss man Grafana über die Docker-Umgebungsvariablen lockern. Zweitens funkt einem oft der Nginx Proxy Manager dazwischen, der eigene Sicherheits-Header setzt.

# 1. Grafana docker-compose.yml anpassen:
environment:
  GF_SECURITY_ALLOW_EMBEDDING: "true"
  GF_AUTH_ANONYMOUS_ENABLED: "false"
  GF_SERVER_COOKIE_SAMESITE: "none"
  GF_SERVER_COOKIE_SECURE: "true"

# 2. Im Nginx Proxy Manager (Advanced Tab) die Header killen:
proxy_hide_header X-Frame-Options;
proxy_hide_header Content-Security-Policy;
22. April 2026 DNS Networking

Die DNS-Falle: Warum Webhooks offline blieben

Nach dem IP-Wechsel lief die Hauptdomain sofort, aber n8n warf unerbittliche Timeouts. Die Ursache war eine tückische Hierarchie-Regel in der DNS-Zone.

Beim Server-Umzug verlässt man sich gerne auf den Wildcard-Eintrag (*.domain.com), der bequem alle Subdomains auf die neue IP-Adresse lenkt. Doch das weltweite DNS-System hat eine eiserne Regel, die mich heute Troubleshooting-Zeit gekostet hat: Ein expliziter Eintrag schlägt immer die Wildcard.

In den IONOS-Einstellungen blockierte ein alter, festgeschriebener A-Record speziell für "n8n" die neue Routenführung. Während die Hauptseite (gecovert vom Root-Eintrag) schon längst vom neuen Server antwortete, liefen API-Calls in Formulare ins Leere. Ein schneller Check über externe DNS-Tools offenbarte den Alt-Eintrag, der das Setup sabotiert hat.

# Das DNS Prioritäts-Regelwerk
*.alpxai.de   -> 95.217.x.x (Neue IP, Wildcard)
n8n.alpxai.de -> 116.203.x.x (Alte IP, Explicit Record)

# Ergebnis: Explizit gewinnt. n8n verweist auf den toten Server.
# Fix: Alten A-Record löschen, Wildcard greifen lassen.
18. April 2026 Docker Proxy

Interne Docker-Routen: Mach deine IPs agnostisch

Harte IP-Adressen im Nginx Proxy Manager sind eine tickende Zeitbombe beim Server-Umzug. Die Lösung liegt im Docker-internen Netzwerk-Routing.

Nach dem Booten des neuen Servers meldete der Proxy für einige Tools plötzlich ein "502 Bad Gateway". Der Fehler? In der grafischen Oberfläche des NPM war die alte, öffentliche Hetzner-IP als Zieladresse fest verdrahtet. Der Proxy hat die Anfrage also nach draußen geschickt, statt sie intern an den Container weiterzugeben.

Die saubere Architekten-Lösung: Container kommunizieren über ein gemeinsames Docker-Bridge-Netzwerk (hier: proxy-tier). Im Proxy trägt man als Forward-Ziel keine IP ein, sondern schlichtweg den Containernamen (z.B. alpxai-n8n). Das isoliert die Dienste nach außen, schließt Sicherheitslücken und macht das Setup komplett IP-agnostisch für künftige Umzüge.

# docker-compose.yml Auszug
services:
  n8n:
    container_name: alpxai-n8n
    networks:
      - proxy-tier

# NPM Konfiguration:
Forward Hostname: alpxai-n8n
Forward Port: 5678
15. April 2026 Docker Networking

Docker-Bridge-Netzwerke: n8n, Grafana und Supabase verbinden

Warum "Connection Refused" oft nur ein fehlendes Kabel im Serverraum ist – und wie man isolierte Container-Stacks über ein gemeinsames Proxy-Netzwerk verheiratet.

Beim Versuch, Grafana an die PostgreSQL-Instanz von Supabase anzudocken, hagelte es DNS-Fehler (lookup supabase-db on 127.0.0.11:53: server misbehaving). Der Grund war ein blinder Fleck in der Architektur: Während das Supabase-API-Gateway im globalen proxy-tier Netzwerk hing, war die Datenbank selbst noch im isolierten Default-Netzwerk eingesperrt.

Anstatt Ports unsicher ins Internet freizugeben, reicht es, alle beteiligten Container offiziell in dasselbe Docker-Netzwerk einzuladen. Danach reicht der reine Container-Name (z.B. supabase-db:5433) als Host-Adresse völlig aus.

# In der docker-compose.yml bei der Datenbank ergänzen:
services:
  db:
    container_name: supabase-db
    networks:
      - default
      - proxy-tier # Die Brücke zu Grafana und n8n

networks:
  default:
    driver: bridge
  proxy-tier:
    external: true
10. April 2026 n8n SQL Automation

Workflows verschlanken: Warum Datenbank-Trigger oft besser als n8n sind

n8n hasst es, wenn Datenmengen gesplittet und wieder gemerged werden. Die Lösung: Die Intelligenz ins Backend verlagern.

Für die AlpxShot-App sollte n8n Schießergebnisse auslesen, prüfen, ob der User existiert, und andernfalls ein Profil anlegen. Der Versuch, dies über "Get Row"- und "Merge"-Nodes abzubilden, endete im Chaos, da n8n die Iteration über Arrays bei fehlschlagenden Lookups oft durcheinanderbringt.

Die Architektur-Lösung: Mache das Middleware-Tool (n8n) wieder "dumm" und die Datenbank "schlau". n8n pusht die Ergebnisse blind in Supabase. Ein BEFORE INSERT SQL-Trigger fängt die Daten ab, sucht selbstständig nach der user_id oder legt dynamisch ein neues, inaktives Profil an.

-- Die schlanke Logik als Postgres Trigger:
CREATE OR REPLACE FUNCTION auto_assign_user_id() RETURNS TRIGGER AS $$
DECLARE v_user_id uuid;
BEGIN
  -- Gibt es schon ein Profil für diesen Namen?
  SELECT user_id INTO v_user_id FROM profiles WHERE display_name = NEW.member_name LIMIT 1;
  IF v_user_id IS NOT NULL THEN
    NEW.user_id := v_user_id;
  ELSE
    -- Profil existiert gar nicht -> anlegen!
    INSERT INTO profiles (display_name, is_active) VALUES (NEW.member_name, false)
    ON CONFLICT (display_name) DO NOTHING;
  END IF;
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;
07. April 2026 n8n SQL Automation

Lead-Gen 2.0: Kontaktformulare ohne Plugins direkt in die Datenbank

Standard-Formular-Plugins bremsen Websites oft aus und speichern Daten in isolierten Silos. Ich habe eine performante Alternative gebaut, die Leads via n8n-Webhook direkt in eine verschlüsselte Postgres-Instanz schießt.

Das Problem bei vielen B2B-Infrastrukturen ist die mangelnde Datendurchgängigkeit. Leads landen oft in WordPress-Datenbanken oder werden unstrukturiert per E-Mail versendet. Das erschwert die spätere Analyse und Skalierung. Meine Lösung verzichtet komplett auf schwere Plugins: Ein schlankes HTML-Formular feuert die Daten per POST-Request direkt an einen n8n-Webhook-Node.

In n8n findet die Magie statt: Die Daten werden validiert, anonymisiert und anschließend in eine Postgres-Datenbank geschrieben. Diese dient als Single Source of Truth. Durch die Anbindung von Grafana habe ich ein Echtzeit-Dashboard für alle eingehenden Anfragen, ohne jemals manuell Daten exportieren zu müssen.

// Beispiel: Frontend Fetch an den n8n Webhook
fetch('https://n8n.alpxai.de/webhook/lead-capture', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    name: formData.name,
    email: formData.email,
    request: formData.message,
    timestamp: new Date().toISOString()
  })
});
06. April 2026 Security n8n

Der unsichtbare Türsteher: Spam-Schutz mit Honeypots

Niemand mag Spam in der Datenbank. Statt Nutzer mit nervigen Captchas zu quälen, habe ich heute einen unsichtbaren Honeypot in mein Formular eingebaut.

Ein "Honeypot" ist ein simples Eingabefeld, das via CSS (display: none oder weit außerhalb des Bildschirms) für Menschen unsichtbar gemacht wird.

Dumme Spam-Bots lesen aber nur den HTML-Code und füllen alle Felder aus, die sie finden. Mein n8n-Workflow prüft nun einfach: Ist das Feld ausgefüllt? Wenn ja -> direkt in den Mülleimer. Die Datenbank bleibt sauber!

// Die n8n IF-Node Logik (Pseudocode):
if (body.bot_check != "") {
  return "SPAM DETECTED";
} else {
  return "SAVE TO POSTGRES";
}
03. April 2026 Grafana SQL

Warum Grafana das bessere Frontend für Postgres ist

Wenn man rohe Daten aus einer PostgreSQL-Datenbank für Entscheider visualisieren will, kann man ewig eigene Dashboards programmieren – oder man nimmt Grafana.

Heute habe ich die Lead-Zentrale finalisiert. Der größte Aha-Moment: Grafana braucht keine komplexe API dazwischen. Man verbindet es direkt als "Data Source" mit PostgreSQL (Read-Only User!).

Um die Anzahl der Leads von heute anzuzeigen, reicht ein einfacher Einzeiler. Das spart mir tagelange Frontend-Entwicklung in React oder Vue.

SELECT count(id)
FROM leads
WHERE created_at >= CURRENT_DATE;
28. März 2026 SQL Security

Infinite Recursion in Supabase: Wenn RLS-Policies sich selbst aussperren

Ein klassischer "Error 500" beim Login: Wie eine gut gemeinte Admin-Prüfung in Row Level Security (RLS) die Datenbank in eine Endlosschleife treibt.

Beim Absichern der Tabelle profiles sollte ein User nur sein eigenes Profil lesen dürfen, ein Admin jedoch alle. Der erste SQL-Ansatz fragte innerhalb der Policy per SELECT role FROM profiles... ab, ob der Nutzer Admin ist.

Das fatale Problem: Postgres muss die RLS-Policy bei jedem SELECT anwenden. Um herauszufinden, ob du lesen darfst, liest Postgres die Tabelle, was die Policy erneut triggert – eine endlose Spirale (Infinite Recursion). Die Lösung ist eine sichere OR-Logik oder eine isolierte Funktion (SECURITY DEFINER), die die Zugriffsprüfung ohne RLS-Schleife durchführt.

-- So bricht man die RLS-Endlosschleife auf:
CREATE POLICY "Profile_Zugriffsregel" ON profiles FOR SELECT TO authenticated
USING (
  -- 1. Ist es das eigene Profil? (schneller Auth-Token-Vergleich)
  auth.uid() = user_id 
  OR 
  -- 2. Ist der User ein Admin? (Isolierte Sub-Query)
  EXISTS (
    SELECT 1 FROM profiles WHERE user_id = auth.uid() AND role = 'admin'
  )
);
15. März 2026 Server Docker

Swap-Death auf dem Hetzner CX22: Wenn Container den Server killen

Warum eine kleine WordPress-Installation plötzlich 100 % CPU-Last erzeugt und den Server einfriert – und wie ein simpler Swap-Airbag das Problem löst.

Ein Hetzner CX22 (4 GB RAM) lief wochenlang stabil mit n8n, Supabase und Grafana. Ein einfaches docker compose up -d für einen neuen MariaDB/WordPress-Stack brachte den Server jedoch zum sofortigen Absturz mit 100 % CPU-Auslastung.

Die Diagnose über free -h zeigte das Problem: 0B Swap konfiguriert. Beim Start beanspruchen Datenbank-Container sofort hunderte Megabyte RAM. Geht Linux der physische Speicher aus und es existiert keine Auslagerungsdatei (Swap), stürzen die Prozesse in einem "Out of Memory"-Loop ab, was extrem hohe I/O-Wartezeiten (CPU-Spikes) erzeugt. Ein 2GB Swapfile rettete den Server ohne Hardware-Upgrade.

# Der 1-Minuten Swap-Airbag
fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab