How to Send a Newsletter with Listmonk (Self-Hosted Mailing Lists with Subscribe Forms and Campaigns)

A newsletter you own: Listmonk. Double opt-in forms, Markdown campaigns, scheduling, opens and clicks, bounces handled — sent through a relay with a reputation.

A monthly email to customers, a newsletter for the blog (110), a club's updates. Mailchimp charges by subscriber and owns the list. Listmonk is a fast, single-binary mailing-list manager: lists, subscribers with attributes, double opt-in forms, campaigns written in Markdown or HTML with templates, scheduling, open and click tracking, bounce processing and one-click unsubscribe. It sends through any SMTP — and that's the part to get right: a relay with a reputation (138's lesson), not your home connection.

Quick reference

TaskWhat
Folder~/docker/listmonk/compose.yaml (app + PostgreSQL)
Webhttps://news.example.com/admin
SendingSettings → SMTP: Amazon SES, Postmark, Mailgun, Brevo, or your Mailcow relay
ListLists → New (double opt-in) → public subscribe form at /subscription/form
CampaignCampaigns → New → Markdown → preview → send test → schedule
DeliverabilitySPF/DKIM/DMARC for the sending domain (138), List-Unsubscribe headers (built in)

1. The compose file

mkdir -p ~/docker/listmonk/{uploads} && cd ~/docker/listmonk
nano compose.yaml

services:
listmonk:
image: listmonk/listmonk:latest
container_name: listmonk
ports:
- "127.0.0.1:9100:9000"
environment:
TZ: Europe/London
LISTMONK_app__address: 0.0.0.0:9000
LISTMONK_app__root_url: https://news.example.com
LISTMONK_db__host: listmonk-db
LISTMONK_db__port: 5432
LISTMONK_db__user: listmonk
LISTMONK_db__password: ${DB_PASSWORD}
LISTMONK_db__database: listmonk
LISTMONK_db__ssl_mode: disable
volumes:
- ./uploads:/listmonk/uploads
depends_on: [listmonk-db]
restart: unless-stopped

listmonk-db:
image: postgres:16-alpine
container_name: listmonk-db
environment:
POSTGRES_USER: listmonk
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: listmonk
volumes:
- ./db:/var/lib/postgresql/data
restart: unless-stopped

First run installs the schema and creates the admin:

docker compose run --rm listmonk ./listmonk --install --yes
docker compose up -d

The install prints (or lets you set) the admin username/password; recent versions ask you to set it via LISTMONK_ADMIN_USER/LISTMONK_ADMIN_PASSWORD for the install step.

2. Proxy

news.example.com → 127.0.0.1:9100 (28), HTTPS, public — subscribe forms, unsubscribe links and tracked links all point here. root_url must match.

3. Sending: a relay, not your house

Settings → SMTP: one of

Host, port 587, STARTTLS, credentials, max connections 2–4, messages per batch per the provider's limits. Test connection. Then the sending domain's DNS: SPF including the relay, the relay's DKIM record, DMARC — the same green checks as 138, done for example.com at the relay's instructions page. Without these, the newsletter is spam.

4. Lists and the subscribe form

Lists → New: "Monthly newsletter", double opt-in (they confirm by email — required for GDPR-grade consent and far better deliverability). Forms (sidebar) generates the HTML for a subscribe box: paste it into the Hugo site (110) or link to https://news.example.com/subscription/form. Import existing subscribers (CSV) with Import → "they've consented" only if they genuinely did.

Subscribers carry attributes ({"city": "Leeds", "customer": true}) for segmentation: a query like subscribers.attribs->>'customer' = 'true' targets a campaign.

5. A campaign

Campaigns → New: name, subject, from John , lists, template (the default is clean; Templates to brand it with your logo and colours), format Markdown — write it. {{ .Subscriber.FirstName }} personalises; {{ TrackLink "https://…" }} tracks a click (or the auto-track option). Preview, Send test to yourself, then Schedule for Tuesday 10 am or Start. The campaign page shows sent/opens/clicks/bounces live; Analytics compares campaigns.

Each email carries a one-click unsubscribe link and the List-Unsubscribe headers Gmail and Yahoo now require from bulk senders.

6. Bounces and hygiene

Settings → Bounces: enable, and connect the relay's bounce mechanism (SES via SNS webhook, Postmark/Mailgun webhooks, or POP3 for a plain mailbox). Hard bounces unsubscribe automatically; keep sending to dead addresses and the relay's reputation — and yours — drops. Send regularly rather than in bursts, keep the list clean, and never buy addresses.

7. Back up

./db (dump, 65) and ./uploads, nightly. The list is the asset; export subscribers as CSV occasionally too.

Troubleshooting

SMTP test fails. Port/TLS mismatch; many relays want 587 STARTTLS, some 465 SSL. Credentials are API-style for SES (SMTP username/password from the SES console, not IAM keys).

Emails land in spam. SPF/DKIM/DMARC on the sending domain; a from-address on a domain the relay has verified; a plain-text alternative (Listmonk sends one); no URL shorteners in the body.

Subscribe form says "invalid". root_url and the form's domain must match; CAPTCHA settings if enabled.

Unsubscribes not working. The links use root_url — must be the public HTTPS address.

Wrapping up

Two containers behind a public HTTPS name, a relay with its DNS records green, a double-opt-in list with a form on the site, a Markdown campaign sent as a test first. You own the list and the archive, pay only the relay's pennies per thousand, and the unsubscribe and bounce handling are the kind that keeps you out of spam folders.

Call us