Newsletter Infrastructure
🛠️ DIY & Open Source 🔐 DSGVO

DSGVO-konformer Newsletter mit Double-Opt-In

Wie ich einen vollständig selbst-gehosteten, datenschutzkonformen Newsletter mit Listmonk, Docker und Open Source aufgesetzt habe — inklusive Double-Opt-In, deutscher Templates und vollautomatischem Kampagnen-Versand.

📅 Mai 2026 · Letztes Update: 26.05.2026

TL;DR

Ich betreibe einen DSGVO-konformen, selbst-gehosteten Newsletter für meine Website hotzmatic.com — mit Double-Opt-In, getrennten Abo-Listen (KI Research & Security), automatischem Versand und vollständiger Datenkontrolle. Kein Mailchimp, kein SendGrid, kein Vendor Lock-in.

Tech-Stack: Listmonk (Go), PostgreSQL, Docker Compose, Mail Hoster SMTP, Nginx Proxy Manager, Python Automation — alles läuft auf einem eigenen Server. → Newsletter ansehen

1. Warum ein eigener Newsletter?

Ich veröffentliche tägliche Blogbeiträge zu zwei Themen: KI-Forschung und IT-Security. Irgendwann kam die Frage auf: Wie bekommen meine Leser diese Inhalte regelmäßig und datenschutzkonform zugestellt?

Die üblichen Verdächtigen (Mailchimp, SendGrid, MailerLite) schieden aus mehreren Gründen aus:

  • Datenschutz: US-Cloud-Anbieter → unklare DSGVO-Rechtslage nach Schrems II
  • Kosten: Ab 2.000 Kontakten werden viele Dienste schnell teuer
  • Vendor Lock-in: Export, Migration, Datenhoheit – alles fremdbestimmt
  • Feature-Grenzen: Keine API-fähige Automation ohne Premium-Tarif

Die Lösung: Ein eigener Server, Open Source Software, volle Kontrolle. Genau mein Style.

2. DSGVO-Anforderungen & Anforderungsprofil

Bevor ich eine Zeile Code geschrieben habe, stand der rechtliche Rahmen. Ein DSGVO-konformer Newsletter muss folgende Punkte erfüllen:

  • Double-Opt-In (DOI): Der Nutzer meldet sich an, bekommt eine Bestätigungsmail und muss explizit klicken – erst dann wird er in die Liste aufgenommen. Nachweisbar und revisionssicher.
  • Getrennte Abo-Listen: Jedes Thema braucht eine eigene Liste. Der Nutzer kann pro Liste entscheiden und sich getrennt abmelden.
  • Jederzeit kündbar: Ein Unsubscribe-Link – ohne Login, ohne Hürden – in jeder E-Mail.
  • Datenhoheit: Alle personenbezogenen Daten (E-Mail, Anmeldezeitpunkt, IP) bleiben auf eigenen Servern in Deutschland.
  • Kein Tracking: Keine versteckten Pixel, kein E-Mail-Tracking durch Dritte. Offene Raten sind Nice-to-Have, aber kein Muss.

💡 Gut zu wissen: Listmonk unterstützt Open-Tracking und Click-Tracking optional — ich habe es bewusst deaktiviert. DSGVO-konform und ressourcenschonend.

3. Der Tech-Stack im Überblick

📨 Listmonk (Go)

Leichtgewichtige Newsletter-App (~10 MB Binary). Single-Binary-Deployment, extrem performant. REST-API, Campaign-Templates, Subscriber-Management. Entwickelt von knadh.

🐘 PostgreSQL 16

Separater DB-Container. Alle Abonnenten, Kampagnen, Templates und Einstellungen in einer relationalen DB – backupbar, migrierbar, auditierbar.

🐳 Docker Compose

App + DB als zwei Services im selben Netzwerk. Port 9010 nur lokal exponiert, extern via Reverse Proxy.

🔒 Nginx Proxy Manager

