Self-hosted newsletter and mailing list manager replacing Mailchimp with high-throughput email campaigns, PostgreSQL storage, and zero per-contact fees.
Mailchimp charges monthly fees that climb with every added subscriber and isolates audience records on third-party cloud infrastructure. listmonk runs on private hardware as a single Go binary, managing millions of contacts inside a PostgreSQL database without per-seat or per-subscriber charges.
The operational difference centers on how each tool connects to external delivery and data pipelines. Mailchimp operates as a walled garden: campaigns route through internal mail servers, subscriber data synchronizes through proprietary webhooks or prepackaged SaaS plugins, and custom workflows depend on closed automation rules. In contrast, listmonk completely decouples campaign orchestration from message transmission. It connects directly to external SMTP relays like Amazon SES, Postmark, or SendGrid, and features a pluggable messenger pipeline that can forward campaign payloads as JSON over HTTP to custom endpoints for SMS, WhatsApp, or mobile push notifications.
For ops leads and engineering teams, this architecture changes how email integrates with existing backend services. Instead of pushing customer records back and forth across SaaS boundaries, listmonk stores subscriber attributes in standard PostgreSQL tables with native JSONB columns. Internal services can query subscriber segments directly with SQL, trigger transactional notifications over a structured HTTP REST API, and ingest bounce telemetry through native webhook endpoints. You lose Mailchimp's turnkey landing page designer and visual drip builder, but you gain full control over data residency and delivery infrastructure.
Is listmonk free for commercial use? Yes. You can deploy it for internal or commercial newsletters without paying licensing or subscriber fees, covering only your hosting server and third-party SMTP relay usage.
listmonk vs Mailchimp — which should I choose? Choose Mailchimp if you want managed deliverability, drag-and-drop visual landing pages, and pre-built customer journey automations. Choose listmonk if you want complete subscriber privacy in PostgreSQL, high sending throughput, and zero per-contact subscription tiers.
What email delivery providers work with listmonk? listmonk connects to any standard SMTP server, including Amazon SES, Postmark, Mailgun, and SendGrid, and provides native webhook listeners for processing bounce notices and spam complaints.
For technical teams seeking to escape per-contact billing, listmonk provides a reliable mailing engine backed by direct PostgreSQL access and clean HTTP APIs. Active development through late 2026 and an AGPL-3.0 license make it a dependable self-hosted foundation, provided you are prepared to manage your own SMTP relays and transactional deliverability.
We started this exact setup from scratch in a clean Linux machine on Oct 9, 2026 and checked it through listmonk's own API: every check passed. Recipes are re-checked regularly and removed if they stop working.
mkdir -p listmonk && cd listmonk
curl -fsSL https://open-source.store/tools/listmonk/compose.yml -o compose.ymlcat > .env <<EOF
POSTGRES_PASSWORD=$(openssl rand -hex 12)
LISTMONK_ADMIN_USER=admin
LISTMONK_ADMIN_PASSWORD=$(openssl rand -hex 12)
EOFPOSTGRES_PASSWORD — Password of the bundled PostgreSQL database. Generated above; keep it secret.LISTMONK_ADMIN_USER — Username of the Super Admin created on the first start.LISTMONK_ADMIN_PASSWORD — Password of the Super Admin created on the first start. Generated above; keep it secret.docker compose up -dx-db-credentials: &db-credentials
POSTGRES_USER: &db-user listmonk
POSTGRES_PASSWORD: &db-password ${POSTGRES_PASSWORD}
POSTGRES_DB: &db-name listmonk
services:
app:
image: listmonk/listmonk:v6.2.0
container_name: listmonk_app
restart: unless-stopped
ports:
- "9000:9000"
networks:
- listmonk
hostname: listmonk.example.com
depends_on:
db:
condition: service_healthy
command: [sh, -c, "./listmonk --install --idempotent --yes --config '' && ./listmonk --upgrade --yes --config '' && ./listmonk --config ''"]
environment:
LISTMONK_app__address: 0.0.0.0:9000
LISTMONK_db__user: *db-user
LISTMONK_db__password: *db-password
LISTMONK_db__database: *db-name
LISTMONK_db__host: db
LISTMONK_db__port: 5432
LISTMONK_db__ssl_mode: disable
LISTMONK_db__max_open: 25
LISTMONK_db__max_idle: 25
LISTMONK_db__max_lifetime: 300s
TZ: Etc/UTC
LISTMONK_ADMIN_USER: ${LISTMONK_ADMIN_USER}
LISTMONK_ADMIN_PASSWORD: ${LISTMONK_ADMIN_PASSWORD}
volumes:
- ./uploads:/listmonk/uploads:rw
healthcheck:
test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1:9000/health || exit 1"]
interval: 10s
timeout: 5s
retries: 6
db:
image: postgres:17-alpine
container_name: listmonk_db
restart: unless-stopped
networks:
- listmonk
environment:
<<: *db-credentials
healthcheck:
test: ["CMD-SHELL", "pg_isready -U listmonk"]
interval: 10s
timeout: 5s
retries: 6
volumes:
- type: volume
source: listmonk-data
target: /var/lib/postgresql/data
networks:
listmonk:
volumes:
listmonk-data:
Ubuntu 24.04 · Apple container, Docker 29.1.3, Compose 2.40.3+ds1-0ubuntu1~24.04.1. Images pulled in 65 s: postgres:17-alpine, listmonk/listmonk:v6.2.0.
Go, gRPC and Protobuf done right, LLM agents in production, and the hard parts of shipping a first product.
Show that listmonk is listed as an open-source alternative. Paste into your README:
Based on the upstream setup. Official compose. Image pinned to v6.2.0, database password and super admin credentials moved to .env, Postgres port no longer published, app waits for a healthy database and gets a healthcheck.
listmonk_app | 2026-10-09T09:41:26.266340257Z Launching listmonk with user=[root] group=[root] PUID=[0] PGID=[0] listmonk_app | 2026-10-09T09:41:26.271086923Z 2026/10/09 09:41:26.270772 main.go:106: v6.2.0 (ef0a7587 2026-06-26T10:21:29Z, linux/arm64) listmonk_app | 2026-10-09T09:41:26.271098798Z 2026/10/09 09:41:26.270953 init.go:331: connecting to db: db:5432/listmonk listmonk_app | 2026-10-09T09:41:26.308984048Z listmonk_app | 2026-10-09T09:41:26.308999757Z ** first time (idempotent) installation ** listmonk_app | 2026-10-09T09:41:26.309000715Z listmonk_app | 2026-10-09T09:41:26.356082798Z 2026/10/09 09:41:26.355980 install.go:93: creating superadmin user 'admin' listmonk_app | 2026-10-09T09:41:26.359657757Z 2026/10/09 09:41:26.359535 install.go:116: setup complete listmonk_app | 2026-10-09T09:41:26.359663882Z 2026/10/09 09:41:26.359552 install.go:117: run the program and access the dashboard at 0.0.0.0:9000 listmonk_app | 2026-10-09T09:41:26.362075965Z 2026/10/09 09:41:26.362013 main.go:106: v6.2.0 (ef0a7587 2026-06-26T10:21:29Z, linux/arm64) listmonk_app | 2026-10-09T09:41:26.362116257Z 2026/10/09 09:41:26.362093 init.go:331: connecting to db: db:5432/listmonk listmonk_app | 2026-10-09T09:41:26.398865090Z 2026/10/09 09:41:26.398499 main.go:165: running upgrade... listmonk_app | 2026-10-09T09:41:26.398921215Z 2026/10/09 09:41:26.398874 upgrade.go:74: no upgrades to run. Database is up to date. listmonk_app | 2026-10-09T09:41:26.401902340Z 2026/10/09 09:41:26.401838 main.go:106: v6.2.0 (ef0a7587 2026-06-26T10:21:29Z, linux/arm64) listmonk_app | 2026-10-09T09:41:26.401947632Z 2026/10/09 09:41:26.401919 init.go:331: connecting to db: db:5432/listmonk listmonk_app | 2026-10-09T09:41:26.452945132Z 2026/10/09 09:41:26.452870 init.go:779: media upload provider: filesystem listmonk_app | 2026-10-09T09:41:26.452977257Z 2026/10/09 09:41:26.452953 init.go:687: initialized email (SMTP) messenger: [email protected] listmonk_app | 2026-10-09T09:41:26.456599548Z ⇨ http server started on [::]:9000 listmonk_db | 2026-10-09T09:41:15.696201752Z The files belonging to this database system will be owned by user "postgres". listmonk_db | 2026-10-09T09:41:15.696224585Z This user must also own the server process. listmonk_db | 2026-10-09T09:41:15.696226835Z listmonk_db | 2026-10-09T09:41:15.696291210Z The database cluster will be initialized with locale "en_US.utf8". listmonk_db | 2026-10-09T09:41:15.696301293Z The default database encoding has accordingly been set to "UTF8". listmonk_db | 2026-10-09T09:41:15.696308293Z The default text search configuration will be set to "english". listmonk_db | 2026-10-09T09:41:15.696314252Z listmonk_db | 2026-10-09T09:41:15.696319668Z Data page checksums are disabled. listmonk_db | 2026-10-09T09:41:15.696334377Z listmonk_db | 2026-10-09T09:41:15.696368127Z fixing permissions on existing directory /var/lib/postgresql/data ... ok listmonk_db | 2026-10-09T09:41:15.697915668Z creating subdirectories ... ok listmonk_db | 2026-10-09T09:41:15.697949377Z selecting dynamic shared memory implementation ... posix listmonk_db | 2026-10-09T09:41:15.705356960Z selecting default "max_connections" ... 100 listmonk_db | 2026-10-09T09:41:15.713093752Z selecting default "shared_buffers" ... 128MB listmonk_db | 2026-10-09T09:41:15.726077502Z selecting default time zone ... UTC listmonk_db | 2026-10-09T09:41:15.726354960Z creating configuration files ... ok listmonk_db | 2026-10-09T09:41:15.772480043Z running bootstrap script ... ok listmonk_db | 2026-10-09T09:41:15.831479085Z sh: locale: not found listmonk_db | 2026-10-09T09:41:15.831489752Z 2026-10-09 09:41:15.831 UTC [35] WARNING: no usable system locales were found listmonk_db | 2026-10-09T09:41:15.957280752Z performing post-bootstrap initialization ... ok listmonk_db | 2026-10-09T09:41:16.003601960Z initdb: warning: enabling "trust" authentication for local connections listmonk_db | 2026-10-09T09:41:16.003616085Z initdb: hint: You can change this by editing pg_hba.conf or using the option -A, or --auth-local and --auth-host, the next time you run initdb. listmonk_db | 2026-10-09T09:41:16.003641293Z syncing data to disk ... ok listmonk_db | 2026-10-09T09:41:16.003643210Z listmonk_db | 2026-10-09T09:41:16.003643752Z listmonk_db | 2026-10-09T09:41:16.003644252Z Success. You can now start the database server using: listmonk_db | 2026-10-09T09:41:16.003644793Z listmonk_db | 2026-10-09T09:41:16.003645252Z pg_ctl -D /var/lib/postgresql/data -l logfile start listmonk_db | 2026-10-09T09:41:16.003645793Z listmonk_db | 2026-10-09T09:41:16.020233585Z waiting for server to start....2026-10-09 09:41:16.020 UTC [41] LOG: starting PostgreSQL 17.11 on aarch64-unknown-linux-musl, compiled by gcc (Alpine 15.2.0) 15.2.0, 64-bit listmonk_db | 2026-10-09T09:41:16.020806252Z 2026-10-09 09:41:16.020 UTC [41] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432" listmonk_db | 2026-10-09T09:41:16.022539460Z 2026-10-09 09:41:16.022 UTC [44] LOG: database system was shut down at 2026-10-09 09:41:15 UTC listmonk_db | 2026-10-09T09:41:16.023862085Z 2026-10-09 09:41:16.023 UTC [41] LOG: database system is ready to accept connections listmonk_db | 2026-10-09T09:41:16.113797127Z done listmonk_db | 2026-10-09T09:41:16.113812460Z server started listmonk_db | 2026-10-09T09:41:16.133357085Z CREATE DATABASE listmonk_db | 2026-10-09T09:41:16.134471502Z listmonk_db | 2026-10-09T09:41:16.134512835Z listmonk_db | 2026-10-09T09:41:16.134564502Z /usr/local/bin/docker-entrypoint.sh: ignoring /docker-entrypoint-initdb.d/* listmonk_db | 2026-10-09T09:41:16.134567794Z listmonk_db | 2026-10-09T09:41:16.134889085Z waiting for server to shut down....2026-10-09 09:41:16.134 UTC [41] LOG: received fast shutdown request listmonk_db | 2026-10-09T09:41:16.136394252Z 2026-10-09 09:41:16.135 UTC [41] LOG: aborting any active transactions listmonk_db | 2026-10-09T09:41:16.137510752Z 2026-10-09 09:41:16.137 UTC [41] LOG: background worker "logical replication launcher" (PID 47) exited with exit code 1 listmonk_db | 2026-10-09T09:41:16.137514044Z 2026-10-09 09:41:16.137 UTC [42] LOG: shutting down listmonk_db | 2026-10-09T09:41:16.138008752Z 2026-10-09 09:41:16.137 UTC [42] LOG: checkpoint starting: shutdown immediate listmonk_db | 2026-10-09T09:41:16.157974127Z 2026-10-09 09:41:16.157 UTC [42] LOG: checkpoint complete: wrote 926 buffers (5.7%); 0 WAL file(s) added, 0 removed, 0 recycled; write=0.006 s, sync=0.013 s, total=0.021 s; sync files=301, longest=0.005 s, average=0.001 s; distance=4266 kB, estimate=4266 kB; lsn=0/191E808, redo lsn=0/191E808 listmonk_db | 2026-10-09T09:41:16.160041085Z 2026-10-09 09:41:16.159 UTC [41] LOG: database system is shut down listmonk_db | 2026-10-09T09:41:16.238235252Z done listmonk_db | 2026-10-09T09:41:16.238250544Z server stopped listmonk_db | 2026-10-09T09:41:16.238575752Z listmonk_db | 2026-10-09T09:41:16.238579960Z PostgreSQL init process complete; ready for start up. listmonk_db | 2026-10-09T09:41:16.238580919Z listmonk_db | 2026-10-09T09:41:16.246366377Z 2026-10-09 09:41:16.246 UTC [1] LOG: starting PostgreSQL 17.11 on aarch64-unknown-linux-musl, compiled by gcc (Alpine 15.2.0) 15.2.0, 64-bit listmonk_db | 2026-10-09T09:41:16.246441544Z 2026-10-09 09:41:16.246 UTC [1] LOG: listening on IPv4 address "0.0.0.0", port 5432 listmonk_db | 2026-10-09T09:41:16.246485169Z 2026-10-09 09:41:16.246 UTC [1] LOG: listening on IPv6 address "::", port 5432 listmonk_db | 2026-10-09T09:41:16.247456002Z 2026-10-09 09:41:16.247 UTC [1] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432" listmonk_db | 2026-10-09T09:41:16.248817919Z 2026-10-09 09:41:16.248 UTC [57] LOG: database system was shut down at 2026-10-09 09:41:16 UTC listmonk_db | 2026-10-09T09:41:16.250795377Z 2026-10-09 09:41:16.250 UTC [1] LOG: database system is ready to accept connections listmonk_db | 2026-10-09T09:41:26.309205673Z 2026-10-09 09:41:26.309 UTC [68] ERROR: relation "settings" does not exist at character 22 listmonk_db | 2026-10-09T09:41:26.309214215Z 2026-10-09 09:41:26.309 UTC [68] STATEMENT: SELECT count(*) FROM settings