Skip to main content

Overview

Listmonk is a high-performance, self-hosted newsletter and mailing list manager distributed as a single Go binary — an alternative to Mailchimp, Sendy, and Mautic. This template deploys the listmonk server (admin dashboard, campaign engine, public subscription and tracking pages, and the transactional-mail API) on port 9000, backed by a bundled PostgreSQL store. The database schema is installed and the Super Admin account is created automatically on first boot, so a default install needs no setup wizard. The bundled database is single-instance by default; set postgresHA.enabled: true (and postgres.enabled: false) for a three-replica Patroni cluster with automatic failover. Listmonk itself always runs as exactly one replica — see Scaling and Availability.
This template does not create a GVC. You must deploy it into an existing GVC.

What Gets Created

The Super Admin and database credentials are not created by this template. You create those dictionary secrets before installing and reference them by name — see Prerequisites.

Prerequisites

Two dictionary secrets must exist before you install. They hold credentials, so they are prerequisite secrets rather than template values — a value would sit in plaintext in the Helm release for the life of the install. Secrets are org-level, so no GVC flag is involved.
1

Create the database credentials secret

A dictionary secret with the keys username, password and database. The bundled PostgreSQL initializes with it and listmonk connects with it:
Set its name in the key of the store you use: postgres.config.credentialsSecretName (single-instance, the default) or postgresHA.config.credentialsSecretName (HA). Secret names are org-wide, so give each listmonk release its own secret. PostgreSQL reads the secret only when its data is first initialized — see Rotating the Database Password for changing it later.
2

Create the admin credentials secret

A dictionary secret holding exactly two keys, username (at least 3 characters) and password (at least 8 characters) — both are upstream requirements:
Set admin.secretName to the name you used.
3

Read the credentials back later

Pass -o yaml. A bare cpln secret reveal prints only a summary table, not the values:
Create both secrets before installing, or the deployment wedges silently. A name that points at a secret which does not exist installs “successfully” and then never starts. The container never runs, so cpln logs returns zero lines — there is nothing to log. The one place the reason appears is status.versions[].message:
Use get-deployments — plain cpln workload get has no versions key; a missing database secret shows the same way on RELEASE_NAME-postgres (or RELEASE_NAME-postgres-ha in HA mode). Creating the missing secret repairs the deployment on its own, or run cpln workload force-redeployment RELEASE_NAME-listmonk --gvc GVC_NAME to pick it up immediately.
Nothing else is required for a default install. Database backups are optional and need a bucket and provider access first — see Backing Up. SMTP is configured in listmonk’s own admin UI after installing — see First Run.

Installation

Install into an existing GVC, naming the admin and database credentials secrets you created:
Or follow the instructions for your preferred method:

UI

Browse, install, and manage templates visually

CLI

Manage templates from your terminal

Terraform

Declare templates in your Terraform configurations

Pulumi

Declare templates in your Pulumi programs

Configuration

Application

Admin Bootstrap

The secret named here must exist before you install — see Prerequisites. Its username and password are read only during the first boot’s schema install, when listmonk creates the Super Admin. After that the account lives in the database: editing the secret does not update it, and an upgrade does not change your existing password. Manage users in Admin → Users.

Root URL

Listmonk builds every subscriber-facing link — subscription forms, unsubscribe links, the public archive, open and click tracking, and the links inside sent email — from its Root URL setting, so it must match the address subscribers actually use. The setting lives in listmonk’s database and a fresh install seeds it to http://localhost:9000, which would break every one of those links. The container therefore sets it at boot, before serving:
  • rootUrl set — written on every boot, so the value in your values file always wins. Set it to your custom domain once you attach one.
  • rootUrl empty and publicAccess.enabled: true — the canonical *.cpln.app endpoint is written, but only while the setting still holds the untouched http://localhost:9000 default. A value you later change in Settings → General is kept.
  • rootUrl empty and publicAccess.enabled: false — nothing is written; set the Root URL yourself in Settings → General.

Access

Public access is on by default and is load-bearing: subscription forms, unsubscribe links, and tracking pixels are served to subscribers on the open internet. The admin UI and API on the same endpoint stay behind listmonk’s own login, guarded by the credential you created. Set publicAccess.enabled: false only for an internal-only instance; see Connecting for how to reach a private install.

Single-Instance PostgreSQL

