Skip to main content

Overview

Listmonk is a high-performance, self-hosted newsletter and mailing list manager distributed as a single Go binary — a self-hosted alternative to Mailchimp, Sendy, and Mautic. This template deploys the listmonk server on port 9000 — admin dashboard, campaign engine, public subscription and tracking pages, and the transactional-mail API — backed by PostgreSQL, with the database schema install and Super Admin bootstrap handled automatically on first boot.

Architecture

  • Listmonk — A stateful, single-replica workload serving the admin UI, the public subscription/tracking pages, and the HTTP API on port 9000. The container boots through listmonk’s own idempotent install chain: it waits for the database, installs the schema, applies any pending migrations, then starts the server. A default install is ready in about a minute with no manual setup step, and a restart re-runs the same chain safely — it detects an already-provisioned database, skips the install, and leaves existing data untouched.
  • PostgreSQL (single-instance, default) — The postgres template as a subchart: the store for all lists, subscribers, campaigns, templates, and settings.
  • PostgreSQL (HA, optional) — The postgres-highly-available template instead: 3× Patroni PostgreSQL with automatic failover, fronted by an HAProxy leader endpoint that listmonk connects through for writes and schema migrations.

What Gets Created

  • Stateful Listmonk Workload — The listmonk server on port 9000, one replica, with configurable CPU and memory.
  • Database Workloads — Single mode: one stateful PostgreSQL workload. HA mode: a stateful Patroni PostgreSQL workload, a stateful etcd workload, and a standard HAProxy leader-routing workload.
  • Volume Sets — A persistent uploads volume set mounted at /listmonk/uploads for media uploaded through the admin UI, plus the database subchart’s data volumes (10 GiB by default; per replica in HA mode).
  • Secrets — The database credentials secret created by the backing store subchart. The Super Admin credentials are not created by this template: you create that dictionary secret yourself before installing and reference it by name (see Prerequisites).
  • Identity & Policy — An identity bound to the listmonk workload, with a policy granting reveal on exactly two secrets: your admin credentials secret and the active backing store’s credentials secret.
  • Cron Backup Workload (optional) — Created in the backing PostgreSQL store when database backups are enabled.
This template does not create a GVC. You must deploy it into an existing GVC.

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. The template creates no admin secret of its own.
1

Create the admin credentials secret

A dictionary secret holding exactly the keys username and password. Secrets are org-level, so no GVC flag is involved:
Set admin.secretName to the name you used.
2

Read the password back later

-o yaml is required; without it the command prints the secret’s metadata table rather than its contents:
A username or password below the minimum length is caught at startup, by design. listmonk’s own --install step fails on every attempt when either value is too short, and that step runs inside the container’s database-wait loop — so before this check existed the only symptom was waiting for database... repeating forever, which sends you to debug a database that is perfectly healthy. The container now checks both lengths first and exits immediately with a message that names the secret to fix — measured firing 33 seconds after install, with zero waiting for database lines:
A missing prerequisite secret wedges the install rather than failing it. The install still exits 0 and reports success, every resource is created, and the workload then never starts. Because the container never ran, cpln logs returns zero lines, which reads as a broken platform rather than a missing prerequisite.The only diagnostic is status.versions[].message, which names the missing secret:
It is get-deployments — plain cpln workload get has no versions key at all. Creating the missing secret clears the wedge on its own, but slowly: recovery measured 9 minutes 31 seconds here, within the 5.5 to 10.5 minute range seen across the catalog, so poll rather than giving up. cpln workload force-redeployment RELEASE_NAME-listmonk --gvc GVC_NAME shortcuts it to roughly 90 seconds.
Two more things are configured after install rather than at install time:
  • SMTP — required before any mail is delivered. Configured in listmonk’s own admin UI (see Post-Install Setup).
  • Database backups (optional) — need a bucket and provider access set up beforehand (see Backing Up).
Once the admin secret exists, install the template using 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

Choosing a Database Mode

Exactly one of the two backing stores must be enabled — the chart enforces this at render. Listmonk is wired to the active database automatically, including for the first-boot schema install. To switch to HA mode, set postgres.enabled: false and postgresHA.enabled: true.

Configuration

Key configuration values (see the template’s values.yaml for the complete set):

Application

  • image — The official listmonk image from Docker Hub.
  • resources — CPU and memory bounds for the listmonk workload. Listmonk is a single Go binary, so the defaults suit typical installs.
  • volumeset.capacity — Initial size in GiB (minimum 10) of the uploads volume set mounted at /listmonk/uploads. It holds images and files uploaded through the admin Media page, including generated thumbnails, and survives restarts and redeploys.
  • timezone — The container timezone, which governs the times used for scheduled campaigns.

Admin Bootstrap

  • admin.secretName — Name of the dictionary secret you created in Prerequisites, holding the username and password of the Super Admin. The credentials never pass through values, and the template creates no secret of its own — it references yours and grants the workload reveal on exactly that one secret.
  • The account is created during the first boot’s schema install, from the secret’s contents. Because the values are no longer visible when the chart renders, the container enforces upstream’s minimum lengths at startup instead — see the length warning in Prerequisites.
The Super Admin is created on the first install only. Afterwards the account lives in the database, and editing the secret does not update it. Manage users afterwards in Admin → Settings → Users.