SSL-Terminierung (Let's Encrypt), Reverse Proxy auf newsletter.hotzmatic.com. Intuitive Web-UI für Proxy-Hosts.

📧 Mail Hoster (mail.hotzmatic.com)

Versand über den eigenen Mailserver. SMTP-Port 587, Auth-Protocol LOGIN. Der Newsletter nutzt eine eigene Absenderadresse. Funktioniert mit jedem SMTP-Anbieter.

🤖 Python Automation

Cron-gesteuertes Script (Mo–Fr, 11:30): Holt Blog-Posts, gruppiert nach Kategorie, erstellt Kampagnen per Listmonk-API und triggert den Versand.

Server: Ein VPS mit Ubuntu 24.04. Läuft 24/7 im Homelab-Netzwerk, angebunden ans Internet über eine öffentliche IP.

4. Setup: Listmonk mit Docker

Das Setup ist überraschend einfach — Listmonk kommt als Single-Binary und lässt sich dank Docker Compose in wenigen Minuten starten.

Verzeichnisstruktur

/home/nova/docker/listmonk/
├── docker-compose.yml    # App + PostgreSQL
├── config.toml           # Listmonk-Konfiguration
└── templates/            # Lokale Template-Vorlagen

docker-compose.yml

services:
  db:
    image: postgres:16-alpine
    container_name: listmonk-db
    restart: unless-stopped
    environment:
      POSTGRES_DB: listmonk
      POSTGRES_USER: listmonk
      POSTGRES_PASSWORD: "${DB_PASSWORD}"
    volumes:
      - listmonk-db-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U listmonk"]
      interval: 10s
      timeout: 5s
      retries: 5

  app:
    image: listmonk/listmonk:latest
    container_name: listmonk
    restart: unless-stopped
    ports:
      - "127.0.0.1:9010:9009"
    environment:
      TZ: Europe/Berlin
    volumes:
      - ./config.toml:/listmonk/config.toml:ro
    depends_on:
      db:
        condition: service_healthy

volumes:
  listmonk-db-data:

⚠️ Wichtig: Der interne Port von Listmonk ist 9009, der externe Port 9010. Binde den Port nur an 127.0.0.1 — nie an 0.0.0.0! Der Zugriff von außen läuft ausschließlich über den Reverse Proxy mit SSL-Verschlüsselung.

Initialisierung

cd /home/nova/docker/listmonk
docker compose up -d

# Einmalig: DB initialisieren
echo "y" | docker compose run --rm --entrypoint "./listmonk --install" app

docker compose restart app

Nach der Initialisierung öffnet man das Admin-UI unter https://newsletter.hotzmatic.com und durchläuft den Setup-Wizard — Benutzer anlegen, SMTP konfigurieren, Listen erstellen.

5. Double-Opt-In & Deutsche Templates

Listmonk unterstützt Double-Opt-In nativ. Die Einstellung findet sich unter Settings → Privacy:

  • Privacy → Double opt-in: AN (Default)
  • Unsubscribe link: AN

Jetzt kommt der entscheidende Teil: Die Standard-Templates sind auf Englisch. Ein deutsches Publikum erwartet deutsche Texte. Listmonk erlaubt benutzerdefinierte Templates unter Settings → Transactional Messages.

Double-Opt-In-E-Mail (Deutsch)

Betreff: "Bestätige deine Anmeldung – hotzmatic.com Newsletter"

<p>Schön, dass du dabei sein möchtest! Klicke auf den Button,
um deine E-Mail-Adresse zu bestätigen und den Newsletter zu abonnieren.</p>

<p><a href="{{ .OptinURL }}">
  ✅ Anmeldung bestätigen
</a></p>

<p style="font-size: 13px;">
  Wenn du dich nicht angemeldet hast, ignoriere diese E-Mail einfach.
</p>

Das Campaign-Template: "Hotzmatic Newsletter (Dark)"

Für die Kampagnen selbst habe ich ein eigenes Dark-Theme-Template im Look von hotzmatic.com erstellt:

  • Template-ID: 5
  • Design: Schwarzer Hintergrund, weiße Schrift, CCFF00-Akzente — passend zum Website-Design
  • Footer: Immer mit Unsubscribe-Link

⚠️ Wichtige Template-Fallstricke in Listmonk v6.1.0:

  • {{ .Body }} existiert nicht in Campaign-Templates — stattdessen {{ template "content" . }} verwenden
  • {{ .UnsubscribeURL }} gibt einen Fehler — stattdessen {{ UnsubscribeURL }} (ohne Punkt!) nutzen
  • body_source muss per SQL auf body gesetzt werden sonst ist der E-Mail-Inhalt leer

6. SMTP-Anbindung an IceWarp

Der Versand läuft über einen Mailserver auf mail.hotzmatic.com. Natürlich funktioniert das Setup auch mit jedem anderen SMTP-Anbieter. Die Konfiguration in der config.toml:

[smtp]
  [[smtp.servers]]
    name = "IceWarp Hotzmatic"
    host = "mail.hotzmatic.com"
    port = 587
    auth_protocol = "login"
    username = "info"
    password = "${SMTP_PASSWORD}"
    max_conns = 10
    default = true

⚠️ Achtung beim Auth-Protocol: IceWarp benötigt auth_protocol = "login", nicht cram-sha256! Der SMTP-Username ist der IceWarp-Account, nicht die From-Adresse. Letztere wird separat im Listmonk-Admin-UI unter Settings → SMTP → Default from email gesetzt.

7. Automatischer Kampagnen-Versand

Das Herzstück der Automation ist ein Python-Script, das werktags um 11:30 Uhr per Cron-Job läuft und die täglichen Blog-Posts als Newsletter versendet.

Workflow

  1. Blog abrufen: Das Script lädt https://hotzmatic.com/blog/index.json — ein von einem parallelen Cron-Job erstelltes JSON-Index-File
  2. Artikel finden: Es sucht nach Beiträgen vom aktuellen Datum (Fallback: gestern, vorgestern)
  3. Nach Kategorie gruppieren: AI-Beiträge → KI-Research-Liste, Security-Beiträge → Security-Report-Liste
  4. HTML extrahieren: Das Script ruft jeden Artikel-URL auf und extrahiert sauberen HTML-Content (ohne Navigations-Elemente, Footer, etc.)
  5. Kampagnen erstellen: Per Listmonk-REST-API werden Kampagnen als Draft angelegt
  6. Body fixen: Per SQL wird body_source = body gesetzt (Listmonk-API ignoriert das Feld)
  7. Sofort versenden: Status auf "scheduled" setzen → Listmonk verschickt sofort

Der Cron-Job läuft auf demselben Server und verwendet die lokale Listmonk-API auf Port 9010. Keine externen Abhängigkeiten, kein API-Key-Rotation-Management.

🤖 Cron-Job auf einen Blick

Name: Newsletter AI + Security Versand

Schedule: 30 11 * * 1-5 (Mo–Fr, 11:30 Uhr)

Script: /home/nova/.hermes/scripts/newsletter-send.py

Server: Server

Quelle: blog/index.json von hotzmatic.com

API-Integration

Listmonk v6.1.0 hat eine REST-API, aber der Authentication-Mechanismus ist... speziell:

  • Basic Auth funktioniert in v6.1.0 unzuverlässig
  • Lösung: Session-basierter Login via /admin/login mit CSRF-Nonce
  • Das Script loggt sich mit gültigem Nonce ein, extrahiert das Session-Cookie und verwendet es für alle API-Aufrufe
# Session holen (Nonce + Login)
s = requests.Session()
login = s.get("http://127.0.0.1:9010/admin/login", timeout=10)
nonce = re.search(r'name="nonce"[^>]*value="([^"]+)"', login.text).group(1)
s.post("http://127.0.0.1:9010/admin/login",
       data={"username": "admin", "password": "...", "nonce": nonce}, timeout=10)

8. Betrieb & Lessons Learned

Was gut läuft

  • Stabiler Betrieb: Seit Inbetriebnahme am 25. Mai 2026 läuft Listmonk ohne Ausfall — kein einziger Container-Neustart nötig
  • Zustellrate: 100% Zustellrate bei den ersten Kampagnen — IceWarp SMTP liefert sauber aus
  • Performance: Listmonk braucht ~30 MB RAM im Leerlauf — läuft problemlos auf einem 4 GB VPS
  • Double-Opt-In: Funktioniert Out-of-the-Box — kein Hexenwerk

Lessons Learned (auch für dein Projekt)

🔴 Kritisch: Template-Syntax in Listmonk v6.1.0

Das war der härteste Fehler. Der E-Mail-Inhalt war leer ([]) beim Kampagnen-Versand. Ursache: Listmonk verwendet html/template von Go, und die Doku war veraltet. Lösung: {{ template "content" . }} statt {{ .Body }} im Campaign-Template. Und: body_source = body per SQL setzen.

🟡 SMTP Auth-Protocol

IceWarp erlaubt standardmäßig nur LOGIN Auth. Das UI-Dropdown von Listmonk war teilweise nicht klickbar — die Konfiguration musste per SQL in die Datenbank geschrieben werden.

🟡 Bcrypt-Hash-Escaping

Ein Admin-Passwort-Reset wurde zum Albtraum: Bash interpretiert $-Zeichen in Bcrypt-Hashes als Variable-Expansion. Lösung: Python subprocess verwenden, nicht psql -c "..." direkt in der Shell.

🟢 Positiv: Docker Compose macht's einfach

Backup? Einfach das Volume sichern. Update? docker compose pull && docker compose up -d. Migration? PostgreSQL-Dump ziehen, woanders einspielen. So einfach sollte Infrastruktur sein.

9. Fazit & Ausblick

Der selbst-gehostete Newsletter mit Listmonk ist ein Paradebeispiel dafür, warum ich Open Source liebe: Volle Kontrolle, DSGVO-Konformität ohne Kompromisse, keine monatlichen Kosten, und ein Setup, das nach einmaligem Aufbau von selbst läuft.

✅ Das funktioniert

  • Double-Opt-In für DSGVO-Konformität
  • Getrennte Listen (KI / Security) mit Wahlfreiheit
  • Automatischer Tagesversand per Cron
  • Deutsche Templates
  • Eigene Infrastruktur in DE

🔜 Geplant

  • Statistiken & Öffnungsraten (DSGVO-konform)
  • Webhook für automatisierte Begrüßungs-Serie
  • Segmentierung nach Interessensprofil

Wer Interesse hat: Der Source-Code des Setups (docker-compose.yml, config.toml, Automation-Script) ist in meiner Dokumentation hinterlegt. AI hat geholfen, die Fallstricke zu dokumentieren — damit der nächste Self-Hoster nicht dieselben Fehler macht.

TL;DR finale Version: Listmonk + Docker + eigener SMTP = DSGVO-konformer Newsletter mit Double-Opt-In, vollem Datenschutz und null Vendor-Risiko.
→ Newsletter abonnieren