The default backing store. Exactly one of postgres or postgresHA must be enabled — the chart refuses to render otherwise.
The credentials secret seeds PostgreSQL on its first boot; editing it afterwards does not change the database password — see Rotating the Database Password. Provider setup for backups is under Backing Up.

Highly Available PostgreSQL

Set postgresHA.enabled: true for near-zero-downtime database upgrades and automatic failover: a three-replica Patroni cluster coordinated by etcd, fronted by an HAProxy leader endpoint that listmonk connects through for writes and schema migrations (the chart refuses to render if that proxy is disabled). The credentials come from the prerequisite secret named by postgresHA.config.credentialsSecretName.
The HA store’s own tuning (Patroni resources, etcd compaction, PgBouncer) is documented on the PostgreSQL Highly Available page.

Connecting

Read the public address once the workload is ready, then open /admin in your browser:
If you installed with publicAccess.enabled: false, reach the admin UI through a tunnel instead and open http://localhost:9000/admin:

First Run

There is no setup wizard — the schema is installed and the Super Admin exists when the workload reports ready. Sign in at /admin with the credentials from your admin secret, then finish the two settings listmonk keeps in its database rather than in Helm values:
1

Configure SMTP

In Settings → SMTP, add your mail provider (Amazon SES, SendGrid, Mailgun, Postmark, or any SMTP relay) and save. Until a working SMTP server is configured, no mail is delivered — a campaign still runs to completion, with every message failing to send.
2

Check the Root URL

In Settings → General, confirm the Root URL is the address your subscribers will use. On a public install the container has already set it to the canonical *.cpln.app endpoint (or to rootUrl if you set one); on a private install, or if the boot log shows a WARN could not set app.root_url line, set it here by hand. See Root URL for how the template manages it.
3

Choose a media store (optional)

Filesystem storage on the bundled uploads volume set works out of the box. To store media in S3-compatible object storage instead, switch the provider in Settings → Media.

Operations

Backing Up

Database backups are off by default. When enabled, a scheduled backup job runs inside the backing PostgreSQL store and uploads to your bucket under the configured prefix — covering lists, subscribers, campaigns, templates, and settings, but not the media on the uploads volume set. Enable with postgres.backup.enabled: true or postgresHA.backup.enabled: true to match your database mode, and complete the storage setup for your provider before installing. In HA mode, postgresHA.backup.mode selects logical (scheduled pg_dump) or wal-g (continuous WAL archiving).
1

Create a bucket

Create an S3 bucket. Set backup.aws.bucket and backup.aws.region to match.
2

Set up a Cloud Account

If you do not have one, create a Cloud Account for your AWS account. Set backup.aws.cloudAccountName to its name.
3

Create a bucket-scoped IAM policy

Create an IAM policy granting the required S3 actions on the bucket, and set backup.aws.policyName to its name:
Media uploads are not part of the database backup. The uploads volume set takes a final snapshot when it is deleted (retained for 7 days), and you can snapshot it yourself at any time:

Restoring a Backup

A restore replays the SQL dump into the backing database from a client inside the GVC that can reach both the bucket and the database — the per-provider commands are on the PostgreSQL page for single mode and the PostgreSQL Highly Available page for HA mode. For this template, substitute the host RELEASE_NAME-postgres.GVC_NAME.cpln.local (single) or RELEASE_NAME-postgres-ha-proxy.GVC_NAME.cpln.local (HA), and the username, password and database values from your database credentials secret.
This restore path has not been verified against a live install of this template; it is the backing store’s own documented procedure. Restore while listmonk is not serving campaigns, and expect to restart the listmonk workload afterwards so it re-reads its settings. Restoring the uploads volume set from a snapshot is likewise unverified here — see Volume Set snapshots.

Upgrading from 1.0.x

Version 1.1.0 moved the Super Admin login out of Helm values and into the prerequisite dictionary secret. An upgrade that still carries either removed key is refused at render, before anything is applied, with a message naming its replacement — there is no compatibility fallback.
An existing install’s Super Admin password does not change on upgrade. The account was written to the database on the first install and the secret is never read again, so its contents only matter to a fresh install. Change the password in Admin → Users. If the install is still carrying the published 1.0.x default (change-me-listmonk-admin), treat that password as compromised and change it now.
1

Create the admin credentials secret

Follow Prerequisites. Putting your current credentials in it keeps your values file honest, but it does not re-seed the account.
2

Drop the removed keys from your values

Remove admin.username and admin.password, and set admin.secretName instead.
3

Upgrade

