Self-Host Listmonk on Liquid Web
TL;DR
- Listmonk: open-source newsletter and transactional email manager (AGPL-3.0, ~19.3k GitHub stars, v6.1.0 from 2026-03-29)
- 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.
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.1.0): no visual drag-and-drop email builder (template editing is HTML/Go templates), no built-in CRM, no advanced drip/trigger automation beyond basic scheduling. 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
# Tested 2026-05-02 on local Docker (4 vCPU / 8 GB RAM)
# Idle: 512 MB RAM | Peak: 750 MB (50k subscriber import + 5k emails/hr)
# 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.1.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.1.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).
| 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
LISTMONK_VERSION=v6.2.0 # check github.com/knadh/listmonk/releases
# 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 | |
|---|---|---|---|
| 5k subscribers | $13/mo | $20/mo | ~$14/mo VPS (4 GB) |
| 25k subscribers | $54/mo | $72/mo | Same VPS |
| 100k subscribers | $270/mo | $350/mo | Same VPS + SMTP cost |
| Per-email fee | โ (after monthly limit) | โ | โ (pay for SMTP only) |
| Self-hostable | โ | โ | โ |
| License | Proprietary | Proprietary | AGPL-3.0 |
| Automation triggers | โ | โ | Basic scheduling only |
Prices subject to change. Verify Mailchimp pricing at mailchimp.com/pricing and Liquid Web at liquidweb.com/vps-hosting/managed-vps/.
The break-even point depends heavily on your subscriber count and send volume. At 25k+ subscribers, Listmonk on a 4 GB VPS ($14/mo) plus a Postmark plan ($75/mo for 500k emails) is $89/mo vs Mailchimp Standard at $72/mo. The trade-off: you keep the subscriber data, have no per-email caps, and own the infrastructure. At 100k subscribers, self-hosting is clearly cheaper.
When this isn't right for youโ
- You need drag-and-drop email design. Listmonk's template system is HTML/Go templates. If your team can't write HTML, the editing experience will frustrate non-technical users. Mailchimp's visual builder is genuinely better for design-heavy newsletters.
- 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`.



