Skip to main content
Open Source Solutions

Listmonk Self-Hosted Newsletter: My Deployment Guide

How I ship Listmonk for clients who want a Mailchimp replacement they actually own, plus the SMTP relay choices that decide whether the campaigns land.

Published Updated 11 min read

I deploy Listmonk for the same client every other month: someone who started on Mailchimp, watched their bill creep past €200 because the list outgrew the pricing tier, and decided they’d rather pay a flat VPS fee. Listmonk self-hosted newsletter platform is the boring, working answer to that bill. I’ve shipped it for around fifteen agencies and B2C operators, mostly in the EU, and it’s been the lowest-maintenance tool in my open-source rotation by a wide margin.

This post is the stack I actually run: Listmonk plus Postgres on a 2GB VPS, fronted by Nginx Proxy Manager, with a transactional SMTP relay handling delivery. Around 30 minutes from a fresh server to a working dashboard ready for a first campaign, assuming you’ve already done the DNS work for SPF, DKIM, and DMARC on the sending domain.

A small honesty check before you start. Listmonk is excellent at what it does, but it isn’t Mailchimp. The onboarding wizard is sparse, the campaign editor is good but not magical, and the integration story is “there’s a REST API, write something.” If you want hand-holding, stay on hosted SaaS.

When Listmonk beats Mailchimp or ConvertKit

For most operators under 5,000 subscribers, hosted SaaS will save you time and money. If you’re under that threshold and you don’t have a strict data-residency requirement, use what you’ve got.

Listmonk wins in three specific scenarios I keep running into:

1. The pricing-tier cliff. Mailchimp jumps fast. Standard goes from €20/month at 500 contacts to €100/month at 10,000 to €350/month at 50,000. ConvertKit is similar past the free 1,000-subscriber tier. For a B2C client with 80,000 newsletter subscribers and one campaign a week, Listmonk on a €15/month VPS plus €30/month of Amazon SES traffic costs roughly an order of magnitude less than the equivalent Mailchimp plan.

2. Data residency and GDPR. I run a few clients in healthcare-adjacent and public-sector spaces. Shipping a 200,000-row subscriber list to a US-based vendor without a Data Protection Impact Assessment is not happening. Listmonk on a Hetzner VPS in Falkenstein keeps every email address inside the same legal jurisdiction as the company that owns it, which keeps the DPO conversation short.

3. Custom workflows the SaaS won’t bend to. Listmonk has a real REST API, a proper template language, and lets you publish public archives that Google can index. I have one client whose newsletter doubles as an SEO surface. The archive ranks on long-tail queries that the email blast itself never could.

Outside those scenarios, hosted SaaS is probably the right call. Don’t self-host a tool you don’t have a reason to self-host.

The Listmonk deployment stack

Two containers do the entire job: Listmonk itself and a Postgres database. The official Compose file is the right starting point, with a few small changes I make for production.

Prerequisites

A 2GB VPS is the realistic floor. A 1GB box runs the Listmonk binary plus Postgres, but leaves no headroom for the Nginx Proxy Manager in front, and any Postgres connection-pool spike pushes the VPS into swap.

You also need a properly hardened server before any of this runs. If you haven’t done that yet, start with Linux server security fundamentals and come back.

DNS access for the sender domain is non-negotiable. Before Listmonk sends a single email, you’ll be adding SPF, DKIM, and DMARC records that match the SMTP relay you pick.

Docker engine

Install Docker from the official Docker repository, following the official installation guide for Ubuntu. Don’t install from apt’s default repo, and don’t run a “convenient” curl-pipe-bash script. The official repository is the only correct source.

The Compose file

This isn’t a Portainer-friendly stack. Listmonk’s first-run install command needs to be invoked inside the container before the app starts properly, and that’s an interactive docker compose run step that’s awkward through a GUI. SSH into the server and work from the terminal.

Create a working directory and the Compose file:

mkdir -p /srv/docker/listmonk && cd /srv/docker/listmonk
nano docker-compose.yml

Paste this in:

version: "3.7"

x-app-defaults: &app-defaults
  restart: unless-stopped
  image: listmonk/listmonk:latest
  ports:
    - "127.0.0.1:9000:9000"
  networks:
    - listmonk
  environment:
    - TZ=Europe/Berlin

x-db-defaults: &db-defaults
  image: postgres:16
  networks:
    - listmonk
  environment:
    - POSTGRES_PASSWORD=change-me-to-a-real-password
    - POSTGRES_USER=listmonk
    - POSTGRES_DB=listmonk
  restart: unless-stopped
  healthcheck:
    test: ["CMD-SHELL", "pg_isready -U listmonk"]
    interval: 10s
    timeout: 5s
    retries: 6