The single replica stops before the new one starts, so expect a brief gap. Lists, subscribers, campaigns, and uploaded media are on the database and the uploads volume set and are untouched.

Upgrading from 1.1.0

postgres.config.username, .password and .database were removed. Put the values your single-instance database was initialized with into the database credentials secret under Prerequisites (keys username, password, database), set postgres.config.credentialsSecretName to its name, and remove the three keys. With postgres.backup.provider: minio, postgres.backup.minio.accessKey / .secretKey were likewise removed — put them in a dictionary secret holding accessKey and secretKey as shown in Backing Up and set postgres.backup.minio.credentialsSecretName to its name.

Upgrading from 1.2.0

Version 1.3.0 adopted the postgres-highly-available 2.5.0 template for the HA path. It affects only installs with postgresHA.enabled: true; single-instance installs need no action. Put the values of postgresHA.postgres.username / .password / .database unchanged into the database credentials secret under Prerequisites, set postgresHA.config.credentialsSecretName to its name, and delete the postgres block under postgresHA. The HA database was initialized with those values; a different password only changes the secret, and listmonk then fails to log in to its own database. If you use MinIO backups on the HA path, create a dictionary secret holding accessKey and secretKey as shown in Backing Up and set postgresHA.backup.minio.credentialsSecretName to its name.

Upgrading from 1.3.1

Version 1.4.0 renamed the listmonk resource limits so it is unambiguous which number is the ceiling. An upgrade that still carries the old names is refused at render: Version 1.5.0 then added rootUrl and the boot-time Root URL handling described under Root URL; no values change is required for it. Then follow Upgrading from 1.5.0.

Upgrading from 1.5.0

Version 1.6.0 removed postgres.credentials: the database credentials are now a prerequisite secret, the same one the database templates require on their own. Carrying the old key forward fails the render. 1.5.0 created the database secret itself, so this upgrade deletes it. Before upgrading, create a secret under a new name holding the values your database was initialized with — not new ones, or listmonk loses its database connection:
Then upgrade with postgres.config.credentialsSecretName (single-instance mode) or postgresHA.config.credentialsSecretName (HA mode) set to that name, and postgres.credentials removed. Your current values are in postgres.credentials of the 1.5.0 release, and also in the old secret (cpln secret reveal SECRET_NAME -o yaml, where SECRET_NAME is the credentialsSecretName your release used, my-listmonk-db-credentials by default) until the upgrade deletes it. The upgrade restarts the bundled PostgreSQL too (it now reads the new secret), so listmonk can briefly lose its database; it reconnects by itself. While access to the new secret propagates, the new version of a workload can wait in get-deployments reporting that it is not allowed to reveal the secret; the previous version keeps serving meanwhile, and it clears by itself.

Upgrading from 1.6.0 or earlier

1.6.1 moves the HA database (postgresHA) to controlplanecorporation/patroni-postgres:0.8, which carries pgvector 0.8.7 (CVE-2026-103484). The fix takes effect when the upgrade restarts the Patroni members; existing data needs nothing. Nothing limits that rollout, so expect a brief interruption of writes while the leader changes. listmonk itself restarts as on any upgrade, which is the longer interruption, because its single replica stops before the new one serves; a few writes on already-open database connections can also fail right after the leader change. Upgrade in a quiet window. If you pinned postgresHA.image in your values, set it to the 0.8 tag: tags 0.1 to 0.7 are refused at render. listmonk does not use the vector extension; if you created it yourself in a database, \dx keeps reporting the old version until you run ALTER EXTENSION vector UPDATE; there, which only updates that record. Single-instance mode (postgres) is unchanged.

Rotating the Database Password

A running workload keeps the credential values it started with, and PostgreSQL reads the secret only when its data is first initialized, so editing the secret alone does not change the database password. Change one key and keep the others with export, edit, apply:
Single-instance mode: run ALTER ROLE for the secret’s username in the database first, then update the secret, then right away force a redeployment of RELEASE_NAME-listmonk. Do not redeploy the database; restarting it only adds downtime.
HA mode needs its own order. The replication role repl shares the password, and a member that restarts with the old password can be promoted to leader, which splits the cluster. Move the leader to member -0 first, so it restarts last, after every other member already has the new password. pgx runs a command in one member (LOCATION is your GVC’s location, DB_USERNAME the secret’s username):
Then force-redeploy RELEASE_NAME-listmonk, which keeps the old password until it restarts. The app cannot open new database connections from the ALTER ROLE until its own redeploy finishes, so rotate in a quiet window. If a member stays starting, or running without streaming, re-copy it from the leader. This copies the whole database, so it costs network traffic in proportion to its size:

