listmonk is a self-hosted newsletter and mailing-list manager written in Go, using PostgreSQL as its datastore and shipped as a single binary. It’s fast, feature-rich, and comes with a modern web UI for campaigns, lists, and subscribers. For ops teams it’s particularly attractive: fewer moving parts, more control, and predictable cost versus SaaS.
This guide summarizes how to put it into production, operate it safely, and make the right surrounding decisions (reverse proxy, backups, upgrades, security, scalability).
1) Reference Architecture
- Application:
listmonkbinary (API + UI). It’s stateless by design, so you can run multiple replicas behind a load balancer if needed. - Data: PostgreSQL stores all state (subscribers, lists, campaigns, logs).
- SMTP: listmonk sends mail via your MTA or an SMTP provider (configure in
config.toml). - Recommended stack: TLS-terminating reverse proxy (Nginx/Traefik), persistent storage for Postgres, monitoring, and backups.
License: AGPLv3. Keep this in mind if you redistribute modifications.
2) Quick Start with Docker (recommended)
The project publishes an official image and a sample docker-compose.yml:
# 1) Fetch the official compose file
curl -LO https://raw.githubusercontent.com/knadh/listmonk/master/docker-compose.yml
# 2) Start services
docker compose up -d
# 3) Access the UI
open http://localhost:9000
Code language: PHP (php)
Production tips
- Declare volumes to persist PostgreSQL and (if customized) the app’s
config.toml. - Put a reverse proxy with TLS in front (see Nginx below).
- Expose only the proxy’s port; keep
:9000private on the container network. - Tune container resources (memory/CPU,
ulimits). - For upgrades:
docker compose pull && docker compose up -dthen runlistmonk --upgrade(see §6).
3) Native Install (binary + systemd)
If you prefer not to use Docker:
# 1) Download the release for your arch
# https://github.com/knadh/listmonk/releases
tar -xzf listmonk_<ver>_linux_amd64.tar.gz -C /opt/listmonk/
# 2) Generate configuration
cd /opt/listmonk
./listmonk --new-config # creates config.toml -> edit it (DB, SMTP, base_url, etc.)
# 3) Initialize (or upgrade) the PostgreSQL schema
./listmonk --install # or --upgrade
# 4) Start
./listmonk
# UI: http://localhost:9000
Code language: PHP (php)
systemd unit (minimal example):
[Unit]
Description=listmonk newsletter service
After=network-online.target postgresql.service
Wants=network-online.target
[Service]
User=listmonk
Group=listmonk
WorkingDirectory=/opt/listmonk
ExecStart=/opt/listmonk/listmonk --config /etc/listmonk/config.toml
Restart=on-failure
LimitNOFILE=65535
[Install]
WantedBy=multi-user.target
Code language: JavaScript (javascript)
4) Reverse Proxy & TLS (Nginx)
server {
listen 80;
server_name mail.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name mail.example.com;
ssl_certificate /etc/letsencrypt/live/mail.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mail.example.com/privkey.pem;
location / {
proxy_set_header Host $host;
proxy_set_header X-R eal-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_pass http://127.0.0.1:9000;
proxy_read_timeout 120s;
}
}
Code language: PHP (php)
Set base_url in config.toml to https://mail.example.com.
5) PostgreSQL Prep & Basic Tuning
listmonk performs write- and read-intensive operations (subscribers, sends, events). Make sure the DB is healthy:
- Version: use PostgreSQL 14+ (or whatever upstream recommends).
- Connections: cap
max_connections; add pgBouncer if your app or SMTP provider causes many short-lived connections. - Initial tuning: use
pgtuneas a baseline (RAM/CPU/IO); watchshared_buffers,work_mem,max_wal_size,checkpoint_timeout. - Maintenance: keep
autovacuumon; export metrics and alert on bloat/lag.
6) Safe Upgrade Cycle
- Application
- Docker:
docker compose pull && docker compose up -d. - Binary: replace the executable with the new release.
- Docker:
- DB schema
- Run
./listmonk --upgrade. It is idempotent—safe to run multiple times. - Always take a Postgres backup before schema changes.
- Run
- Rollback
- Keep the previous binary.
- If the release included migrations, check release notes for down-migrations (usually not). On failure, restore the backup.
7) Backups & Restore
PostgreSQL example (off-host):
# Backup (compressed custom format)
PGHOST=db.internal PGUSER=listmonk PGPASSWORD=*** \
pg_dump -Fc -d listmonk > /backups/listmonk_$(date +%F).dump
# Restore
createdb -U postgres listmonk_restore
pg_restore -U postgres -d listmonk_restore /backups/listmonk_2025-11-01.dump
Code language: PHP (php)
Automate with systemd timers or your backup tool; replicate to external storage and test restores regularly.
8) Deliverability: DNS & SMTP
listmonk manages campaigns, but delivery depends on your MTA/provider:
- Set up SPF, DKIM, DMARC.
- Use an SMTP provider with good reputation (or manage your own Postfix/Exim and IP hygiene).
- Respect provider rate limits; tune send concurrency accordingly.
- Process bounces; ensure proper opt-in and clear unsubscribe.
SMTP settings live in
config.toml. See listmonk docs for exact fields and provider specifics.
9) Day-2 Ops & Observability
- Logs: centralize stdout/stderr (journald, Loki/ELK).
- Metrics: monitor PostgreSQL (Prometheus +
postgres_exporter) and the reverse proxy. - Alerts: UI uptime on
:9000, send queues (if applicable), latencies, 5xx/4xx, DB disk usage. - Capacity: track app/DB CPU/RAM, IOPS, table growth (subscribers/events can grow fast).
10) Scalability
- Application: since it’s stateless, run N replicas behind a LB (Nginx/HAProxy/Traefik) to increase concurrency or isolate sending workers from the UI.
- Database: scale vertically and optimize first; for heavy reads add read replicas. Sharding only if justified—it complicates deletes, reporting, and joins.
- Queues/Jobs: schedule large blasts off-peak and honor provider throttling.
11) Security
- Mandatory TLS at the proxy; HSTS if appropriate.
- Admin access: restrict by VPN/IP allowlist if your risk profile requires it.
- Secrets: env vars or a secret store; no secrets in repos.
- PostgreSQL: least privilege accounts, encryption in transit (
hostssl), encrypted backups. - Updates: track listmonk releases and Go/PostgreSQL security patches.
12) Clean-Install Procedure (Docker, condensed)
- DNS and TLS ready (
mail.example.com). curl -LOthe officialdocker-compose.yml.- Edit credentials and SMTP in
config.toml(mount a volume). docker compose up -d.- Put Nginx/Traefik in front; keep
:9000private. - Create the admin on first login; configure lists and templates.
- Set up backups & monitoring on day 0.
Ops FAQ
Can I move to another host without losing data?
Yes. listmonk is stateless; migrate PostgreSQL and config.toml. With Docker, move volumes too. Update base_url if the FQDN changes.
How do I upgrade the schema with minimal downtime?
Take a snapshot/backup; fetch the new binary/image; run --upgrade (idempotent). Schedule a short window and run smoke tests.
What if I send millions of emails?
The app is built for high throughput, but the real limit is usually the MTA/SMTP and the DB. Use provider rate limits, scale the app horizontally, and size PostgreSQL (I/O, WAL, checkpoints).
Can I put it behind corporate SSO?
Yes. Place it behind your reverse proxy with OIDC/SAML (e.g., oauth2-proxy in front of :9000). Keep a local break-glass admin.
Conclusion
Scaling and operating your own mailer doesn’t have to be painful. listmonk reduces the problem to one binary + PostgreSQL, with strong performance and a small operational surface. As with any production system, success lives in the basics: TLS, backups, monitoring, rehearsed upgrades, and an architecture you can draw on a whiteboard. Start small, measure, and add complexity only when the numbers demand it.
