📅 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
📖 Inhalt
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!) nutzenbody_sourcemuss per SQL aufbodygesetzt 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
- Blog abrufen: Das Script lädt
https://hotzmatic.com/blog/index.json— ein von einem parallelen Cron-Job erstelltes JSON-Index-File - Artikel finden: Es sucht nach Beiträgen vom aktuellen Datum (Fallback: gestern, vorgestern)
- Nach Kategorie gruppieren: AI-Beiträge → KI-Research-Liste, Security-Beiträge → Security-Report-Liste
- HTML extrahieren: Das Script ruft jeden Artikel-URL auf und extrahiert sauberen HTML-Content (ohne Navigations-Elemente, Footer, etc.)
- Kampagnen erstellen: Per Listmonk-REST-API werden Kampagnen als Draft angelegt
- Body fixen: Per SQL wird
body_source = bodygesetzt (Listmonk-API ignoriert das Feld) - 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/loginmit 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