Self-Hosting Guide: Deploy Listmonk for Email Newsletters
Self-Hosting Guide: Deploy Listmonk for Email Newsletters
Listmonk is the fastest open source newsletter tool — a single Go binary that can send 100,000+ emails per hour. Self-hosting replaces Mailchimp, ConvertKit, and Buttondown at a fraction of the cost. No per-subscriber pricing.
Requirements
- VPS with 512 MB RAM minimum
- Docker and Docker Compose
- Domain name (e.g.,
mail.yourdomain.com) - SMTP service (Amazon SES, Resend, SendGrid)
- DNS access for SPF/DKIM/DMARC records
- 5+ GB disk
Step 1: Create Docker Compose
# docker-compose.yml
services:
listmonk:
image: listmonk/listmonk:latest
container_name: listmonk
restart: unless-stopped
ports:
- "9000:9000"
volumes:
- ./config.toml:/listmonk/config.toml
depends_on:
- db
db:
image: postgres:16-alpine
container_name: listmonk-db
restart: unless-stopped
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=listmonk
- POSTGRES_USER=listmonk
- POSTGRES_PASSWORD=your-strong-password
volumes:
postgres_data:
Step 2: Create Configuration
Create config.toml:
[app]
address = "0.0.0.0:9000"
admin_username = "admin"
admin_password = "your-admin-password"
[db]
host = "db"
port = 5432
user = "listmonk"
password = "your-strong-password"
database = "listmonk"
ssl_mode = "disable"
max_open = 25
max_idle = 25
max_lifetime = "300s"
Step 3: Initialize Database
# Start database first
docker compose up -d db
# Run database setup
docker compose run --rm listmonk ./listmonk --install
# Start Listmonk
docker compose up -d
Step 4: Reverse Proxy (Caddy)
# /etc/caddy/Caddyfile
mail.yourdomain.com {
reverse_proxy localhost:9000
}
sudo systemctl restart caddy
Step 5: Configure SMTP
In Settings → SMTP:
| Provider | Host | Port | Auth |
|---|---|---|---|
| Amazon SES | email-smtp.us-east-1.amazonaws.com | 587 | SMTP credentials |
| Resend | smtp.resend.com | 587 | API key |
| SendGrid | smtp.sendgrid.net | 587 | API key |
| Mailgun | smtp.mailgun.org | 587 | API key |
| Brevo | smtp-relay.brevo.com | 587 | API key |
SMTP configuration in Listmonk:
- Settings → SMTP → Add new
- Enter host, port, auth type, credentials
- Set max connections (5-10 for most providers)
- Set retries and wait time
- Test the connection
Cost comparison for 50K emails/month:
| Provider | Price |
|---|---|
| Amazon SES | $5 |
| Resend | $20 |
| SendGrid | $15 |
| Brevo | Free (300/day) |
| Mailgun | $35 |
Step 6: Set Up DNS Records
Required for deliverability:
# SPF — authorize your SMTP provider
yourdomain.com TXT "v=spf1 include:amazonses.com ~all"
# DKIM — sign your emails (get from SMTP provider)
selector._domainkey.yourdomain.com TXT "v=DKIM1; k=rsa; p=..."
# DMARC — tell receivers what to do with failed auth
_dmarc.yourdomain.com TXT "v=DMARC1; p=quarantine; rua=mailto:dmarc@yourdomain.com"
# Return-Path (optional but recommended)
bounce.yourdomain.com CNAME feedback-smtp.us-east-1.amazonses.com
Step 7: Create Subscriber Lists
- Lists → Create new
- Create lists like:
- Newsletter (public, double opt-in)
- Product Updates (public, double opt-in)
- Internal (private, single opt-in)
Import subscribers:
- Subscribers → Import
- Upload CSV with columns:
email, name, list - Map fields and import
Step 8: Create Email Templates
Listmonk uses Go templates:
{{ template "header" . }}
<h1>{{ .Campaign.Subject }}</h1>
<p>Hey {{ .Subscriber.FirstName }},</p>
{{ .Campaign.Body }}
<p>
<a href="{{ .Campaign.URL }}">Read more →</a>
</p>
{{ template "footer" . }}
Template variables:
| Variable | Value |
|---|---|
{{ .Subscriber.Email }} | Subscriber email |
{{ .Subscriber.FirstName }} | First name |
{{ .Subscriber.LastName }} | Last name |
{{ .Campaign.Subject }} | Campaign subject |
{{ .Campaign.Body }} | Campaign content |
{{ .UnsubscribeURL }} | Unsubscribe link |
Step 9: Create and Send Campaigns
- Campaigns → Create new
- Write content using the rich text editor or HTML
- Select target lists
- Preview and test (send test email to yourself)
- Schedule or send immediately
Campaign types:
- Regular — one-time send
- Optin — double opt-in confirmation
Step 10: Add Subscription Forms
Embed form on your website:
<form method="post" action="https://mail.yourdomain.com/subscription/form">
<input type="hidden" name="nonce" />
<input type="email" name="email" required placeholder="your@email.com" />
<input type="text" name="name" placeholder="Your name" />
<input type="hidden" name="l" value="YOUR_LIST_UUID" />
<button type="submit">Subscribe</button>
</form>
API subscription:
curl -X POST 'https://mail.yourdomain.com/api/subscribers' \
-u 'admin:your-admin-password' \
-H 'Content-Type: application/json' \
--data '{
"email": "user@example.com",
"name": "User Name",
"lists": [1],
"status": "enabled"
}'
Production Hardening
Performance tuning:
[app]
concurrency = 10 # Concurrent campaign workers
message_rate = 100 # Emails per second
batch_size = 1000 # Batch size per worker
Backups:
# Database backup (daily cron)
docker exec listmonk-db pg_dump -U listmonk listmonk > /backups/listmonk-$(date +%Y%m%d).sql
Updates:
docker compose pull
docker compose up -d
Monitoring:
- Monitor port 9000 with Uptime Kuma
- Track bounce rates (keep under 2%)
- Monitor SMTP queue depth
- Check DMARC reports for delivery issues
Resource Usage
| Subscribers | RAM | CPU | Disk |
|---|---|---|---|
| 1-10K | 256 MB | 1 core | 2 GB |
| 10K-100K | 512 MB | 2 cores | 5 GB |
| 100K-1M | 1 GB | 4 cores | 20 GB |
Listmonk is extraordinarily lightweight — the Go binary uses minimal resources.
VPS Recommendations
| Provider | Spec | Price |
|---|---|---|
| Hetzner | 2 vCPU, 2 GB RAM | €4.50/month |
| DigitalOcean | 1 vCPU, 1 GB RAM | $6/month |
| Linode | 1 vCPU, 1 GB RAM | $5/month |
Total cost: VPS ($5) + SMTP ($5 SES) = $10/month for 50K+ emails. Mailchimp would charge $299/month for 50K subscribers.
Compare email newsletter tools on OSSAlt — features, deliverability, and pricing side by side.