<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Listmonk – Serverküche</title><link>https://serverkueche.de/tags/listmonk/</link><description>Listmonk – Neueste Beiträge von Serverküche</description><generator>Hugo</generator><language>de-DE</language><managingEditor>feedback@serverkueche.de (Serverküche)</managingEditor><webMaster>feedback@serverkueche.de (Serverküche)</webMaster><copyright>2026 Serverküche</copyright><lastBuildDate>Thu, 05 Nov 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://serverkueche.de/tags/listmonk/index.xml" rel="self" type="application/rss+xml"/><item><title>listmonk: der eigene Newsletter-Server – DSGVO-freundlich</title><link>https://serverkueche.de/tutorials/listmonk-newsletter/</link><pubDate>Thu, 05 Nov 2026 00:00:00 +0000</pubDate><author>feedback@serverkueche.de (Serverküche)</author><guid>https://serverkueche.de/tutorials/listmonk-newsletter/</guid><description>listmonk 6.2 mit Postgres 18 hinter Traefik: Liste mit Double Opt-in, erste Kampagne, Datenschutz-Schalter – und ein Testlauf, bevor echte Mails rausgehen.</description><content:encoded><![CDATA[<p>Newsletter selbst verschicken heißt: Die Adressliste liegt bei dir, nicht bei einem Dienstleister,
der sie „zur Verbesserung des Angebots&quot; auswertet. listmonk macht das erstaunlich bequem – der
schwierige Teil ist nicht die Software, sondern dass die Mails auch ankommen.</p>
<h2 id="was-bauen-wir">Was bauen wir?</h2>
<p>Wir betreiben <strong>listmonk 6.2.0</strong> mit <strong>PostgreSQL 18.6</strong> hinter Traefik. Am Ende hast du eine
Empfängerliste mit <strong>Double Opt-in</strong>, eine fertig verschickte Kampagne und die Datenschutzschalter
so gesetzt, dass Abonnenten ihre Daten selbst exportieren und löschen können.</p>
<p>Dazwischen liegt ein Schritt, den die meisten Anleitungen auslassen: Wir schicken die erste
Kampagne an einen <strong>lokalen Auffang-Server</strong>, 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.</p>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-amber-400 bg-amber-50 dark:border-amber-700 dark:bg-amber-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">⚠️</span>Die Software ist der leichte Teil
  </p>
  <div class="prose-kitchen text-sm">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
<a href="/tutorials/mailserver-zustellbarkeit/">Mailserver-Zustellbarkeit</a>; ohne diese Vorarbeit landet
auch der schönste Newsletter im Spam.</div>
</div>
<h2 id="voraussetzungen">Voraussetzungen</h2>
<ul>
<li><a href="/tutorials/reverse-proxy-traefik/">Traefik als Reverse Proxy</a> mit funktionierendem
Let&rsquo;s-Encrypt-Resolver und einer Domain</li>
<li>Ein <strong>Mailserver oder SMTP-Zugang</strong>, dessen Absenderdomain per
<a href="/tutorials/mailserver-zustellbarkeit/">SPF, DKIM und DMARC</a> abgesichert ist</li>
<li>Rund 500 MB Arbeitsspeicher für Anwendung und Datenbank</li>
</ul>
<div class="not-prose my-6 overflow-hidden rounded-xl border border-paprika-200 bg-paprika-50 dark:border-paprika-800 dark:bg-paprika-900/20"
     data-track-content data-content-name="Affiliate-Box · /tutorials/listmonk-newsletter/" data-content-piece="VPS 1000 G12.5">
  <div class="flex items-center justify-between border-b border-paprika-200 bg-paprika-100 px-4 py-1.5 text-xs font-semibold uppercase tracking-wide text-paprika-700 dark:border-paprika-800 dark:bg-paprika-900/40 dark:text-paprika-300">
    <span>🍳 Empfehlung</span>
    <span title="Mit * markierte Links sind Affiliate-Links.">Anzeige</span>
  </div>
  <div class="flex flex-col gap-4 p-4 sm:flex-row sm:items-center sm:justify-between">
    <div>
      <p class="text-lg font-bold text-slate-900 dark:text-white">VPS 1000 G12.5</p>
      <p class="mt-1 text-sm text-slate-600 dark:text-slate-300">4 vCore · 8 GB RAM · 128 GB SSD</p>
      <p class="mt-1 text-sm font-semibold text-paprika-700 dark:text-paprika-400">ab 14,50 €/Monat</p>
      <p class="mt-2 text-sm text-slate-600 dark:text-slate-400">listmonk ist sparsam – für ein paar tausend Adressen reicht der kleine VPS.</p>
    </div>
    <a href="https://www.netcup.com/de/server/vps/vps-1000-g12.5-24m-eu?ref=44083#vps-1000-g12.5-12m-eu" rel="sponsored noopener" target="_blank"
   data-track-event="Affiliate|netcup: Affiliate-Box|VPS 1000 G12.5 · {page}"
   class="inline-flex shrink-0 items-center justify-center rounded-lg bg-paprika-600 px-5 py-2.5 font-semibold text-white transition-colors hover:bg-paprika-700">
  Zu netcup →