Scaling and Availability

  • Listmonk is a single instance by design — there is no replicas knob. Listmonk runs its campaign workers in-process, and upstream allows only one instance per database: two would send every campaign twice. The workload is pinned to one replica and rolls out without surge — the old replica stops before the new one starts — so an upgrade or restart causes a brief gap in availability rather than overlapping senders. Do not hand-scale the workload or point a second copy at the same database.
  • Database availability is the one knob you have. The default single-instance store restarts on upgrade and reschedules on node failure; postgresHA.enabled: true gives a three-replica cluster with automatic failover — see Highly Available PostgreSQL.
  • The first cpln helm upgrade after an install may restart the bundled database even when nothing changed; listmonk waits for it and comes back on its own.

Troubleshooting

Cause: the secret named by admin.secretName, or the database credentials secret, did not exist at install time. The container never ran, so there is no output to log. A missing database secret shows the same way on RELEASE_NAME-postgres (or RELEASE_NAME-postgres-ha).Fix: read the one place the reason appears:
Create the secret as shown in Prerequisites. The deployment recovers on its own, or run cpln workload force-redeployment RELEASE_NAME-listmonk --gvc GVC_NAME to pick it up immediately.
Cause: the username is under 3 characters or the password under 8. Upstream’s install step would otherwise fail on every attempt inside the database-wait loop, which looks like a database problem — so the container checks the lengths first and names the real cause.Fix: update the secret with compliant values, then force a redeployment:
Cause: PostgreSQL is not accepting connections yet. A few of these lines on a fresh install are normal while the database initializes (the HA store also needs its leader endpoint to settle). If they continue indefinitely, listmonk cannot authenticate — typically because the database credentials secret was changed after the database volume was first initialized, so listmonk presents a password the database never received.Fix: check the database workload with cpln workload get-deployments RELEASE_NAME-postgres --gvc GVC_NAME -o yaml (or RELEASE_NAME-postgres-ha in HA mode). If the secret was changed after the first boot, restore the original values or follow Rotating the Database Password; the password is applied to the database on its first boot only.
Cause: no SMTP server is configured. Listmonk stores SMTP settings in its database, not in boot configuration, so a fresh install cannot deliver mail; each message fails individually and the campaign still completes.Fix: add your provider in Settings → SMTP (see First Run) and start the campaign again.
Cause: your values file still carries a key removed in a later version — admin.username / admin.password (1.1.0), postgres.config.username / .password / .database (1.2.0), resources.cpu / resources.memory (1.4.0), or postgres.credentials (1.6.0). Each guard names its replacement, and nothing is applied, so the running release is untouched.Fix: follow the matching subsection above — 1.0.x, 1.1.0, 1.3.1, or 1.5.0 — then upgrade again.

Important Notes

  • Create the admin and database credentials secrets before installing. A reference to a secret that does not exist wedges the workload with zero log lines; see Prerequisites for the one command that shows the reason.
  • The Super Admin is created on first boot only. Editing the secret later does not change the account, and upgrades never change your existing password — manage users in Admin → Users.
  • Editing the database credentials secret does not change the database password. Run ALTER ROLE first and then force-redeploy listmonk; in HA mode move the leader first — see Rotating the Database Password.
  • Give each listmonk release in an org its own database credentials secret. Secret names are org-wide, so two releases pointed at one secret share a password.
  • Do not scale listmonk past one replica or point a second instance at the same database — duplicate campaign sends result. See Scaling and Availability.
  • No mail is delivered until SMTP is configured in Settings → SMTP. Starting a campaign before that is not an error — it finishes with nothing sent.
  • Keep publicAccess enabled for subscriber-facing pages to work, and keep the Root URL equal to the address subscribers use — see Root URL.
  • Use /health for external health checks, not /api/health, which requires an authenticated session.
  • Data lives on the volume sets and survives redeploys and upgrades; cpln helm uninstall deletes them — all lists, subscribers, campaigns, and media go with the release. Enable backups for production data.

External References

Listmonk Documentation

Official listmonk documentation

Configuration Reference

Settings, SMTP options, and filesystem or S3 media storage

Concepts

How lists, subscribers, campaigns, and templates fit together

API Reference

Manage lists, subscribers, and campaigns over HTTP

Listmonk Template

View the source files, default values, and chart definition