Setting up a personal newsletter service with Listmonk: docker-compose + an email-sending VPS

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-stopped

First 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:

  1. Sign up. Create an SMTP credential (username + password or API key).
  2. Add your sending domain (e.g. example.com).
  3. 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.
  4. Wait for the provider’s domain status to flip green. Until it does, your emails will land in spam.
  5. 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_dump nightly 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.

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.