</a>

  </div><div class="px-4 pb-4"><div class="not-prose my-3 rounded-lg border border-herb-500/40 bg-herb-50 px-3 py-2 text-sm text-slate-700 dark:bg-herb-900/20 dark:text-slate-200"
     data-voucher-api="vps-1000" data-voucher-msg-empty="Gerade ist für diesen Tarif kein Code frei – nimm den 5,95-€-Code." data-voucher-msg-limit="Für heute sind keine weiteren Codes drin – nimm den 5,95-€-Code oder versuch es morgen." data-voucher-msg-error="Der Code ließ sich gerade nicht abrufen – nimm den 5,95-€-Code."
     data-voucher-hint="Warenkorb vorher leeren – der Code legt den Server selbst hinein.">
  <p class="flex flex-wrap items-center gap-x-2 gap-y-1"><span>💶 <span class="font-semibold">1 Monat gratis</span>:</span>
    <button type="button" data-voucher-reveal data-copy-title="Zum Kopieren klicken"
            class="cursor-pointer rounded bg-white px-2 py-0.5 font-mono text-sm font-semibold text-herb-800 hover:ring-2 hover:ring-herb-500/40 dark:bg-slate-800 dark:text-herb-400">Code anzeigen</button>
    <button type="button" data-voucher-next hidden
            class="rounded border border-herb-500/40 px-2 py-0.5 text-xs text-herb-800 hover:bg-white dark:text-herb-400 dark:hover:bg-slate-800">anderer Code</button>
  </p><p class="mt-1 text-xs text-slate-500 dark:text-slate-400" data-voucher-hint-target>
    Einmal einlösbar, gilt für VPS 1000 G12.5.
    <a href="https://www.netcup.com/de/checkout/warenkorb?ref=44083" rel="sponsored noopener" target="_blank"
       data-track-event="Affiliate|netcup: Gutschein einlösen|VPS 1000 G12.5 · {page}"
       class="font-medium text-herb-800 underline underline-offset-2 hover:text-herb-900 dark:text-herb-400">Im Warenkorb einlösen →</a>
  </p><p class="mt-2 flex flex-wrap items-center gap-x-2 gap-y-1 border-t border-herb-500/30 pt-2">
    <span>💶 <span class="font-semibold">5,95 € Gutschein</span> für netcup-Neukunden:</span>
    <button type="button" data-voucher-code data-track-voucher="36nc17844976032"
            title="Zum Kopieren klicken" class="cursor-pointer rounded bg-white px-2 py-0.5 font-mono text-sm font-semibold text-herb-800 hover:ring-2 hover:ring-herb-500/40 dark:bg-slate-800 dark:text-herb-400">36nc17844976032</button>
    <span class="text-xs text-slate-500 dark:text-slate-400">immer gültig · nicht für Domains und VPS Lite</span>
  </p><p class="mt-1 text-xs"><a href="/netcup-gutschein/" class="font-medium text-herb-800 underline underline-offset-2 hover:text-herb-900 dark:text-herb-400">Alle netcup-Gutscheine →</a></p>