Access

  • publicAccess.enabled — Serve listmonk on the auto-assigned *.cpln.app HTTPS canonical endpoint. It is deliberately on by default and load-bearing: the subscriber-facing pages — subscription forms, unsubscribe links, and tracking pixels — are served to your subscribers on the open internet, so a private instance cannot do the job. The admin UI and admin API on the same endpoint remain authentication-gated, behind a credential you created rather than a published default. Set to false for an internal-only instance: external requests are then refused at the edge, and in-GVC callers still reach it per internalAccess.
  • internalAccess.type — Controls which workloads can reach listmonk over the internal network:
Firewall changes take roughly 30 seconds to a few minutes to propagate after an upgrade reports success — re-test rather than trusting the first response.

Backing Store

Enable exactly one of postgres (single-instance, default) or postgresHA (HA) — see Choosing a Database Mode. In both modes, change the database password before installing (postgres.credentials.password / postgres.credentials.password); it seeds the database on first boot and cannot be changed by editing values afterwards. If you run more than one release of this template in the same organization, give each its own postgres.config.credentialsSecretName. Secret names are organization-wide, so a second release left on the default name is refused at install and creates nothing — the first release is unaffected. The database holds everything except uploaded media: lists, subscribers, campaigns, templates, users, and all of the settings you configure in the admin UI.
Template version 1.0.0 did not compact the etcd cluster inside the bundled highly available database, so etcd’s backend grows with time alone and goes read-only once it reaches its 2 GiB quota — after roughly 110 days — taking PostgreSQL failover with it. Only installs running the HA database are affected (postgresHA.enabled, which is off by default here); see etcd History Compaction for the mechanism and the symptoms. Upgrade to 1.0.1 or later to turn compaction on: that stops further growth but cannot shrink a backend that has already grown, and a cluster that has already raised a NOSPACE alarm needs operator recovery rather than an upgrade.

Upgrading From 1.0.x

Version 1.1.0 moved the Super Admin login out of Helm values. Earlier versions shipped a username and a working password as values, used exactly as written — a published default guarding a login form on the public endpoint, sitting in the Helm release for the life of the install.
An upgrade that still carries either removed key is rejected at render, before anything is applied. Each guard names its replacement, and there is no compatibility fallback — the version bump is the migration path. A real upgrade carrying an old key failed at render, created no Helm revision, and left the running release healthy and untouched:
Leaving admin.secretName empty is refused the same way.
An existing install’s Super Admin password does not change on upgrade. The account was written to the database on the first install and LISTMONK_ADMIN_* is never read again, so the secret’s contents only matter to a fresh install. Change the password in Admin → Settings → 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.
To upgrade an existing install:
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.

Connecting

A default install reaches ready in about a minute (62 seconds measured). Once the workload reports ready, open https://<canonical>.cpln.app/admin and sign in with the credentials from your admin secret — the schema is already installed and the Super Admin already exists, so there is no setup wizard and no manual install step.

Post-Install Setup

Mail delivery and object-storage media are listmonk settings stored in its database, not template values. Configure them in the admin UI after installing:
1

Configure SMTP

In Admin → Settings → SMTP, add your mail provider (Amazon SES, SendGrid, Mailgun, Postmark, or any SMTP relay) and save. Until a working SMTP server is configured, campaigns still run to completion but no mail is delivered.
2

Set the root URL

In Admin → Settings → General, set the root URL to your canonical *.cpln.app endpoint, or to your custom domain once you attach one, so that links and tracking URLs inside your emails point at the right host.
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 Admin → Settings → Media.

Backing Up

Database backups are optional and disabled 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, and settings, but not the uploaded media on the listmonk volume set. Enable with postgres.backup.enabled or postgresHA.backup.enabled (matching your database mode), and complete the storage setup for your provider before installing.
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:
In HA mode, postgresHA.backup.mode selects logical (scheduled pg_dump) or wal-g (continuous WAL archiving). The full per-provider walkthrough lives in the backing postgres and postgres-highly-available template documentation.

Important Notes

  • Single instance by design — there is no replicas knob. Listmonk runs its campaign workers in-process, so two instances against one database 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, which means an upgrade or restart causes a brief gap in availability instead of overlapping senders. Durability comes from PostgreSQL and the uploads volume set.
  • Create the admin secret before installing — a missing prerequisite secret leaves the workload waiting on something that does not exist, with zero log lines. See Prerequisites for how to diagnose it.
  • Mind the minimum credential lengths — a username under 3 characters or a password under 8 makes upstream’s install step fail; the container catches this at startup and names the secret instead of looping on a misleading database message.
  • Change the database password before installing (postgres.credentials.password / postgres.credentials.password) — it is bundled plumbing, used exactly as given, and it remains a value by design. Both the admin account and the database are seeded on first boot only.
  • No mail is delivered until SMTP is configured in Admin → Settings → SMTP. Before that, starting a campaign is not an error — it runs and finishes with zero messages sent.
  • Keep publicAccess enabled for subscriber-facing pages to work. Subscription forms, unsubscribe links, and tracking pixels must be reachable from the internet; the admin UI and API stay authentication-gated either way.
  • Use /health for external health checks, not /api/health — the latter requires an authenticated session.
  • Volumes survive reinstalls under the same release name; uninstalling deletes them — the database volumes and the uploads volume set go with the release, taking all lists, subscribers, campaigns, and media with them. Use postgresHA and/or enable backups for durable 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