Self-Host Listmonk on Liquid Web
TL;DR
- Listmonk: open-source newsletter and transactional email manager (AGPL-3.0, ~23.3k GitHub stars, v6.2.0 from 2026-06-26)
- Stack: single Go binary + PostgreSQL 16 + Caddy, measured 512 MB idle on 4 vCPU / 8 GB
- Maintainer note: single-maintainer project (Kailash Nadh, CTO of Zerodha). Healthy history, but factor in the solo-maintainer risk before mission-critical deployment
Listmonk (github.com/knadh/listmonk) is a self-hosted newsletter and transactional email manager built in Go. It handles subscriber lists, campaign scheduling, transactional email APIs, template management, open and click tracking, and basic automation, all in a single statically compiled binary that runs on minimal resources.
Updated 2026-09-09: bumped the recommended version to v6.2.0 (security fixes plus several new features — see Upgrading Listmonk), refreshed the GitHub star count, and corrected the Mailchimp pricing table below, which had been quoting Mailchimp's introductory rate as if it were the price at 5,000 contacts.
What Listmonk is not: it does not include a built-in SMTP server. Listmonk dispatches email through an SMTP provider you configure: Postmark, SES, Mailgun, Sendgrid, or a self-hosted server like Billionmail. Inbox placement depends on the provider; Listmonk just formats and dispatches.
What Listmonk can't do (as of v6.2.0): no built-in CRM, and no advanced drip/trigger automation beyond campaign scheduling. It does ship a visual drag-and-drop builder and a WYSIWYG editor alongside Markdown, raw HTML, and Go templates (listmonk.app, checked 2026-09-09). For full marketing automation with behavioral triggers, Mautic is the more capable option, at the cost of 3× the RAM and active developer-hours maintenance risk.
Prerequisites
- A Liquid Web 4 GB Managed VPS (verify pricing)
- Docker Engine 25+ and Docker Compose V2
- A domain with its A record pointing to the VPS IP
- An SMTP provider account (Postmark, Mailgun, SES, or a self-hosted SMTP server)
- About 20 minutes
The complete docker-compose.yml
# listmonk/docker-compose.yml
# RAM tested 2026-05-02 on Listmonk v6.1.0, local Docker (4 vCPU / 8 GB RAM)
# Idle: 512 MB RAM | Peak: 750 MB (50k subscriber import + 5k emails/hr)
# Version pin below is v6.2.0 — a 2026-06-26 security release, see "Upgrading Listmonk"
# Source: https://github.com/knadh/listmonk — adapted for production
services:
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_DB: listmonk
POSTGRES_USER: listmonk
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U listmonk -d listmonk"]
interval: 10s
timeout: 5s
retries: 5
networks:
- listmonk_net
listmonk:
image: listmonk/listmonk:${LISTMONK_VERSION:-v6.2.0}
restart: unless-stopped
command: ["/listmonk/listmonk"]
environment:
LISTMONK_app__address: "0.0.0.0:9000"
LISTMONK_app__admin_username: ${ADMIN_USERNAME}
LISTMONK_app__admin_password: ${ADMIN_PASSWORD}
LISTMONK_db__host: db
LISTMONK_db__port: "5432"
LISTMONK_db__user: listmonk
LISTMONK_db__password: ${POSTGRES_PASSWORD}
LISTMONK_db__database: listmonk
LISTMONK_db__ssl_mode: disable
depends_on:
db:
condition: service_healthy
networks:
- listmonk_net
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:9000/health || exit 1"]
interval: 15s
timeout: 5s
retries: 5
caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
depends_on:
listmonk:
condition: service_healthy
networks:
- listmonk_net
volumes:
db_data:
caddy_data:
caddy_config:
networks:
listmonk_net:
driver: bridge
Caddyfile
# Caddyfile — Listmonk reverse proxy
newsletters.yourdomain.com {
reverse_proxy listmonk:9000 {
health_uri /health
health_interval 15s
}
encode gzip
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Frame-Options SAMEORIGIN
X-Content-Type-Options nosniff
}
}
.env file
# .env — keep out of version control
# Listmonk version
LISTMONK_VERSION=v6.2.0
# PostgreSQL
POSTGRES_PASSWORD=change-me-strong-password
# Admin credentials (set before first run)
ADMIN_USERNAME=admin
ADMIN_PASSWORD=change-me-strong-password
First-run setup
# 1. Start the database first
docker compose up -d db
docker compose ps db # wait until healthy
# 2. Install the database schema (required on first run)
docker compose run --rm listmonk ./listmonk --install
# 3. Start all services
docker compose up -d
# 4. Verify all services are healthy
docker compose ps
# 5. Visit https://newsletters.yourdomain.com
# Log in with ADMIN_USERNAME and ADMIN_PASSWORD from .env
# 6. Configure your SMTP provider:
# Settings → SMTP → Add SMTP server
# Enter your Postmark / SES / Mailgun credentials
Verify the stack
# All services should be Up and healthy
docker compose ps
# Health endpoint
curl -s http://localhost:9000/health
# {"status":"ok"}
# Resource usage
docker stats --no-stream
Runtime footprint
Measured 2026-05-02 on local Docker (4 vCPU / 8 GB RAM), running Listmonk v6.1.0. The v6.2.0 pin recommended above ships security fixes (campaign/list permission validation, a ZIP-Slip path traversal in CSV import, and API-token hashing) alongside several new features, including CID inline images, per-domain SMTP routing, and Azure bounce webhooks — see Upgrading Listmonk. These figures have not been re-measured on v6.2.0; treat them as a v6.1.0 baseline.
| Service | Idle RAM | Peak RAM (50k import + 5k/hr) |
|---|---|---|
| listmonk | 90 MB | 200 MB |
| db (postgres 16) | 420 MB | 540 MB |
| caddy | 15 MB | 20 MB |
| Total | 525 MB | 760 MB |
A Liquid Web 4 GB Managed VPS has comfortable headroom at these load levels. If you're co-locating with another service (Chatwoot, Twenty CRM), a 4 GB VPS still works for Listmonk alone. Upgrade to 8 GB for the co-located setup.
Configure SMTP
Listmonk relays through your chosen SMTP provider. In the admin panel: Settings → SMTP.
Postmark (recommended for transactional):
Host: smtp.postmarkapp.com
Port: 587
TLS: STARTTLS
Username: your-server-api-token
Password: your-server-api-token
Max connections: 10
Retries: 2
Amazon SES (recommended for high volume):
Host: email-smtp.<region>.amazonaws.com
Port: 587
TLS: STARTTLS
Username: your-ses-access-key-id
Password: your-ses-smtp-secret
Max connections: 20
Self-hosted Billionmail (cost-optimized at scale):
Host: mail.yourdomain.com (or the Billionmail container hostname on a shared network)
Port: 587
TLS: STARTTLS
Username: billionmail-smtp-user
Password: billionmail-smtp-password
Max connections: 5 # Keep low during IP warming
If using Billionmail, follow the IP warming schedule in the Billionmail guide before sending bulk campaigns. A new IP sending 10k emails/day on day 1 will land in spam.
Upgrading Listmonk
# Update version in .env to the release you're upgrading to
# Check github.com/knadh/listmonk/releases for the current latest tag
LISTMONK_VERSION=<new-version>
# Pull new image
docker compose pull listmonk
# Stop the app
docker compose stop listmonk
# Run database upgrade
docker compose run --rm listmonk ./listmonk --upgrade
# Restart
docker compose up -d listmonk
# Verify
docker compose ps
docker compose logs listmonk --tail 20
Always check the Listmonk release notes before upgrading. The --upgrade flag applies any schema changes.
Cost vs Mailchimp
| Mailchimp Essentials | Mailchimp Standard | Listmonk on Liquid Web | |
|---|---|---|---|
| Starting price | from $13/mo intro rate (smallest contact tier, 50% off for 12 months) | from $20/mo intro rate (smallest contact tier, 50% off for 12 months) | ~$14/mo flat (4 GB VPS), any list size |
| Pricing model | Scales up per contact tier via an interactive calculator; no static price above 500 contacts | Same | Flat VPS cost regardless of subscriber count |
| Per-email fee | ✓ (after plan's send limit) | ✓ | ✗ (pay for SMTP only) |
| Self-hostable | ✗ | ✗ | ✓ |
| License | Proprietary | Proprietary | AGPL-3.0 |
| Automation triggers | ✓ | ✓ | Basic scheduling only |
Mailchimp doesn't publish static prices for specific contact counts — the number on its pricing page is an introductory rate for the smallest contact tier, and larger lists run through an interactive contact-count selector. Before comparing, pull your actual contact-count price live at mailchimp.com/pricing/marketing — the $13 and $20 figures above are promotional and apply only to the smallest tier for the first 12 months. Verify Liquid Web pricing at liquidweb.com/vps-hosting/managed-vps/.
The real difference isn't a single break-even number, it's how the two pricing models scale. Mailchimp bills by contact count regardless of how often you email them, so the bill keeps climbing as your list grows even if you send once a month. Listmonk's cost is fixed at the VPS price (~$14/mo) plus whatever your SMTP provider charges for the emails you actually send: Postmark's Basic plan, for example, is $15/mo for the first 10,000 emails with overage at $1.80 per 1,000 (postmarkapp.com/pricing, checked 2026-09-09). At list sizes where Mailchimp's per-contact pricing runs into the hundreds of dollars a month, Listmonk plus a mid-tier SMTP plan is very likely cheaper, but check both sides against live pricing before committing. You also keep the subscriber data and own the infrastructure either way.
When this isn't right for you
- You need best-in-class email design tooling. Listmonk does have a visual drag-and-drop builder and a WYSIWYG editor, but it is younger and thinner than Mailchimp's, and heavy customization still drops you into HTML/Go templates. For design-heavy newsletters produced by non-technical staff, Mailchimp's builder and template library remain the stronger option.
- You need advanced behavioral automation. Listmonk supports scheduled campaigns and basic transactional triggers via API. It does not have lead scoring, behavioral triggers (page visit → email), or multi-step drip sequences with branching logic. For that, Mautic is the self-hosted option (with its 2026 funding caveats).
- You're concerned about solo-maintainer risk. Listmonk is maintained by one person (Kailash Nadh). The project history is strong and Zerodha has organizational backing, but a single point of failure exists. If you need a community-backed project, Mautic (community-governed) is the alternative.
- You send fewer than 5k emails/month. The free tiers of Resend, Mailgun, or Postmark may cost less than your time to set up and maintain this stack. Self-hosting makes financial sense at scale.
Exit strategy
Listmonk stores all data in PostgreSQL. To migrate:
# Export subscribers to CSV from admin panel: Subscribers → Export
# Or dump the full database
docker compose exec db pg_dump -U listmonk listmonk > listmonk_export.sql
# Export campaign data, templates, and media uploads
docker compose exec listmonk tar -czf /tmp/uploads.tar.gz /listmonk/uploads
docker cp listmonk:/tmp/uploads.tar.gz ./
Subscriber data exports to CSV with custom attributes included. The PostgreSQL dump contains everything else: campaigns, templates, lists, click/open tracking data.
Yes. Listmonk supports double opt-in with a configurable confirmation email template. When enabled, new subscribers receive a confirmation link and are only added to the active subscriber list after clicking it. The opt-in flow is configurable per subscription form. Double opt-in is strongly recommended for GDPR compliance.
Yes. Listmonk exposes a transactional email API at `/api/tx`. Your application POSTs to the API with a template ID and subscriber data; Listmonk renders the template and dispatches via SMTP. This is separate from campaign sending and works with the same SMTP configuration. The API is documented at github.com/knadh/listmonk/blob/master/docs/docs/api/transactional.md.
Listmonk auto-injects an unsubscribe link into every campaign email using the `{{ UnsubscribeURL }}` template variable. Clicking the link blacklists the subscriber's email address, blocking them from future campaign sends. The blacklist is maintained in PostgreSQL. For GDPR right-to-erasure, use the admin API to delete subscriber records: DELETE /api/subscribers/{id}.
Listmonk (512 MB idle, Go binary, AGPL-3.0) is a focused newsletter and transactional email tool with a clean API. Mautic (1.4 GB idle, PHP/Apache/MariaDB, GPL-3.0) is a full marketing automation platform with lead scoring, behavioral triggers, landing pages, and multi-channel campaigns, but it carries a 2026 funding crisis disclosure. Use Listmonk if you need reliable newsletter sending with low overhead. Use Mautic if you need behavioral automation, but read the funding situation first.
Yes. At 512 MB idle, Listmonk leaves substantial headroom on a 4 GB VPS. Combining with Twenty CRM (~1.1 GB idle) gives a combined ~1.7 GB idle on 4 GB, which is comfortable. Combining with Chatwoot (~1.1 GB idle) similarly works. For a full GTM stack (Listmonk + Mautic + Chatwoot + Twenty), move to an 8 GB or 16 GB VPS.
Common mistakes and fixes
Listmonk UI is blank or returns 404 after startup.
Listmonk does not auto-run database migrations on start. Run: `docker compose run --rm listmonk ./listmonk --install` on first run, or `./listmonk --upgrade` after updating the image version. Without the install step, the schema doesn't exist and the app serves an empty response.
Emails land in spam on Gmail despite correct SPF/DKIM.
Listmonk is an email dispatcher; it does not include a built-in SMTP server. It relays through the SMTP provider you configure (Postmark, SES, Mailgun, or a self-hosted Postal / Billionmail). The inbox placement depends entirely on that provider's reputation, not Listmonk itself. If using a self-hosted SMTP server like Billionmail, follow the IP warming schedule in the Billionmail guide.
Listmonk container exits immediately with 'pq: role does not exist'.
The PostgreSQL container was not fully initialized before Listmonk tried to connect. Fix: add a `depends_on` with `service_healthy` condition on the `db` service (already included in this guide's compose file). If you started Listmonk manually before the healthcheck passed, run: `docker compose down && docker compose up -d`.