LIVEReading: Self-host a newsletter with listmonkTotal time: 20 minSteps: 5Worked first time: 70% LIVEReading: Self-host a newsletter with listmonkTotal time: 20 minSteps: 5Worked first time: 70%
CBW
Mediumgithub.com/knadh/listmonk2026-05-20

Self-host a newsletter with listmonk

listmonk is a self-hosted newsletter manager — bring your own SMTP, your subscriber list lives in your Postgres, no Mailchimp tax. Bonus section: pipe drafts through Claude before you hit send.

// Build stats

  • Total time20 min
  • Number of steps5
  • DifficultyMedium
  • Worked first time70%
// Before you start

What you need

  • Docker + Docker Compose installed (the supported install path)
  • A SMTP provider — Amazon SES, Postmark, Sendgrid, or a self-hosted relay (Gmail SMTP works for ~100/day test sends only)
  • A domain you control, with SPF/DKIM records you can edit. Without these, mail goes straight to spam
  • Approximately 1 GB RAM free for listmonk + its Postgres
  • Optional: an Anthropic or OpenAI API key for the AI-assisted draft step at the end
01
Step 1 of 5

Pull the official docker-compose stack

2 min

listmonk ships a complete docker-compose file with the app + Postgres pre-wired. Don't try to roll your own — the upstream compose handles volume mounts, the init job that creates the database schema, and admin-user creation in one shot.

Terminal · mac
$ mkdir ~/listmonk && cd ~/listmonk
$
$ # Download the official compose file
$ curl -L -o docker-compose.yml https://raw.githubusercontent.com/knadh/listmonk/master/docker-compose.yml
$
$ # Pull images
$ docker compose pull
What you should see
Docker pulls listmonk and postgres images. `docker compose config` validates the file without error.
This might happen

`docker compose` says 'unknown command'.

You're on the old Compose v1 (`docker-compose` with a hyphen). Either install Compose v2 (it ships with current Docker Desktop) or use `docker-compose` for every command in this guide.

02
Step 2 of 5

Initialize the database and create your admin user

2 min

listmonk's first-run command creates the schema in Postgres and prompts you to set the admin username and password. This is interactive — don't run it with `-d`. After this one-time step, future starts are just `docker compose up -d`.

Terminal · mac
$ # Run the one-time init (interactive)
$ docker compose up db -d
$ sleep 5
$ docker compose run --rm app ./listmonk --install --idempotent --yes
$
$ # Set admin user — interactive prompt
$ docker compose run --rm app ./listmonk --set-config admin_username=admin admin_password=<choose a strong one>
$
$ # Now start everything
$ docker compose up -d
What you should see
Docker logs show 'database is being installed' then 'OK'. `docker compose ps` lists `listmonk_app` and `listmonk_db` both as 'running'.
This might happen

App container restarts in a loop.

Almost always a DB connection issue. Check `docker compose logs app` — common causes: the db container isn't healthy yet (rerun after a few seconds), or the password contains a special character that broke the connection string.

03
Step 3 of 5

Log in and connect your SMTP provider

5 min

Open http://localhost:9000 in your browser and log in with the admin user. Navigate to Settings → SMTP and fill in your provider's details. Postmark and SES are the cheapest and have the highest deliverability for transactional + newsletter mixed sending. Save and click 'Test' to send a one-off test to your own address.

