Zum Inhalt springen
Serverküche
Suche

Die Suche wird geladen … (nur in der veröffentlichten Seite verfügbar).

Anwendungen Schwierigkeit: Fortgeschritten

listmonk: der eigene Newsletter-Server – DSGVO-freundlich

listmonk 6.2 mit Postgres 18 hinter Traefik: Liste mit Double Opt-in, erste Kampagne, Datenschutz-Schalter – und ein Testlauf, bevor echte Mails rausgehen.

· 11 Min. Lesezeit ·Dauer: ca. 60 Minuten
Inhaltsverzeichnis

Newsletter selbst verschicken heißt: Die Adressliste liegt bei dir, nicht bei einem Dienstleister, der sie „zur Verbesserung des Angebots" auswertet. listmonk macht das erstaunlich bequem – der schwierige Teil ist nicht die Software, sondern dass die Mails auch ankommen.

Was bauen wir?

Wir betreiben listmonk 6.2.0 mit PostgreSQL 18.6 hinter Traefik. Am Ende hast du eine Empfängerliste mit Double Opt-in, eine fertig verschickte Kampagne und die Datenschutzschalter so gesetzt, dass Abonnenten ihre Daten selbst exportieren und löschen können.

Dazwischen liegt ein Schritt, den die meisten Anleitungen auslassen: Wir schicken die erste Kampagne an einen lokalen Auffang-Server, bevor eine einzige Mail nach draußen geht. Ein Newsletter, der beim ersten Versuch in den Spamordner rutscht, kostet dich Reputation, die du mühsam zurückgewinnen musst.

Die Software ist der leichte Teil

listmonk verschickt Mails – ob sie ankommen, entscheidet dein Mailserver. SPF, DKIM und DMARC müssen sauber stehen, bevor du echte Empfänger anschreibst. Wie das geht, steht in Mailserver-Zustellbarkeit; ohne diese Vorarbeit landet auch der schönste Newsletter im Spam.

Voraussetzungen

  • Traefik als Reverse Proxy mit funktionierendem Let’s-Encrypt-Resolver und einer Domain
  • Ein Mailserver oder SMTP-Zugang, dessen Absenderdomain per SPF, DKIM und DMARC abgesichert ist
  • Rund 500 MB Arbeitsspeicher für Anwendung und Datenbank
🍳 Empfehlung Anzeige

VPS 1000 G12.5

4 vCore · 8 GB RAM · 128 GB SSD

ab 14,50 €/Monat

listmonk ist sparsam – für ein paar tausend Adressen reicht der kleine VPS.

Zu netcup →

💶 1 Monat gratis:

Einmal einlösbar, gilt für VPS 1000 G12.5. Im Warenkorb einlösen →

💶 5,95 € Gutschein für netcup-Neukunden: immer gültig · nicht für Domains und VPS Lite

Alle netcup-Gutscheine →

Schritt für Schritt

Schritt 1: Der Stack

Terminal
mkdir -p /opt/listmonk/uploads && cd /opt/listmonk

Zugangsdaten zuerst. Benutzername und Passwort des Administrators werden beim allerersten Start aus diesen Variablen angelegt – danach ignoriert listmonk sie:

