How to Self-Host Listmonk: Email Newsletter and Mailing List Server 2026
TL;DR
Listmonk (AGPL 3.0, ~14K GitHub stars, Go + React) is a self-hosted email newsletter and mailing list manager — a free Mailchimp replacement. It manages subscribers, sends campaigns, tracks opens and clicks, handles bounces, and segments audiences. Mailchimp charges $13/month for 500 subscribers; Listmonk costs only the price of your transactional email provider (SES, Postmark, Mailgun). Single Go binary + PostgreSQL — handles millions of subscribers.
Key Takeaways
- Listmonk: AGPL 3.0, ~14K stars, Go — Mailchimp alternative, high performance
- Unlimited subscribers: No per-subscriber pricing — pay only for email delivery
- Templates: HTML + plain text email templates with Go templating
- Segmentation: Lists + dynamic subscriber segments with custom attributes
- Bounce handling: Automatic bounce and complaint processing (SES/Postmark webhooks)
- API: Full REST API for programmatic subscriber management
Listmonk vs Mailchimp vs Ghost Newsletters
| Feature | Listmonk | Mailchimp | Ghost |
|---|---|---|---|
| License | AGPL 3.0 | Proprietary | MIT |
| Cost (500 subs) | Free + SMTP | $13/mo | Free hosting |
| Cost (10K subs) | Free + SMTP | $100/mo | ~$25/mo cloud |
| Transactional | Via SMTP provider | No | No |
| Subscriber attributes | Custom fields | Merge tags | No |
| Dynamic segments | Yes | Yes | No |
| Open/click tracking | Yes | Yes | Yes |
| Double opt-in | Yes | Yes | Yes |
| Bounce handling | Yes | Yes | Yes |
| Multi-list | Yes | Yes | Via publications |
| API | Full REST | REST | Content API |
| Self-hosted | Yes | No | Yes |
Part 1: Docker Setup
# docker-compose.yml
services:
listmonk:
image: listmonk/listmonk:latest
container_name: listmonk
restart: unless-stopped
ports:
- "9000:9000"
volumes:
- ./config.toml:/listmonk/config.toml:ro
- listmonk_uploads:/listmonk/uploads
depends_on:
db:
condition: service_healthy
command: ["./listmonk", "--config", "/listmonk/config.toml"]
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_DB: listmonk
POSTGRES_USER: listmonk
POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U listmonk"]
interval: 10s
volumes:
listmonk_uploads:
postgres_data:
# config.toml
[app]
address = "0.0.0.0:9000"
[db]
host = "db"
port = 5432
user = "listmonk"
password = "YOUR_POSTGRES_PASSWORD"
database = "listmonk"
ssl_mode = "disable"
max_open = 25
max_idle = 25
max_lifetime = "300s"
# Initialize the database:
docker compose run --rm listmonk ./listmonk --install
# Start:
docker compose up -d
Visit http://your-server:9000 → log in (default: admin / listmonk — change immediately).
Part 2: HTTPS with Caddy
newsletter.yourdomain.com {
reverse_proxy localhost:9000
}
Part 3: Configure Email (SMTP)
Settings → SMTP
Amazon SES
SMTP Host: email-smtp.us-east-1.amazonaws.com
SMTP Port: 587
Auth: STARTTLS
Username: your-ses-smtp-username
Password: your-ses-smtp-password
From: "Your Newsletter" <newsletter@yourdomain.com>
Postmark
SMTP Host: smtp.postmarkapp.com
SMTP Port: 587
Username: your-postmark-server-api-token
Password: your-postmark-server-api-token
From: newsletter@yourdomain.com
Self-Hosted (Maddy/Postfix)
SMTP Host: mailserver.yourdomain.com
SMTP Port: 587
From: newsletter@yourdomain.com
Test the configuration with Send Test Email.
Part 4: Subscriber Management
Create a Mailing List
- Lists → + New List
- Name: "Weekly Newsletter"
- Type: Public (users can subscribe via form) or Private (admin-only)
- Opt-in type: Double opt-in (recommended) or Single opt-in
Import Subscribers
email,name
alice@example.com,Alice Smith
bob@example.com,Bob Jones
- Subscribers → Import → Upload CSV
- Map CSV columns to subscriber fields
- Assign to list(s)
- Listmonk imports and (optionally) sends double opt-in confirmation
Custom Subscriber Attributes
Add custom data per subscriber:
{
"plan": "premium",
"signup_date": "2026-01-15",
"country": "US"
}
Use in email templates: {{ .Subscriber.Attribs.plan }}
Subscription Form
Listmonk generates an embeddable HTML form:
<form action="https://newsletter.yourdomain.com/subscription/form" method="post">
<input type="hidden" name="nonce" />
<input type="email" name="email" placeholder="Your email" required />
<input type="text" name="name" placeholder="Your name" />
<input type="hidden" name="l" value="LIST_UUID" />
<button type="submit">Subscribe</button>
</form>
<script src="https://newsletter.yourdomain.com/public/static/listmonk.js"></script>
Part 5: Creating Campaigns
Draft a Campaign
- Campaigns → + New Campaign
- Name: "March Newsletter"
- Subject: "Your March Update"
- From name/email
- Select lists: "Weekly Newsletter"
- Template: Choose or create
Email Templates
Listmonk uses Go templates:
<!-- template.html -->
<!DOCTYPE html>
<html>
<body style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h1>{{ .Campaign.Name }}</h1>
<p>Hi {{ .Subscriber.FirstName }},</p>
{{ template "content" . }}
<hr>
<p style="font-size: 12px; color: #666;">
You're receiving this because you subscribed to {{ .Lists.String }}.
<a href="{{ .UnsubscribeURL }}">Unsubscribe</a>
</p>
</body>
</html>
Campaign Content (Markdown or HTML)
# March Update
This month we shipped:
- **Feature A**: Description of feature A
- **Feature B**: Description of feature B
[Read the full changelog →](https://yourdomain.com/changelog)
Thanks for reading,
The Team
Schedule or Send
- Send Now: Delivers immediately
- Schedule: Pick a future date/time
- Batch size: Controls send rate (e.g., 1000/hour to avoid spam filters)
Part 6: Analytics
After sending, Campaigns → [Campaign] → Stats:
- Sent: Total emails delivered
- Opens: % opened (requires tracking pixel)
- Clicks: % clicked any link
- Bounces: Hard/soft bounces (requires bounce webhook)
- Unsubscribes: Unsubscribe rate
Part 7: Bounce Handling (SES)
Automatically remove hard-bounced emails:
- In AWS SES: Email → Notifications → Bounce topic → SNS topic
- SNS → Subscribe → HTTPS endpoint:
https://newsletter.yourdomain.com/webhooks/service/ses - In Listmonk: Settings → Bounce → Enable SES bounces
Listmonk automatically:
- Disables hard-bounced subscribers
- Marks complaint emails as unsubscribed
Part 8: REST API
# Get API credentials: Settings → API Users
# List subscribers:
curl https://newsletter.yourdomain.com/api/subscribers \
-u "admin:your-password" | jq '.data.results[].email'
# Add subscriber:
curl -X POST https://newsletter.yourdomain.com/api/subscribers \
-u "admin:your-password" \
-H "Content-Type: application/json" \
-d '{
"email": "newuser@example.com",
"name": "New User",
"status": "enabled",
"lists": [1],
"preconfirm_subscriptions": true
}'
# Get campaign stats:
curl https://newsletter.yourdomain.com/api/campaigns/42/stats \
-u "admin:your-password" | jq '.data'
Maintenance
# Update Listmonk:
docker compose pull
docker compose up -d
# Backup database:
docker exec db pg_dump -U listmonk listmonk \
| gzip > listmonk-db-$(date +%Y%m%d).sql.gz
# Logs:
docker compose logs -f listmonk
# Cleanup old analytics (optional, saves space):
# Settings → Maintenance → Purge analytics
See all open source email marketing and newsletter tools at OSSAlt.com/categories/marketing.