Terminal · mac
$ # Open the admin in your browser:
$ http://localhost:9000
$
$ # Then in the UI:
$ # Settings → SMTP → enable
$ # Host: smtp.postmarkapp.com (or smtp.sendgrid.net, email-smtp.us-east-1.amazonaws.com, etc.)
$ # Port: 587
$ # AuthProt: PLAIN
$ # Username: <your provider's SMTP username>
$ # Password: <SMTP password / token>
$ # FromEmail: hello@yourdomain.com
$ # Click 'Send test email' to your own address
What you should see
A green 'Test email sent successfully' toast in the admin UI. Within a minute, the test email arrives in your inbox (check spam too, until you've added SPF/DKIM).
This might happen

Test email never arrives.

Two layers to check: (1) listmonk logs (`docker compose logs app | tail`) — look for SMTP errors. (2) your provider's send-log dashboard — most will show the rejection reason. Almost always one of: wrong creds, port blocked, or the From address isn't verified at the provider.

04
Step 4 of 5

Import (or create) your first subscriber list

3 min

Lists are the unit of segmentation in listmonk — each campaign sends to one or more lists. Create a list, then either invite manually, import a CSV, or expose the public subscribe form. For your first send, manually subscribe yourself + 2-3 trusted recipients so deliverability problems show up before you blast a real audience.

Terminal · mac
$ # In the UI:
$ # Lists → + New List → Name: 'Beta readers' → Type: Public → Save
$ #
$ # Then either:
$ # (a) Subscribers → + New → fill name + email + assign to list
$ # (b) Subscribers → Import → upload a CSV with `email,name` headers
$ # (c) Lists → click your list → 'Public link' → share that URL externally
What you should see
The list shows a subscriber count >= 1. Each subscriber has a status of 'enabled'.
This might happen

CSV import shows 0 successful rows.

listmonk's CSV expects exact column headers `email,name,status` (lowercase). Open the CSV, fix the header row, and re-import. The error log under Import history tells you which row failed.

05
Step 5 of 5

Draft a campaign and (optionally) run it through Claude first

5 min

Campaigns → New. Pick the list, pick a template (a stock one works for the test), and write subject + body. listmonk uses Sprig templating for personalization (e.g. `{{ .Subscriber.FirstName }}`). For the AI-assist step, write your rough notes locally, pipe them through Claude with a 'tighten this newsletter' prompt, and paste the result into listmonk. Schedule for 5 minutes from now to confirm the queue fires.

Terminal · mac
$ # Optional: AI-assist draft (run on your laptop, not in the container)
$ cat my-rough-notes.md | \
$ curl -s https://api.anthropic.com/v1/messages \
$ -H "x-api-key: $ANTHROPIC_API_KEY" \
$ -H "anthropic-version: 2023-06-01" \
$ -H "content-type: application/json" \
$ -d @- <<'EOF'
$ {
$ "model": "claude-sonnet-4-6",
$ "max_tokens": 1500,
$ "messages": [{
$ "role": "user",
$ "content": "Rewrite the following rough newsletter notes into a tight, friendly issue. Keep links. Aim for ~400 words.\n\n<<PASTE NOTES HERE>>"
$ }]
$ }
$ EOF
$
$ # Then in listmonk UI:
$ # Campaigns → New → Name + Subject + Body (paste from Claude or your editor)
$ # → Schedule for: <5 minutes from now>
$ # → Save and start
What you should see
Campaign moves through statuses: scheduled → running → finished. The progress bar fills as listmonk sends. Your beta-reader inbox receives the email.
This might happen

Sends very slowly (one mail every several seconds).

listmonk's default concurrency is conservative. Settings → Performance → bump `concurrency` to 5 or 10 (your SMTP provider likely allows much more). Don't crank it past your provider's documented rate limit.

// Status

cooked. baked. worked.

A running listmonk instance at http://localhost:9000 (or your domain) with one list, a few test subscribers, and a campaign that sent successfully through your own SMTP provider.

// the honest bit

The honest part

Heads up — drafted from listmonk's official docs and standard self-hosted SMTP setup, not a CBW hands-on run. Deliverability is the actual hard part of newsletters: SPF + DKIM + DMARC + a warm-up period for new domains. listmonk itself rarely fails — what fails is mail landing in inboxes vs. spam. For the AI-assist piece, the Claude API call shown is illustrative; in practice you'd probably want a small shell script or Raycast snippet that wraps it cleanly. If you don't want to operate Postgres + Docker + SMTP yourself, hosted Buttondown or beehiiv solve the same problem for ~$10-20/month.