Terminal
umask 077
{
  echo "POSTGRES_PASSWORD=$(openssl rand -base64 24 | tr -d '=+/' | cut -c1-24)"
  echo "LISTMONK_ADMIN_USER=DEIN_BENUTZERNAME"
  echo "LISTMONK_ADMIN_PASSWORD=DEIN_STARKES_PASSWORT"
} > .env
YAML
# /opt/listmonk/compose.yaml
services:
  db:
    image: postgres:18.6-alpine
    container_name: listmonk-db
    restart: unless-stopped
    environment:
      POSTGRES_USER: listmonk
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: listmonk
    volumes:
      - pgdata:/var/lib/postgresql
    networks: [listmonk-intern]
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U listmonk"]
      interval: 10s
      timeout: 5s
      retries: 6

  app:
    image: listmonk/listmonk:v6.2.0
    container_name: listmonk
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
    command: ["sh", "-c", "./listmonk --install --idempotent --yes --config '' && ./listmonk --upgrade --yes --config '' && ./listmonk --config ''"]
    environment:
      LISTMONK_app__address: 0.0.0.0:9000
      LISTMONK_db__host: db
      LISTMONK_db__port: 5432
      LISTMONK_db__user: listmonk
      LISTMONK_db__password: ${POSTGRES_PASSWORD}
      LISTMONK_db__database: listmonk
      LISTMONK_db__ssl_mode: disable
      LISTMONK_ADMIN_USER: ${LISTMONK_ADMIN_USER}
      LISTMONK_ADMIN_PASSWORD: ${LISTMONK_ADMIN_PASSWORD}
      TZ: Europe/Berlin
    volumes:
      - ./uploads:/listmonk/uploads:rw
    networks: [listmonk-intern, proxy]
    labels:
      - traefik.enable=true
      - traefik.http.routers.listmonk.rule=Host(`DEINE_DOMAIN`)
      - traefik.http.routers.listmonk.entrypoints=websecure
      - traefik.http.routers.listmonk.tls.certresolver=le
      - traefik.http.services.listmonk.loadbalancer.server.port=9000

volumes:
  pgdata:

networks:
  listmonk-intern:
    internal: true
  proxy:
    external: true

Die command-Zeile sieht umständlich aus, macht aber genau drei sinnvolle Dinge nacheinander: --install --idempotent legt das Datenbankschema an, falls es fehlt, und tut sonst nichts; --upgrade fährt bei einem neuen Image die fälligen Migrationen; danach startet der eigentliche Dienst. Das --config '' ist kein Tippfehler, sondern die Anweisung „benutze keine Konfigurationsdatei, sondern nur die Umgebungsvariablen".

Die Datenbank liegt in einem eigenen Netz mit internal: true – sie ist damit weder von außen noch von anderen Containern im proxy-Netz erreichbar.

PostgreSQL 18: Der Mount hat sich geändert