</div></div>
</div>

<h2 id="schritt-für-schritt">Schritt für Schritt</h2>
<h3 id="schritt-1-der-stack">Schritt 1: Der Stack</h3>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mkdir -p /opt/listmonk/uploads <span class="o">&amp;&amp;</span> <span class="nb">cd</span> /opt/listmonk</span></span></code></pre></div>
</div>
<p>Zugangsdaten zuerst. Benutzername und Passwort des Administrators werden beim <strong>allerersten</strong> Start
aus diesen Variablen angelegt – danach ignoriert listmonk sie:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">umask</span> <span class="m">077</span>
</span></span><span class="line"><span class="cl"><span class="o">{</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;POSTGRES_PASSWORD=</span><span class="k">$(</span>openssl rand -base64 <span class="m">24</span> <span class="p">|</span> tr -d <span class="s1">&#39;=+/&#39;</span> <span class="p">|</span> cut -c1-24<span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;LISTMONK_ADMIN_USER=DEIN_BENUTZERNAME&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;LISTMONK_ADMIN_PASSWORD=DEIN_STARKES_PASSWORT&#34;</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span> &gt; .env</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">YAML</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># /opt/listmonk/compose.yaml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">db</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">postgres:18.6-alpine</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">listmonk-db</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">POSTGRES_USER</span><span class="p">:</span><span class="w"> </span><span class="l">listmonk</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">POSTGRES_PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="l">${POSTGRES_PASSWORD}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">POSTGRES_DB</span><span class="p">:</span><span class="w"> </span><span class="l">listmonk</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">pgdata:/var/lib/postgresql</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">networks</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">listmonk-intern]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">healthcheck</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">test</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;CMD-SHELL&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;pg_isready -U listmonk&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">interval</span><span class="p">:</span><span class="w"> </span><span class="l">10s</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">timeout</span><span class="p">:</span><span class="w"> </span><span class="l">5s</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">retries</span><span class="p">:</span><span class="w"> </span><span class="m">6</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">app</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">listmonk/listmonk:v6.2.0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">listmonk</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">depends_on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">db</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">condition</span><span class="p">:</span><span class="w"> </span><span class="l">service_healthy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;sh&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;-c&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;./listmonk --install --idempotent --yes --config &#39;&#39; &amp;&amp; ./listmonk --upgrade --yes --config &#39;&#39; &amp;&amp; ./listmonk --config &#39;&#39;&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">LISTMONK_app__address</span><span class="p">:</span><span class="w"> </span><span class="m">0.0.0.0</span><span class="p">:</span><span class="m">9000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">LISTMONK_db__host</span><span class="p">:</span><span class="w"> </span><span class="l">db</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">LISTMONK_db__port</span><span class="p">:</span><span class="w"> </span><span class="m">5432</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">LISTMONK_db__user</span><span class="p">:</span><span class="w"> </span><span class="l">listmonk</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">LISTMONK_db__password</span><span class="p">:</span><span class="w"> </span><span class="l">${POSTGRES_PASSWORD}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">LISTMONK_db__database</span><span class="p">:</span><span class="w"> </span><span class="l">listmonk</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">LISTMONK_db__ssl_mode</span><span class="p">:</span><span class="w"> </span><span class="l">disable</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">LISTMONK_ADMIN_USER</span><span class="p">:</span><span class="w"> </span><span class="l">${LISTMONK_ADMIN_USER}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">LISTMONK_ADMIN_PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="l">${LISTMONK_ADMIN_PASSWORD}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">TZ</span><span class="p">:</span><span class="w"> </span><span class="l">Europe/Berlin</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">./uploads:/listmonk/uploads:rw</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">networks</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">listmonk-intern, proxy]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">traefik.enable=true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">traefik.http.routers.listmonk.rule=Host(`DEINE_DOMAIN`)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">traefik.http.routers.listmonk.entrypoints=websecure</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">traefik.http.routers.listmonk.tls.certresolver=le</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">traefik.http.services.listmonk.loadbalancer.server.port=9000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">pgdata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">networks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">listmonk-intern</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">internal</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">proxy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">external</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span></span></span></code></pre></div>
</div>
<p>Die <code>command</code>-Zeile sieht umständlich aus, macht aber genau drei sinnvolle Dinge nacheinander:
<code>--install --idempotent</code> legt das Datenbankschema an, falls es fehlt, und tut sonst nichts;
<code>--upgrade</code> fährt bei einem neuen Image die fälligen Migrationen; danach startet der eigentliche
Dienst. Das <code>--config ''</code> ist kein Tippfehler, sondern die Anweisung „benutze <strong>keine</strong>
Konfigurationsdatei, sondern nur die Umgebungsvariablen&quot;.</p>
<p>Die Datenbank liegt in einem eigenen Netz mit <code>internal: true</code> – sie ist damit weder von außen noch
von anderen Containern im <code>proxy</code>-Netz erreichbar.</p>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-amber-400 bg-amber-50 dark:border-amber-700 dark:bg-amber-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">⚠️</span>PostgreSQL 18: Der Mount hat sich geändert
  </p>
  <div class="prose-kitchen text-sm">Ältere Anleitungen hängen die Datenbank unter <code>/var/lib/postgresql/**data**</code> 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 <code>/var/lib/postgresql</code>.</div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker compose up -d
</span></span><span class="line"><span class="cl">docker compose logs app <span class="p">|</span> grep -E <span class="s2">&#34;superadmin|setup complete|upgrade|server started&#34;</span></span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">install.go:93: creating superadmin user &#39;DEIN_BENUTZERNAME&#39;
</span></span><span class="line"><span class="cl">install.go:116: setup complete
</span></span><span class="line"><span class="cl">main.go:165: running upgrade...
</span></span><span class="line"><span class="cl">upgrade.go:74: no upgrades to run. Database is up to date.
</span></span><span class="line"><span class="cl">⇨ http server started on [::]:9000</span></span></code></pre></div>
</div>
<p>Die drei Stufen aus der <code>command</code>-Zeile sind gut zu erkennen: Installation, Upgrade-Prüfung,
Dienststart.</p>
<h3 id="schritt-2-anmelden-adresse-eintragen-beispieldaten-wegräumen">Schritt 2: Anmelden, Adresse eintragen, Beispieldaten wegräumen</h3>
<p>Unter <code>https://DEINE_DOMAIN/admin</code> meldest du dich mit den Zugangsdaten aus der <code>.env</code> an.</p>
<p><figure class="my-6"><img src="/tutorials/listmonk-newsletter/lm-dashboard_hu_c764e81285d97366.webp" srcset="/tutorials/listmonk-newsletter/lm-dashboard_hu_b35f9321b517e94f.webp 480w, /tutorials/listmonk-newsletter/lm-dashboard_hu_c764e81285d97366.webp 768w, /tutorials/listmonk-newsletter/lm-dashboard_hu_a502cd8bde9b4632.webp 1200w, /tutorials/listmonk-newsletter/lm-dashboard_hu_761460a3479f89f9.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/tutorials/listmonk-newsletter/lm-dashboard_hu_2d505ad442f24ee2.webp"
    alt="Das Dashboard von listmonk mit Kennzahlen zu Listen, Kampagnen und Abonnenten." title="Das Dashboard nach der Anmeldung – die Zahlen stammen noch aus den Beispieldaten"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Das Dashboard nach der Anmeldung – die Zahlen stammen noch aus den Beispieldaten</figcaption></figure></p>
<p>Als Erstes gehst du auf <strong>Settings → General</strong> und trägst zwei Dinge ein:</p>
<ul>
<li><strong>Root URL</strong> – <code>https://DEINE_DOMAIN</code>, ohne Schrägstrich am Ende. Ab Werk steht dort
<code>http://localhost:9000</code>, und aus diesem Wert baut listmonk <strong>jeden Link in jeder Mail</strong>: den
Bestätigungslink des Double Opt-ins, den Abmeldelink, „View in browser&quot;. Bleibt der Standard
stehen, kommen bei deinen Empfängern Links an, die ins Leere führen – und niemand kann seine
Anmeldung bestätigen.</li>
<li><strong>Default <code>from</code> email</strong> – der Absender, etwa <code>Kochbrief &lt;kochbrief@deine-domain.de&gt;</code>. Die
Adresse muss zu der Domain passen, für die SPF, DKIM und DMARC eingerichtet sind.</li>
</ul>
<p>Speichern, listmonk lädt sich danach kurz neu.</p>
<p>listmonk legt beim ersten Start außerdem <strong>Beispieldaten</strong> an: zwei Listen („Default list&quot; und
„Opt-in list&quot;), zwei erfundene Abonnenten (<code>john@example.com</code>, <code>anon@example.com</code>) und eine
Kampagne namens „Test campaign&quot;. 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
<strong>Subscribers</strong>, die Listen unter <strong>Lists</strong>, die Kampagne unter <strong>Campaigns</strong>.</p>
<h3 id="schritt-3-smtp--erst-auffangen-dann-verschicken">Schritt 3: SMTP – erst auffangen, dann verschicken</h3>
<p>Unter <strong>Settings → SMTP</strong> trägst du den Mailserver ein. Bevor du dort deine echten Zugangsdaten
hinterlegst, lohnt ein Zwischenschritt: ein <strong>lokaler Auffang-Server</strong>, der alle Mails annimmt und
keine weiterleitet. So siehst du, was listmonk tatsächlich verschickt, ohne Reputation zu
verbrennen.</p>
<p>Ergänze dafür vorübergehend einen Container im Stack:</p>
<div class="sk-code">
  <span class="sk-code-head">YAML</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="w">  </span><span class="nt">mailpit</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">axllent/mailpit:v1.33.0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">listmonk-mailpit</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">networks</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">listmonk-intern]</span></span></span></code></pre></div>
</div>
<p>In den SMTP-Einstellungen zeigst du dann auf <code>mailpit</code>, Port <code>1025</code>, <strong>Auth protocol: None</strong> und
<strong>TLS: Off</strong>:</p>
<p><figure class="my-6"><img src="/tutorials/listmonk-newsletter/lm-smtp_hu_978217d4fdaa64b8.webp" srcset="/tutorials/listmonk-newsletter/lm-smtp_hu_dee5ae0a80cd270.webp 480w, /tutorials/listmonk-newsletter/lm-smtp_hu_978217d4fdaa64b8.webp 768w, /tutorials/listmonk-newsletter/lm-smtp_hu_cf61865eb42e8e0.webp 1200w, /tutorials/listmonk-newsletter/lm-smtp_hu_a32ddd7f858c283d.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/tutorials/listmonk-newsletter/lm-smtp_hu_1a286c6aa4382063.webp"
    alt="Die SMTP-Einstellungen von listmonk mit Host, Port, Authentifizierung, TLS und Wiederholungsversuchen." title="Die SMTP-Seite – hier noch auf den lokalen Auffang-Server gerichtet"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Die SMTP-Seite – hier noch auf den lokalen Auffang-Server gerichtet</figcaption></figure></p>
<p>Für den echten Betrieb trägst du danach den Hostnamen deines Mailservers ein, Port <strong>587</strong>, <strong>Auth
protocol: PLAIN</strong> oder <strong>LOGIN</strong>, deine Zugangsdaten und <strong>TLS: STARTTLS</strong>. Die Verknüpfungen über
den Feldern (Gmail, Amazon SES, Mailgun …) füllen die Felder für bekannte Anbieter vor.</p>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-paprika-400 bg-paprika-50 dark:border-paprika-700 dark:bg-paprika-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">🔥</span>Skip TLS verification bleibt aus
  </p>
  <div class="prose-kitchen text-sm">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.</div>
</div>
<h3 id="schritt-4-eine-liste-mit-double-opt-in">Schritt 4: Eine Liste mit Double Opt-in</h3>
<p>Unter <strong>Lists → New</strong> legst du die Empfängerliste an. Zwei Einstellungen entscheiden über die
rechtliche Seite:</p>
<ul>
<li><strong>Type: Private</strong> – die Liste taucht nicht in öffentlichen Anmeldeformularen auf, solange du das
nicht willst. „Public&quot; brauchst du für ein Anmeldeformular auf der eigenen Website.</li>
<li><strong>Opt-in: Double</strong> – nach der Anmeldung geht eine Bestätigungsmail raus, und erst der Klick darin
macht aus dem Eintrag einen bestätigten Abonnenten.</li>
</ul>
<p><strong>Double Opt-in ist in Deutschland faktisch Pflicht.</strong> Es ist zugleich dein Nachweis: listmonk
speichert, wann jemand bestätigt hat. Ohne diesen Nachweis stehst du bei einer Beschwerde ohne
Beleg da.</p>
<h3 id="schritt-5-die-erste-kampagne">Schritt 5: Die erste Kampagne</h3>
<p>Unter <strong>Campaigns → New</strong> legst du Name, Betreff, Liste und Format fest. Als Inhaltstyp bietet
listmonk unter anderem einen Rich-Text-Editor und <strong>Markdown</strong> an – letzteres ist für einen
schlichten Textnewsletter oft die bessere Wahl, weil weniger HTML weniger Spam-Verdacht bedeutet.</p>
<p>Im Text arbeitest du mit Platzhaltern:</p>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl"># Ausgabe 1
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Hallo {{ .Subscriber.FirstName }},
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">diese Woche: Kürbissuppe, Zwiebelkuchen und ein Glas Federweißer.</span></span></code></pre></div>
</div>
<p><code>{{ .Subscriber.FirstName }}</code> wird beim Versand pro Empfänger ersetzt – eine Abonnentin namens
„Leserin&quot; bekommt „Hallo Leserin,&quot;.</p>
<p>Einen Abmeldelink musst du hier nicht einbauen: Die Vorlage, in die listmonk den Text steckt
(„Default campaign template&quot;, unter <strong>Campaigns → Templates</strong>), setzt in den Fuß jeder Mail
„Unsubscribe&quot; und „View in browser&quot;. Der individuelle Link dahinter heißt <code>{{ UnsubscribeURL }}</code>,
und der <strong>gehört in jede Mail</strong>.</p>
<p>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 <code>List-Unsubscribe</code>-Header aus den
Datenschutz-Einstellungen geht zwar mit, ersetzt den sichtbaren Abmeldelink im Text aber nicht.
Schreib <code>{{ UnsubscribeURL }}</code> deshalb in jede eigene Vorlage, dann kannst du ihn nicht vergessen.</p>
<p>Ein Klick auf <strong>Start</strong> verschickt die Kampagne. Danach steht in der Übersicht, was passiert ist:</p>
<p><figure class="my-6"><img src="/tutorials/listmonk-newsletter/lm-campaigns-list_hu_999f25f0e4ba1d45.webp" srcset="/tutorials/listmonk-newsletter/lm-campaigns-list_hu_d1eec6d53c37c116.webp 480w, /tutorials/listmonk-newsletter/lm-campaigns-list_hu_999f25f0e4ba1d45.webp 768w, /tutorials/listmonk-newsletter/lm-campaigns-list_hu_52b1b9c1c25ad6df.webp 1200w, /tutorials/listmonk-newsletter/lm-campaigns-list_hu_3bdf96d7832c9d99.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/tutorials/listmonk-newsletter/lm-campaigns-list_hu_62abf4ca69b446ae.webp"
    alt="Die Kampagnenübersicht von listmonk mit einer abgeschlossenen Kampagne und der Statistik 1 von 1 versendet." title="Nach dem Versand: Status Finished und die Zahl der zugestellten Nachrichten"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Nach dem Versand: Status Finished und die Zahl der zugestellten Nachrichten</figcaption></figure></p>
<p>Jetzt schaust du in den Auffang-Server: Stimmen Absender, Betreff samt Umlauten und der aufgelöste
Platzhalter? Zeigen die Links im Fuß auf <code>https://DEINE_DOMAIN/…</code> und nicht auf <code>localhost</code>? 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:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker compose <span class="nb">exec</span> app wget -qO- http://mailpit:8025/api/v1/message/latest</span></span></code></pre></div>
</div>
<p>Danach nimmst du den <code>mailpit</code>-Block wieder aus der Compose-Datei und startest den Stack mit
<code>docker compose up -d --remove-orphans</code> neu.</p>
<p><figure class="my-6"><img src="/tutorials/listmonk-newsletter/lm-campaign_hu_b3e394da066d9efb.webp" srcset="/tutorials/listmonk-newsletter/lm-campaign_hu_8702d1da383d72a6.webp 480w, /tutorials/listmonk-newsletter/lm-campaign_hu_b3e394da066d9efb.webp 768w, /tutorials/listmonk-newsletter/lm-campaign_hu_8484ccd18b0d7273.webp 1200w, /tutorials/listmonk-newsletter/lm-campaign_hu_a6eff9cd9b0afe0c.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/tutorials/listmonk-newsletter/lm-campaign_hu_b7b8f563d4584dab.webp"
    alt="Die Detailansicht einer listmonk-Kampagne mit Betreff, Liste, Format und Inhalt." title="Die Kampagnenansicht mit Betreff, Empfängerliste und Inhalt"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Die Kampagnenansicht mit Betreff, Empfängerliste und Inhalt</figcaption></figure></p>
<h3 id="schritt-6-die-datenschutz-schalter">Schritt 6: Die Datenschutz-Schalter</h3>
<p>Unter <strong>Settings → Privacy</strong> steht das, was den Unterschied zwischen „selbst gehostet&quot; und
„datenschutzfreundlich&quot; ausmacht. Jeder Schalter ist eine bewusste Entscheidung:</p>
<ul>
<li><strong>Disable tracking</strong> – 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.</li>
<li><strong>Individual subscriber tracking</strong> – 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.</li>
<li><strong>Include <code>List-Unsubscribe</code> header</strong> – lass das an. Mailprogramme zeigen damit einen
Abmeldeknopf direkt an, und große Anbieter werten das positiv.</li>
<li><strong>Allow exporting</strong> und <strong>Allow wiping</strong> – 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.</li>
<li><strong>Record opt-in IP address</strong> – 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.</li>
</ul>
<p>Meine Empfehlung: Tracking pro Person aus, <code>List-Unsubscribe</code> 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.</p>
<h2 id="wenn-es-nicht-funktioniert">Wenn es nicht funktioniert</h2>
<div class="troubleshoot not-prose">
<p><strong>Der Container startet immer neu und meldet <code>error loading flags: flag needs an argument: --config</code>.</strong> Beim Kopieren der <code>command</code>-Zeile sind die beiden einfachen Anführungszeichen hinter <code>--config</code> verlorengegangen – das passiert leicht, wenn man den Befehl durch mehrere Shell-Ebenen schickt. Prüf mit <code>docker inspect listmonk --format '{{json .Config.Cmd}}'</code>, was tatsächlich ankommt: Dort muss hinter jedem <code>--config</code> 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.</p>
<p><strong>listmonk startet nicht mehr und meldet <code>error reading SMTP config: decoding failed</code>.</strong> 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 <code>settings</code>; mit <code>docker compose exec db psql -U listmonk -d listmonk</code> kommst du heran und kannst den Schlüssel <code>smtp</code> per <code>UPDATE</code> wieder auf einen gültigen Block setzen. Danach <code>docker compose restart app</code>.</p>
<p><strong>Der Datenbank-Container startet nicht und redet von <code>pg_ctlcluster</code>.</strong> Das ist die PostgreSQL-18-Umstellung: Der Mount darf nicht mehr auf <code>/var/lib/postgresql/data</code> zeigen, sondern muss auf <code>/var/lib/postgresql</code>. Bei einer bestehenden Datenbank reicht das Verschieben nicht – dort brauchst du <code>pg_dump</code> mit der alten Hauptversion und einen Import in die neue.</p>
<p><strong>Die Kampagne bleibt auf <code>running</code> stehen und es wird nichts versendet.</strong> Dann erreicht listmonk den SMTP-Server nicht. <code>docker compose logs app | grep -i smtp</code> 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.</p>
<p><strong>Die Mails kommen an, landen aber im Spam.</strong> 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 <a href="/tutorials/mailserver-zustellbarkeit/">Mailserver-Zustellbarkeit</a>. Vermeide außerdem reines Bild-HTML und schick beim ersten Mal an eine kleine Liste, statt zehntausend Adressen auf einmal anzuschreiben.</p>

</div>
<p class="mt-3 text-sm text-slate-500 dark:text-slate-400">
  Dein Problem ist nicht dabei?
  <a href="/fehler/" class="font-medium text-paprika-700 hover:underline dark:text-paprika-400">Alle Fehlermeldungen durchsuchen →</a>
</p>

<h2 id="wartung--backups">Wartung &amp; Backups</h2>
<p><strong>Updates</strong> sind ein Tag-Wechsel; die Migration erledigt die <code>--upgrade</code>-Stufe im <code>command</code> beim
Start automatisch:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">cd</span> /opt/listmonk
</span></span><span class="line"><span class="cl">docker compose <span class="nb">exec</span> -T db pg_dump -U listmonk listmonk &gt; vor-update-<span class="k">$(</span>date +%F<span class="k">)</span>.sql
</span></span><span class="line"><span class="cl">sed -i <span class="s1">&#39;s/listmonk:v6.2.0/listmonk:vNEUE_VERSION/&#39;</span> compose.yaml
</span></span><span class="line"><span class="cl">docker compose pull <span class="o">&amp;&amp;</span> docker compose up -d
</span></span><span class="line"><span class="cl">docker compose logs app <span class="p">|</span> grep -i upgrade</span></span></code></pre></div>
</div>
<p>Der Dump vorher ist Pflicht: Wenn eine Migration schiefgeht, liegt deine komplette Empfängerliste
darin. Und <strong>die PostgreSQL-Hauptversion nicht gleichzeitig anfassen</strong> – dafür ist immer Dump und
Import fällig.</p>
<p><strong>Sichern</strong> musst du zwei Dinge, plus die Zugangsdaten:</p>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">pg_dump-Ausgabe            # Abonnenten, Listen, Kampagnen, Opt-in-Nachweise
</span></span><span class="line"><span class="cl">/opt/listmonk/uploads/     # Bilder aus dem Medienbereich
</span></span><span class="line"><span class="cl">/opt/listmonk/.env         # Datenbankpasswort</span></span></code></pre></div>
</div>
<p>Der Dump ist hier besonders wichtig, weil darin die <strong>Bestätigungszeitpunkte</strong> 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 <a href="/tutorials/backups-mit-restic/">Restic-Backup</a>.</p>
<p><strong>Laufende Pflege</strong> 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&quot;
aufbewahrt.</p>
<p><strong>Realistischer Aufwand:</strong> 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.</p>
]]></content:encoded></item></channel></rss>