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 credentials are not created by this template. You create that dictionary secret before installing and reference it by name — see Prerequisites.

Prerequisites

One dictionary secret must exist before you install. The Super Admin guards a login form on the public endpoint, so its credentials are a prerequisite secret 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 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.
2

Read the credentials back later

Pass -o yaml. A bare cpln secret reveal prints only a summary table, not the values:
Create the secret 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. 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. The bundled database password is not a prerequisite — it is internal plumbing no human types elsewhere, so this template creates that secret for you from postgres.credentials.* (change the password before installing). 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 secret you created and a database password of your own:
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 database password seeds PostgreSQL on its first boot and is used exactly as given — change it before installing, not after. 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 are the same postgres.credentials.* values above, written by this template into the secret named here.
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 postgres.credentials.*.
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

Version 1.2.0 moved the bundled database to the postgres 3.4.1 template, which no longer takes credentials as values. Listmonk absorbed that change rather than passing it on, so there is no new prerequisite — only a rename on the default (single-instance) path: Carrying an old key fails the render with the PostgreSQL template’s message, which tells you to create a dictionary secret yourself. Ignore that advice here — this template creates it. Move the three keys and you are done.

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. Carrying the old block forward fails at render with the postgres block was REMOVED in 2.5.0. Delete the postgres block under postgresHA and set postgres.credentials.* to the same values it held — the HA database was initialised with them. This template writes them into the secret named by postgresHA.config.credentialsSecretName, so do not create that secret yourself. 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.

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 did not exist at install time. The container never ran, so there is no output to log.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 postgres.credentials.* 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 credentials were changed after the first boot, restore the original values; 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: both releases use the same postgres.config.credentialsSecretName (or postgresHA.config.credentialsSecretName). Secret names are org-wide, and the first release owns the default my-listmonk-db-credentials. Nothing is shared, overwritten or deleted — the second install simply creates nothing.Fix: give the second release its own secret name and install 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), or resources.cpu / resources.memory (1.4.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, or 1.3.1 — then upgrade again.

Important Notes

  • Create the admin secret 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.
  • Change postgres.credentials.password before installing, not after. It seeds the database on its first boot and is used exactly as given; changing it afterwards leaves listmonk with a password the database does not know.
  • Give each listmonk release in an org its own postgres.config.credentialsSecretName. Secret names are org-wide; a second release left on the default name is refused at install.
  • 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