Ältere Anleitungen hängen die Datenbank unter /var/lib/postgresql/**data** ein. Mit PostgreSQL 18 verweigert der Container dann den Start – auch bei leerem Volume, weil die Prüfung am Mount hängt und nicht am Inhalt. Der Mount gehört eine Ebene höher, auf /var/lib/postgresql.
Terminal
docker compose up -d
docker compose logs app | grep -E "superadmin|setup complete|upgrade|server started"
Ausgabe
install.go:93: creating superadmin user 'DEIN_BENUTZERNAME'
install.go:116: setup complete
main.go:165: running upgrade...
upgrade.go:74: no upgrades to run. Database is up to date.
⇨ http server started on [::]:9000

Die drei Stufen aus der command-Zeile sind gut zu erkennen: Installation, Upgrade-Prüfung, Dienststart.

Schritt 2: Anmelden, Adresse eintragen, Beispieldaten wegräumen

Unter https://DEINE_DOMAIN/admin meldest du dich mit den Zugangsdaten aus der .env an.

Das Dashboard von listmonk mit Kennzahlen zu Listen, Kampagnen und Abonnenten.
Das Dashboard nach der Anmeldung – die Zahlen stammen noch aus den Beispieldaten

Als Erstes gehst du auf Settings → General und trägst zwei Dinge ein:

  • Root URL – https://DEINE_DOMAIN, ohne Schrägstrich am Ende. Ab Werk steht dort http://localhost:9000, und aus diesem Wert baut listmonk jeden Link in jeder Mail: den Bestätigungslink des Double Opt-ins, den Abmeldelink, „View in browser". Bleibt der Standard stehen, kommen bei deinen Empfängern Links an, die ins Leere führen – und niemand kann seine Anmeldung bestätigen.
  • Default from email – der Absender, etwa Kochbrief <kochbrief@deine-domain.de>. Die Adresse muss zu der Domain passen, für die SPF, DKIM und DMARC eingerichtet sind.

Speichern, listmonk lädt sich danach kurz neu.

listmonk legt beim ersten Start außerdem Beispieldaten an: zwei Listen („Default list" und „Opt-in list"), zwei erfundene Abonnenten (john@example.com, anon@example.com) und eine Kampagne namens „Test campaign". Räum das weg, bevor du echte Adressen einträgst – sonst stehen die Beispieladressen irgendwann in deiner ersten echten Kampagne. Die Beispielabonnenten löschst du unter Subscribers, die Listen unter Lists, die Kampagne unter Campaigns.

Schritt 3: SMTP – erst auffangen, dann verschicken

Unter Settings → SMTP trägst du den Mailserver ein. Bevor du dort deine echten Zugangsdaten hinterlegst, lohnt ein Zwischenschritt: ein lokaler Auffang-Server, der alle Mails annimmt und keine weiterleitet. So siehst du, was listmonk tatsächlich verschickt, ohne Reputation zu verbrennen.

Ergänze dafür vorübergehend einen Container im Stack:

YAML
  mailpit:
    image: axllent/mailpit:v1.33.0
    container_name: listmonk-mailpit
    restart: unless-stopped
    networks: [listmonk-intern]

In den SMTP-Einstellungen zeigst du dann auf mailpit, Port 1025, Auth protocol: None und TLS: Off:

Die SMTP-Einstellungen von listmonk mit Host, Port, Authentifizierung, TLS und Wiederholungsversuchen.
Die SMTP-Seite – hier noch auf den lokalen Auffang-Server gerichtet

Für den echten Betrieb trägst du danach den Hostnamen deines Mailservers ein, Port 587, Auth protocol: PLAIN oder LOGIN, deine Zugangsdaten und TLS: STARTTLS. Die Verknüpfungen über den Feldern (Gmail, Amazon SES, Mailgun …) füllen die Felder für bekannte Anbieter vor.

Skip TLS verification bleibt aus

Für den lokalen Auffang-Server ist die Option belanglos, im echten Betrieb ist sie gefährlich: Ohne Zertifikatsprüfung merkst du nicht, wenn sich jemand zwischen listmonk und deinen Mailserver setzt – und über diese Verbindung gehen deine SMTP-Zugangsdaten. Schalte sie aus, sobald du auf den echten Server umstellst.

Schritt 4: Eine Liste mit Double Opt-in

Unter Lists → New legst du die Empfängerliste an. Zwei Einstellungen entscheiden über die rechtliche Seite:

  • Type: Private – die Liste taucht nicht in öffentlichen Anmeldeformularen auf, solange du das nicht willst. „Public" brauchst du für ein Anmeldeformular auf der eigenen Website.
  • Opt-in: Double – nach der Anmeldung geht eine Bestätigungsmail raus, und erst der Klick darin macht aus dem Eintrag einen bestätigten Abonnenten.

Double Opt-in ist in Deutschland faktisch Pflicht. Es ist zugleich dein Nachweis: listmonk speichert, wann jemand bestätigt hat. Ohne diesen Nachweis stehst du bei einer Beschwerde ohne Beleg da.

Schritt 5: Die erste Kampagne

Unter Campaigns → New legst du Name, Betreff, Liste und Format fest. Als Inhaltstyp bietet listmonk unter anderem einen Rich-Text-Editor und Markdown an – letzteres ist für einen schlichten Textnewsletter oft die bessere Wahl, weil weniger HTML weniger Spam-Verdacht bedeutet.

Im Text arbeitest du mit Platzhaltern:

Ausgabe
# Ausgabe 1

Hallo {{ .Subscriber.FirstName }},

diese Woche: Kürbissuppe, Zwiebelkuchen und ein Glas Federweißer.

{{ .Subscriber.FirstName }} wird beim Versand pro Empfänger ersetzt – eine Abonnentin namens „Leserin" bekommt „Hallo Leserin,".

Einen Abmeldelink musst du hier nicht einbauen: Die Vorlage, in die listmonk den Text steckt („Default campaign template", unter Campaigns → Templates), setzt in den Fuß jeder Mail „Unsubscribe" und „View in browser". Der individuelle Link dahinter heißt {{ UnsubscribeURL }}, und der gehört in jede Mail.

Verlass dich dabei nicht auf die Software: Baust du dir eine eigene Vorlage und vergisst den Link, speichert und verschickt listmonk sie trotzdem anstandslos. Der List-Unsubscribe-Header aus den Datenschutz-Einstellungen geht zwar mit, ersetzt den sichtbaren Abmeldelink im Text aber nicht. Schreib {{ UnsubscribeURL }} deshalb in jede eigene Vorlage, dann kannst du ihn nicht vergessen.

Ein Klick auf Start verschickt die Kampagne. Danach steht in der Übersicht, was passiert ist:

Die Kampagnenübersicht von listmonk mit einer abgeschlossenen Kampagne und der Statistik 1 von 1 versendet.
Nach dem Versand: Status Finished und die Zahl der zugestellten Nachrichten

Jetzt schaust du in den Auffang-Server: Stimmen Absender, Betreff samt Umlauten und der aufgelöste Platzhalter? Zeigen die Links im Fuß auf https://DEINE_DOMAIN/… und nicht auf localhost? Erst wenn das stimmt, stellst du SMTP auf den echten Mailserver um. Die Mails liegen in Mailpit, das nur im internen Netz hängt; abrufen kannst du sie über seine API, zum Beispiel die zuletzt eingegangene:

Terminal
docker compose exec app wget -qO- http://mailpit:8025/api/v1/message/latest

Danach nimmst du den mailpit-Block wieder aus der Compose-Datei und startest den Stack mit docker compose up -d --remove-orphans neu.

Die Detailansicht einer listmonk-Kampagne mit Betreff, Liste, Format und Inhalt.
Die Kampagnenansicht mit Betreff, Empfängerliste und Inhalt

Schritt 6: Die Datenschutz-Schalter

Unter Settings → Privacy steht das, was den Unterschied zwischen „selbst gehostet" und „datenschutzfreundlich" ausmacht. Jeder Schalter ist eine bewusste Entscheidung:

  • Disable tracking – schaltet die Zählung von Öffnungen und Klicks komplett ab. Ein Newsletter ohne Zählpixel braucht dafür keine Einwilligung und keinen Absatz in der Datenschutzerklärung.
  • Individual subscriber tracking – koppelt Öffnungen und Klicks an die einzelne Person. Ab Werk ist das aus: Dann zählt listmonk weiter, aber nur in der Summe. Das ist der brauchbare Mittelweg, wenn du wissen willst, ob eine Ausgabe ankommt.
  • Include List-Unsubscribe header – lass das an. Mailprogramme zeigen damit einen Abmeldeknopf direkt an, und große Anbieter werten das positiv.
  • Allow exporting und Allow wiping – Abonnenten können ihre gespeicherten Daten selbst abrufen und sich vollständig löschen. Das sind Auskunfts- und Löschanspruch, ohne dass du eine Mail beantworten musst. Beides ist ab Werk an.
  • Record opt-in IP address – speichert zusätzlich die IP-Adresse, von der aus jemand bestätigt hat. Das stärkt den Nachweis, ist aber ein weiteres personenbezogenes Datum; ab Werk aus.

Meine Empfehlung: Tracking pro Person aus, List-Unsubscribe an, Export und Löschung an – das ist zugleich der Auslieferungszustand, du musst also nur nichts verstellen. Damit beantwortest du die häufigsten Anfragen, bevor sie gestellt werden.

Wenn es nicht funktioniert

Der Container startet immer neu und meldet error loading flags: flag needs an argument: --config. Beim Kopieren der command-Zeile sind die beiden einfachen Anführungszeichen hinter --config verlorengegangen – das passiert leicht, wenn man den Befehl durch mehrere Shell-Ebenen schickt. Prüf mit docker inspect listmonk --format '{{json .Config.Cmd}}', was tatsächlich ankommt: Dort muss hinter jedem --config ein leeres Argument stehen. Schreib die Compose-Datei im Zweifel mit einem Editor direkt auf dem Server statt sie über verschachtelte Anführungszeichen zu erzeugen.

listmonk startet nicht mehr und meldet error reading SMTP config: decoding failed. In den gespeicherten Einstellungen steht ein ungültiger Wert – etwa eine Zeitangabe ohne Einheit. Das Tückische: Die Weboberfläche ist dann nicht erreichbar, du kannst es also nicht dort korrigieren. Die Einstellungen liegen in der Tabelle settings; mit docker compose exec db psql -U listmonk -d listmonk kommst du heran und kannst den Schlüssel smtp per UPDATE wieder auf einen gültigen Block setzen. Danach docker compose restart app.

Der Datenbank-Container startet nicht und redet von pg_ctlcluster. Das ist die PostgreSQL-18-Umstellung: Der Mount darf nicht mehr auf /var/lib/postgresql/data zeigen, sondern muss auf /var/lib/postgresql. Bei einer bestehenden Datenbank reicht das Verschieben nicht – dort brauchst du pg_dump mit der alten Hauptversion und einen Import in die neue.

Die Kampagne bleibt auf running stehen und es wird nichts versendet. Dann erreicht listmonk den SMTP-Server nicht. docker compose logs app | grep -i smtp zeigt den Grund; häufig ist es ein falscher Port (587 statt 465 oder umgekehrt), eine falsche TLS-Einstellung oder ein Provider, der Port 25 nach außen sperrt. Zum Eingrenzen hilft der lokale Auffang-Server aus Schritt 3: Funktioniert der Versand dorthin, liegt es nicht an listmonk.

Die Mails kommen an, landen aber im Spam. Das ist kein listmonk-Problem. Prüf, ob SPF, DKIM und DMARC für die Absenderdomain stimmen und ob die Absenderadresse zur Domain passt, über die du versendest – wie das geht, steht in Mailserver-Zustellbarkeit. Vermeide außerdem reines Bild-HTML und schick beim ersten Mal an eine kleine Liste, statt zehntausend Adressen auf einmal anzuschreiben.

Dein Problem ist nicht dabei? Alle Fehlermeldungen durchsuchen →

Wartung & Backups

Updates sind ein Tag-Wechsel; die Migration erledigt die --upgrade-Stufe im command beim Start automatisch:

Terminal
cd /opt/listmonk
docker compose exec -T db pg_dump -U listmonk listmonk > vor-update-$(date +%F).sql
sed -i 's/listmonk:v6.2.0/listmonk:vNEUE_VERSION/' compose.yaml
docker compose pull && docker compose up -d
docker compose logs app | grep -i upgrade

Der Dump vorher ist Pflicht: Wenn eine Migration schiefgeht, liegt deine komplette Empfängerliste darin. Und die PostgreSQL-Hauptversion nicht gleichzeitig anfassen – dafür ist immer Dump und Import fällig.

Sichern musst du zwei Dinge, plus die Zugangsdaten:

Ausgabe
pg_dump-Ausgabe            # Abonnenten, Listen, Kampagnen, Opt-in-Nachweise
/opt/listmonk/uploads/     # Bilder aus dem Medienbereich
/opt/listmonk/.env         # Datenbankpasswort

Der Dump ist hier besonders wichtig, weil darin die Bestätigungszeitpunkte der Double-Opt-ins stehen. Verlierst du die, hast du im Streitfall keinen Nachweis mehr, dass jemand zugestimmt hat. Täglich per systemd-Timer und ab ins Restic-Backup.

Laufende Pflege gibt es kaum, aber zwei Gewohnheiten lohnen sich: Nach jeder Kampagne einmal auf die Bounce-Zahl schauen und harte Bounces aus der Liste nehmen – tote Adressen kosten Reputation. Und die Liste nicht künstlich am Leben halten: Wer sich abmeldet, wird gelöscht, nicht „für später" aufbewahrt.

Realistischer Aufwand: Das Aufsetzen kostet einen Abend, der laufende Betrieb ein paar Minuten pro Ausgabe. Der Teil, der wirklich Aufmerksamkeit braucht, ist die Zustellbarkeit – und die entscheidet sich vor dem ersten Versand, nicht danach.

Feedback per E-Mail: feedback@serverkueche.de

Das könnte dir auch schmecken