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 port9000, 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.
What Gets Created
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.Create the database credentials secret
username, password and database. The bundled PostgreSQL initializes with it and listmonk connects with it: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.Create the admin credentials secret
username (at least 3 characters) and password (at least 8 characters) — both are upstream requirements:admin.secretName to the name you used.Read the credentials back later
-o yaml. A bare cpln secret reveal prints only a summary table, not the values:Installation
Install into an existing GVC, naming the admin and database credentials secrets you created:UI
CLI
Terraform
Pulumi
Configuration
Application
Admin Bootstrap
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
http://localhost:9000, which would break every one of those links. The container therefore sets it at boot, before serving:
rootUrlset — written on every boot, so the value in your values file always wins. Set it to your custom domain once you attach one.rootUrlempty andpublicAccess.enabled: true— the canonical*.cpln.appendpoint is written, but only while the setting still holds the untouchedhttp://localhost:9000default. A value you later change in Settings → General is kept.rootUrlempty andpublicAccess.enabled: false— nothing is written; set the Root URL yourself in Settings → General.
Access
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 ofpostgres or postgresHA must be enabled — the chart refuses to render otherwise.
Highly Available PostgreSQL
SetpostgresHA.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.
Connecting
/admin in your browser:
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:
Configure SMTP
Check the Root URL
*.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.Choose a media store (optional)
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 withpostgres.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).
- AWS S3
- Google Cloud Storage
- S3-compatible (MinIO)
Create a bucket
backup.aws.bucket and backup.aws.region to match.Set up a Cloud Account
backup.aws.cloudAccountName to its name.Create a bucket-scoped IAM policy
backup.aws.policyName to its name: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 hostRELEASE_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.
Upgrading from 1.0.x
Version1.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.
1.0.x default (change-me-listmonk-admin), treat that password as compromised and change it now.
Create the admin credentials secret
Drop the removed keys from your values
admin.username and admin.password, and set admin.secretName instead.Upgrade
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
Version1.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
Version1.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:
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
Version1.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:
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: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.
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):
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
replicasknob. 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: truegives a three-replica cluster with automatic failover — see Highly Available PostgreSQL. - The first
cpln helm upgradeafter an install may restart the bundled database even when nothing changed; listmonk waits for it and comes back on its own.
Troubleshooting
Workload never starts and cpln logs returns zero lines
Workload never starts and cpln logs returns zero lines
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:cpln workload force-redeployment RELEASE_NAME-listmonk --gvc GVC_NAME to pick it up immediately.Container exits immediately with a message naming the admin secret
Container exits immediately with a message naming the admin secret
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:Boot log repeats waiting for database
Boot log repeats waiting for database
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.Subscription, unsubscribe or tracking links point at localhost:9000
Subscription, unsubscribe or tracking links point at localhost:9000
WARN could not set app.root_url line. On a private install (publicAccess.enabled: false with no rootUrl) nothing sets it.Fix: set rootUrl to the address subscribers use (or correct it in Settings → General) — see Root URL. Check the boot log with:Campaign finishes with zero messages sent
Campaign finishes with zero messages sent
Upgrade is refused at render naming a removed key
Upgrade is refused at render naming a removed key
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 ROLEfirst 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
publicAccessenabled for subscriber-facing pages to work, and keep the Root URL equal to the address subscribers use — see Root URL. - Use
/healthfor external health checks, not/api/health, which requires an authenticated session. - Data lives on the volume sets and survives redeploys and upgrades;
cpln helm uninstalldeletes them — all lists, subscribers, campaigns, and media go with the release. Enable backups for production data.