You write. People want to subscribe. Substack used to be the obvious answer until they started taking 10% plus payment processing, plus the platform-risk of the next political controversy. Buttondown is a nicer option for $9-29/mo. ConvertKit and MailerLite both work. But if you have ~5,000 subscribers, control matters more than the $20/month, and you already run a server: Listmonk is the open-source self-hosted answer, and it’s now mature enough to run a real list.
The catch is that “self-hosted email” means two things: the Listmonk app (easy) and the email-sending infrastructure (hard). Don’t try to send from your own SMTP server — you’ll go straight to spam. Use a transactional-email provider for the actual delivery, and Listmonk for everything else. Here’s the setup that works.
The architecture
- Listmonk on a $5 VPS — the web UI, subscriber DB, campaigns, double-opt-in handling, click tracking.
- Postgres alongside it, in the same docker-compose — required.
- Caddy in front for TLS.
- Amazon SES / Brevo / Mailgun / Postmark as the SMTP relay for actual sending. Their reputation, their IP warmup, their deliverability.
Listmonk doesn’t try to deliver email itself; it hands every message to your SMTP relay. That’s the right architecture. The relay’s pricing is per-email, low for the volumes a personal newsletter has — SES is $0.10 per 1,000 emails.
Step 1 — docker-compose for Listmonk + Postgres
# /opt/listmonk/docker-compose.yml
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: change-me-strong
POSTGRES_USER: listmonk
POSTGRES_DB: listmonk
volumes:
- ./pgdata:/var/lib/postgresql/data
restart: unless-stopped
app:
image: listmonk/listmonk:latest
depends_on:
- db
ports:
- "127.0.0.1:9000:9000"
volumes:
- ./uploads:/listmonk/uploads
- ./config.toml:/listmonk/config.toml:ro
restart: unless-stoppedFirst boot: docker compose run --rm app ./listmonk --install — creates the schema. Then docker compose up -d.
Step 2 — front it with Caddy
# /etc/caddy/Caddyfile
list.example.com {
reverse_proxy 127.0.0.1:9000
}Visit https://list.example.com/admin, set up your admin account, change the default credentials immediately.
Step 3 — pick an SMTP relay and verify your domain
Whatever provider you pick, the steps are similar:
- Sign up. Create an SMTP credential (username + password or API key).
- Add your sending domain (e.g.
example.com). - Add the DNS records the provider gives you: an SPF record, a DKIM CNAME or TXT, a DMARC record. Don’t skip DMARC; major mailbox providers now require it for any meaningful deliverability.
- Wait for the provider’s domain status to flip green. Until it does, your emails will land in spam.
- Test by sending one email to a Gmail address you control. Open the headers; verify SPF, DKIM, DMARC all say “pass.”
This step is the most boring and the most important. A wrong DKIM key means every newsletter goes to spam, and you’ll spend a week wondering why nobody’s reading.
Step 4 — wire the SMTP relay into Listmonk
Listmonk admin → Settings → SMTP. Add a server:
# For Amazon SES (us-east-1):
Hostname: email-smtp.us-east-1.amazonaws.com
Port: 587
Auth method: Login
Username: <your SMTP username from SES IAM>
Password: <your SMTP password from SES IAM>
TLS: STARTTLS
Max conns: 10
Idle timeout: 15s
# Sending speed: 14 emails/sec is a typical SES sandbox cap; raise after warm-up.Hit Test connection; you’ll see “OK” if creds are right. If not, the error usually tells you what’s wrong (auth failed, TLS required, etc.).
Step 5 — subscriber management
- Create a list (Lists → Add). Name it; set Type: Public if you’ll have a public subscribe form, Private if you import addresses by hand. Set Opt-in: Double — every subscriber confirms via clicking a link in their first email. Skip this and your list will accumulate spam-trap addresses, ruining your sender reputation.
- Use the public subscribe form. Listmonk auto-generates one at
https://list.example.com/subscription/form. Embed it on your site as an iframe or link, or implement a custom form that POSTs to the API. - Import existing subscribers via CSV (Subscribers → Import). Mark the source so you can segment/blacklist later.
Step 6 — sending the first campaign
Campaigns → New. Pick a list, paste your content (Listmonk supports HTML, Markdown, and rich-text). Listmonk has a built-in template system — create one with your branding once, reuse forever.
Always send a test to yourself first. Tap the “send a test” button, send to a mailbox you own, render-check it on phone and desktop. Then schedule the real send.
The catches I’ve learned
- SES sandbox. AWS SES starts in sandbox mode — only verified addresses can receive. Request production access before you actually need to send. Approval takes ~24 hours.
- Don’t import 10,000 addresses on day one. Email providers monitor sending velocity. Going from 0 to 10,000 emails in your first send looks like a spammer trying to drain a stolen list. Send to small batches first.
- Bounces and complaints. Wire SES’s SNS feedback into Listmonk’s bounce-handling endpoint. Otherwise bounces accumulate silently and your reputation slowly tanks.
- Backups. The Postgres DB has all your subscribers.
pg_dumpnightly to off-host storage. The day you need a backup is the day you regret not having one. - Unsubscribe must be one-click. Listmonk handles this by default. Don’t disable it. Mailbox providers downrank senders whose unsubscribe is hidden.
Cost in 2026
- VPS: $5/month for a Hetzner CX22.
- SES: $0.10 per 1,000 emails. 5,000 subscribers, 4 sends/month = 20,000 emails = $2/month.
- Domain + DNS: $12/year. Free if you already have one.
- Total: ~$7/month for a 5,000-subscriber list. Compare to Buttondown’s $29/mo at that scale, or ConvertKit’s $79/mo.
End-state: you write, hit send, the relay handles delivery, you watch open rates in the Listmonk dashboard. No platform with a content-moderation policy, no surprise pricing tier hikes, your subscriber list is rows in a Postgres DB you back up. Genuinely yours.
Photo: iPhone showing the Mail inbox by Solen Feyissa on Pexels.