services:
  db:
    <<: *db-defaults
    container_name: listmonk_db
    volumes:
      - listmonk-data:/var/lib/postgresql/data

  app:
    <<: *app-defaults
    container_name: listmonk_app
    depends_on:
      - db
    volumes:
      - ./config.toml:/listmonk/config.toml

networks:
  listmonk:

volumes:
  listmonk-data:

A few notes on what I changed from the upstream sample, because each change is deliberate.

Postgres is pinned to major version 16 instead of the older postgres:13 in the original docs. Pin a current major you’ll actually maintain; skipping a major-version migration for years is how legacy installs get stuck.

The Postgres port is no longer published to the host. There is no reason to expose a database to the public internet. The original Compose file mapped Postgres to host port 9432, which is the kind of footgun I’d rather not ship to a client.

The Listmonk port binds to 127.0.0.1:9000 instead of 0.0.0.0:9000. A reverse proxy terminates TLS on 443 and forwards there. Skip the proxy and you’re running an admin UI on plain HTTP, which is bad on every axis.

The Postgres password is the one thing in this file you absolutely have to change before bringing it up. I am being literal about that. The default listmonk:listmonk from the upstream tutorial is in every search index ever made; do not ship it.

The config.toml file

Create the matching config file in the same directory:

nano config.toml

Paste this:

[app]
address = "0.0.0.0:9000"

# Disable BasicAuth. You'll create the real admin user via the install wizard.
admin_username = ""
admin_password = ""

[db]
host = "listmonk_db"
port = 5432
user = "listmonk"
password = "change-me-to-a-real-password"
database = "listmonk"
ssl_mode = "disable"
max_open = 25
max_idle = 25
max_lifetime = "300s"
params = ""

Three things to flag here.

address = "0.0.0.0:9000" makes Listmonk listen on all interfaces inside the container. The host-side binding to 127.0.0.1 from the Compose file is what keeps it private.

host = "listmonk_db" matches the Postgres container name. The original tutorial used host = "localhost", which only works inside the same container; from the Listmonk container it has to be the database service name.

The BasicAuth fields are left empty. Listmonk’s modern releases have a proper multi-user role-based auth system in the web UI. You’ll create the first super-admin user during the install wizard.

First-run install and start

Bring up the database first, run the install command, then start the app:

docker compose up -d db
docker compose run --rm app ./listmonk --install
docker compose up -d app

The --install step seeds the schema and creates the initial admin password. Watch the output: it prints a randomly generated password you’ll need on first login, and it only prints it once.

Visit http://<server-ip>:9000 from a workstation on the same network (or use SSH port-forwarding) to confirm the UI loads, then move on to the proxy.

Reverse proxy with Nginx Proxy Manager

I use Nginx Proxy Manager for every Docker stack I deploy. If you followed my Portainer + Nginx Proxy Manager + Vaultwarden guide, NPM is already running on the same VPS or a sibling host.

Add a Proxy Host pointing newsletter.yourdomain.com to http://<server-ip>:9000, request a Let’s Encrypt certificate, tick “Force SSL” and “HTTP/2 Support”. Two minutes of clicking, and you have a TLS-secured admin dashboard.

If you’d rather use Caddy or hand-rolled Nginx, the only requirements are: terminate TLS, forward to port 9000 on the Listmonk host, and pass X-Forwarded-For and X-Forwarded-Proto so public-archive URLs render with the right scheme.

SMTP relay: the choice that decides everything

Here’s the part the install guide is too polite about: Listmonk doesn’t solve email delivery on its own. It’s a sender, not a relay. Plug it into your domain’s mail server, blast 10,000 newsletter sends, and watch half land in spam while the rest get rate-limited.

The fix is the same one every bulk-email operator already knows: route everything through a real SMTP provider with a managed sender reputation.

The three relays I ship Listmonk with, depending on the client:

  • Postmark for clients who care about deliverability above everything else and send under 1 million emails a month. Postmark is strict about content and bounce rates, which is exactly why their delivery numbers are good. Around €15 to €60/month for typical newsletter volumes.
  • Amazon SES for clients already in AWS or pushing past 500,000 sends a month who want pay-per-send pricing. Cheapest at scale, but plan a day to get out of the SES sandbox before anything works for real recipients.
  • Mailgun EU for clients with a hard “no US data residency” requirement. The EU plan keeps logs in Frankfurt, which keeps the DPO conversation short.

Configure the relay inside Listmonk under Settings → SMTP. Add the SMTP host (e.g. smtp.postmarkapp.com), port 587, the relay-issued credentials, and a from-address on a domain you’ve authenticated. Listmonk lets you add multiple SMTP servers and load-balance across them, which is useful when you want to split marketing and transactional reputation across two providers.

Before sending a real campaign, send yourself a test email through Listmonk and run the result through Mail-Tester or MXToolbox. You’re looking for a 9/10 or 10/10 score and clean SPF, DKIM, and DMARC alignment. Anything less means the DNS work isn’t done.

Bounces, unsubscribes, and list hygiene

This is the part that takes a Listmonk deployment from “it sends” to “it sends and stays delivered.” Unsubscribes work out of the box: the link goes through Listmonk’s own endpoint, the address gets flagged, and they stop receiving sends.

Bounces you have to wire up. Listmonk supports three methods:

  • POP3 mailbox polling. Point Listmonk at a mailbox where bounce notifications arrive (e.g. bounces@yourdomain.com), and Listmonk polls it on a schedule.
  • SMTP relay webhooks. Postmark, SendGrid, and Mailgun post bounce events to a webhook URL you configure inside Listmonk. The cleanest option if your relay supports it.
  • Amazon SES SNS notifications. SES posts to an SNS topic; Listmonk has a built-in SNS endpoint that consumes them.

Pick one and configure it before your first real campaign. The alternative is hard-bouncing addresses staying on the active list and getting retried for months, which is a one-way ticket to the spam folder for everyone else on it.

In Settings → Bounces, set “Action on hard bounce” to “Block subscriber” or “Delete subscriber” with a threshold of 1. Soft bounces I usually set to “Block” at a threshold of 3, so transient failures don’t unsubscribe legitimate addresses.

Run a list cleaning pass quarterly. Anyone in bounced or blocklisted for more than 90 days is unlikely to come back, and removing them improves sender stats with the relay.

Verification: did the campaign actually land?

The mistake I see in greenfield Listmonk deployments: people install it, send a campaign, see “delivered” in the dashboard, and assume the job is done. The dashboard tells you the relay accepted the message. It doesn’t tell you the message reached the inbox.

Run this verification on day one, before any real subscriber sees a campaign:

  1. Create a test list with five seed addresses across different providers: Gmail, Outlook, your own domain on Mailcow, an Apple Mail or iCloud account, and one Yahoo or Proton account.
  2. Send a real campaign to that list using the actual template you’d ship.
  3. Check each inbox. Inbox, not Promotions, not Spam.
  4. Run the email through Mail-Tester. Aim for 9/10 or 10/10.
  5. If anything lands in Spam or Promotions, fix DNS, sender reputation, or content before going further.

Repeat the seed-list test before every major campaign and before every change to the SMTP relay. The 90 seconds it takes catches every flavor of misconfigured DNS.

What I deliberately don’t bother with

A few things I’ve consciously left out of the baseline stack:

  • Multi-server Listmonk clusters. Listmonk supports horizontal scaling via shared Postgres, but I’ve not seen an agency workload that justified it. Until you’re past 5 million sends a month, vertical scaling on one VPS is simpler.
  • Built-in image hosting. Listmonk lets you upload images for templates. For anything serious, I host campaign images on a separate static host or CDN. Mixing newsletter sends with image traffic on the same VPS makes Postgres backups bigger than they need to be.
  • Replacing transactional email with Listmonk. Listmonk is a newsletter and bulk-mail tool. For password resets, receipts, and account notifications, use a transactional-mail provider directly. Different reputation, different tool.

Closing the loop

Self-hosted Listmonk earns its place in two specific scenarios: budgets where Mailchimp’s per-subscriber pricing breaks the spreadsheet, and deployments with strict data-residency requirements that rule out US SaaS. In those scenarios, the stack above runs reliably across the dozen-or-so client deployments I currently operate. Two containers, one SMTP relay, a reverse proxy in front, and a quarterly list-hygiene pass.

Pair it with Uptime Kuma so you find out the day Listmonk stops responding instead of the day a subscriber complains, and slot it into whatever else lives in your operations-automation stack. If you also need full marketing automation rather than newsletters specifically, Mautic is the heavier tool for that job. Listmonk is the lighter, sharper choice when newsletters are the product.

The stack isn’t glamorous. It’s just what works.

Watch on YouTube

Video walkthrough

Prefer the screen-recording version of this guide? Watch it on YouTube — opens in a new tab so the player only loads when you ask for it.

Frequently Asked Questions

Want this handled, not just understood?

Reading the playbook is one thing. Running it on production at 2am is another. If you'd rather have me run it for you, the door is open.

Apply for Access