> ## Documentation Index
> Fetch the complete documentation index at: https://docs.controlplane.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Listmonk

> Deploy listmonk on Control Plane using the Template Catalog. Self-hosted newsletter and mailing list manager — a Mailchimp alternative — with automatic schema install and admin bootstrap on first boot, persistent media uploads, public subscription pages, and a single-instance or highly available PostgreSQL backing store.

## 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-catalog/templates/postgres) template as a subchart: the store for all lists, subscribers, campaigns, templates, and settings.
* **PostgreSQL (HA, optional)** — The [postgres-highly-available](/template-catalog/templates/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).
* **Secret** — A dictionary secret holding the admin bootstrap username and password. The database credentials secret is created by the backing store subchart.
* **Identity & Policy** — An identity bound to the listmonk workload, with a policy granting `reveal` on exactly two secrets: the admin bootstrap secret and the active backing store's credentials secret.
* **Cron Backup Workload** *(optional)* — Created in the backing PostgreSQL store when database backups are enabled.

<Note>
  This template does not create a GVC. You must deploy it into an existing GVC.
</Note>

## Prerequisites

None for a default install — it deploys with a working PostgreSQL backend, an installed schema, a bootstrapped admin account, and an auto-assigned HTTPS endpoint. Two 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](#post-install-setup)).
* **Database backups** *(optional)* — need a bucket and provider access set up beforehand (see [Backing Up](#backing-up)).

Install the template using your preferred method:

<CardGroup cols={2}>
  <Card title="UI" href="/template-catalog/install-manage/ui" icon="laptop">
    Browse, install, and manage templates visually
  </Card>

  <Card title="CLI" href="/template-catalog/install-manage/cli" icon="terminal">
    Manage templates from your terminal
  </Card>

  <Card title="Terraform" href="/template-catalog/install-manage/terraform" icon={<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><g fill-rule="evenodd"><path d="M77.941 44.5v36.836L46.324 62.918V26.082zm0 0" fill="#5c4ee5"/><path d="M81.41 81.336l31.633-18.418V26.082L81.41 44.5zm0 0" fill="#4040b2"/><path d="M11.242 42.36L42.86 60.776V23.941L11.242 5.523zm0 0M77.941 85.375L46.324 66.957v36.82l31.617 18.418zm0 0" fill="#5c4ee5"/></g></svg>}>
    Declare templates in your Terraform configurations
  </Card>

  <Card
    title="Pulumi"
    href="/template-catalog/install-manage/pulumi"
    icon={<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" id="Pulumi-Icon--Streamline-Svg-Logos" height="24" width="24">
    <desc>
        Pulumi Icon Streamline Icon: https://streamlinehq.com
    </desc>
    <path fill="#f26e7e" d="M4.683025 13.3318c0.869125 -0.5018 0.870575 -2.1264 0.003225 -3.62865s-2.27504 -2.313275 -3.1441725 -1.811475C0.672945 8.3935 0.6715 10.0181 1.53885 11.52035c0.86735 1.502275 2.27505 2.313275 3.144175 1.81145Zm0.0052 3.2167c0.86735 1.502275 0.865925 3.126875 -0.003225 3.628675 -0.86915 0.5018 -2.2768275 -0.309225 -3.144175 -1.81145 -0.8673525 -1.50225 -0.8659075 -3.126875 0.003225 -3.628675 0.8691325 -0.5018 2.276825 0.309225 3.144175 1.81145Zm5.922875 3.4243c0.86735 1.50225 0.8659 3.126775 -0.003225 3.62875 -0.869125 0.501775 -2.27685 -0.309325 -3.1442 -1.81155 -0.867325 -1.50225 -0.865875 -3.12685 0.00325 -3.628675 0.869125 -0.5018 2.276825 0.309225 3.144175 1.811475Zm-0.001925 -6.845275c0.86735 1.50225 0.8659 3.12685 -0.003225 3.628675 -0.869125 0.5018 -2.276825 -0.309225 -3.144175 -1.811475 -0.86735 -1.50225 -0.8659 -3.12685 0.003225 -3.62865 0.869125 -0.501825 2.276825 0.3092 3.144175 1.81145Z" stroke-width="0.25"></path>
    <path fill="#8a3391" d="M22.45775 11.524125c0.86725 -1.502225 0.865925 -3.12685 -0.003225 -3.62865 -0.869125 -0.501825 -2.276825 0.3092 -3.144175 1.811475 -0.86735 1.50225 -0.8659 3.126825 0.003225 3.62865 0.869125 0.501825 2.276825 -0.3092 3.144175 -1.811475Zm0.000175 3.2151c0.869075 0.5018 0.870625 2.1264 0.003225 3.62865 -0.86735 1.50225 -2.27505 2.313275 -3.144175 1.81145 -0.869125 -0.5018 -0.870575 -2.126425 -0.003225 -3.62865 0.86735 -1.50225 2.27505 -2.313275 3.144175 -1.81145ZM16.536225 18.157875c0.86915 0.501825 0.8706 2.126425 0.00325 3.628675 -0.86735 1.502125 -2.275075 2.313225 -3.1442 1.81145 -0.869125 -0.50175 -0.870575 -2.126425 -0.003225 -3.62865 0.867375 -1.502275 2.27505 -2.3133 3.144175 -1.811475Zm-0.003325 -6.843775c0.869125 0.5018 0.870575 2.126425 0.003225 3.628675s-2.27505 2.313275 -3.1442 1.811475c-0.869125 -0.501825 -0.870575 -2.126425 -0.003225 -3.628675 0.86735 -1.502275 2.27505 -2.313275 3.1442 -1.811475Z" stroke-width="0.25"></path>
    <path fill="#f7bf2a" d="M15.138225 2.06721c0 1.003615 -1.40625 1.817215 -3.14095 1.817215 -1.7347 0 -3.14095 -0.8136 -3.14095 -1.817215C8.856325 1.06359 10.262575 0.25 11.997275 0.25c1.7347 0 3.14095 0.81359 3.14095 1.81721ZM9.2166 5.482375c0 1.003625 -1.40625 1.8172 -3.14095 1.8172 -1.7347 0 -3.14095 -0.813575 -3.14095 -1.8172s1.40625 -1.817225 3.14095 -1.817225c1.7347 0 3.14095 0.8136 3.14095 1.817225Zm8.71005 1.8172c1.7347 0 3.14095 -0.813575 3.14095 -1.8172s-1.40625 -1.817225 -3.14095 -1.817225c-1.7347 0 -3.14095 0.8136 -3.14095 1.817225s1.40625 1.8172 3.14095 1.8172Zm-2.788425 1.605625c0 1.003625 -1.40625 1.8172 -3.14095 1.8172 -1.7347 0 -3.14095 -0.813575 -3.14095 -1.8172 0 -1.0036 1.40625 -1.8172 3.14095 -1.8172 1.7347 0 3.14095 0.8136 3.14095 1.8172Z" stroke-width="0.25"></path>
    </svg>}
  >
    Declare templates in your Pulumi programs
  </Card>
</CardGroup>

## 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.

|                   | `postgres` (default)                   | `postgresHA`                                         |
| ----------------- | -------------------------------------- | ---------------------------------------------------- |
| What runs         | One single-replica PostgreSQL workload | 3× Patroni PostgreSQL, etcd, HAProxy leader endpoint |
| Database failover | None                                   | Automatic (Patroni leader election)                  |
| Best for          | Development and lightweight installs   | Production                                           |

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):

```yaml theme={null}
image: listmonk/listmonk:v6.2.0 # official Docker Hub image

resources: # single Go binary — light footprint
  cpu: 500m
  memory: 512Mi
  minCpu: 150m
  minMemory: 256Mi

# Persistent media uploads (filesystem media store), mounted at /listmonk/uploads
volumeset:
  capacity: 10 # initial capacity in GiB (minimum is 10)

admin:
  username: admin                    # min 3 chars
  password: change-me-listmonk-admin # min 8 chars — change before installing

timezone: Etc/UTC # container TZ — governs campaign scheduling times

publicAccess:
  enabled: true # HTTPS admin UI + public subscription/tracking pages via the canonical *.cpln.app endpoint

internalAccess:
  type: same-gvc # options: none, same-gvc, same-org, workload-list
  workloads: []  # only used when type is same-gvc or workload-list

postgres: # default: single-instance PostgreSQL
  enabled: true
  image: postgres:18 # listmonk requires Postgres 12+
  config:
    username: listmonk
    password: change-me-listmonk-db # change before installing
    database: listmonk
  volumeset:
    capacity: 10 # initial capacity in GiB (minimum is 10)
  backup:
    enabled: false        # true = scheduled DB backups to object storage
    schedule: "0 2 * * *" # daily at 2am UTC
    provider: aws         # aws | gcp | minio
    aws:
      bucket: my-backup-bucket
      region: us-east-1
      cloudAccountName: my-backup-cloudaccount
      policyName: my-backup-policy
      prefix: listmonk/backups

postgresHA: # durable HA: 3-replica Patroni store with an HAProxy leader endpoint (disable postgres first)
  enabled: false
  postgres:
    username: listmonk
    password: change-me-listmonk-db # change before installing
    database: listmonk
  replicas: 3
  volumeset:
    capacity: 10 # initial capacity in GiB per replica (minimum is 10)
  backup:
    enabled: false # true = scheduled backups to object storage
    mode: logical  # logical | wal-g
    provider: aws  # aws | gcp | minio
    aws:
      bucket: my-backup-bucket
      region: us-east-1
      cloudAccountName: my-backup-cloudaccount
      policyName: my-backup-policy
      prefix: listmonk/backups
```

### 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

```yaml theme={null}
admin:
  username: admin                    # min 3 chars
  password: change-me-listmonk-admin # min 8 chars — change before installing
```

These become the Super Admin account created during the first boot's schema install, and are stored in the template-managed `{release}-listmonk-admin` dictionary secret. The chart rejects a username shorter than 3 characters or a password shorter than 8 at render time, before anything is deployed.

<Warning>
  The Super Admin is created on the **first install only**. Afterwards the account lives in the database, and changing `admin.username` / `admin.password` in values does not update it. Set a strong password before you install; manage users afterwards in **Admin → Settings → Users**.
</Warning>

### Access

* `publicAccess.enabled` — Serve listmonk on the auto-assigned `*.cpln.app` HTTPS canonical endpoint (default). This is what makes the subscriber-facing pages — subscription forms, unsubscribe links, and tracking pixels — reachable from the internet. The admin UI and admin API on the same endpoint remain authentication-gated. 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:

| Type            | Description                                                            |
| --------------- | ---------------------------------------------------------------------- |
| `none`          | No internal access.                                                    |
| `same-gvc`      | Allow access from all workloads in the same GVC (default).             |
| `same-org`      | Allow access from all workloads in the same organization.              |
| `workload-list` | Allow access only from workloads listed in `internalAccess.workloads`. |

<Note>
  Firewall changes take up to a couple of minutes to propagate after a `helm upgrade` reports success.
</Note>

### Backing Store

Enable exactly one of `postgres` (single-instance, default) or `postgresHA` (HA) — see [Choosing a Database Mode](#choosing-a-database-mode). In both modes, **change the database password before installing** (`postgres.config.password` / `postgresHA.postgres.password`); it seeds the database on first boot and cannot be changed by editing values afterwards.

The database holds everything except uploaded media: lists, subscribers, campaigns, templates, users, and all of the settings you configure in the admin UI.

## Connecting

| What                     | Value                                                                                                     |
| ------------------------ | --------------------------------------------------------------------------------------------------------- |
| Public URL               | `status.canonicalEndpoint` of `{release}-listmonk` (`cpln workload get {release}-listmonk -o yaml`)       |
| Admin UI / login         | `https://<canonical>.cpln.app/admin`                                                                      |
| Public subscription page | `https://<canonical>.cpln.app/subscription/form`                                                          |
| Health check             | `https://<canonical>.cpln.app/health` — public and unauthenticated (`/api/health` requires a session)     |
| HTTP API                 | `https://<canonical>.cpln.app/api/...` — authenticated; lists, subscribers, campaigns, transactional mail |
| Internal (same GVC)      | `http://{release}-listmonk.{gvc}.cpln.local:9000`                                                         |
| Admin credentials        | The `admin.username` / `admin.password` values, stored in the `{release}-listmonk-admin` secret           |
| Database (single mode)   | `{release}-postgres.{gvc}.cpln.local:5432`                                                                |
| Database (HA mode)       | `{release}-postgres-ha-proxy.{gvc}.cpln.local:5432` (HAProxy leader endpoint)                             |

Once the workload reports ready, open `https://<canonical>.cpln.app/admin` and sign in with the bootstrap credentials — 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:

<Steps>
  <Step title="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.
  </Step>

  <Step title="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.
  </Step>

  <Step title="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**.
  </Step>
</Steps>

## 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.

<Tabs>
  <Tab title="AWS S3">
    <Steps>
      <Step title="Create a bucket">
        Create an S3 bucket. Set `backup.aws.bucket` and `backup.aws.region` to match.
      </Step>

      <Step title="Set up a Cloud Account">
        If you do not have one, [create a Cloud Account](https://docs.controlplane.com/guides/create-cloud-account) for your AWS account. Set `backup.aws.cloudAccountName` to its name.
      </Step>

      <Step title="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:

        ```json theme={null}
        {
          "Version": "2012-10-17",
          "Statement": [
            { "Effect": "Allow", "Action": ["s3:ListBucket"], "Resource": "arn:aws:s3:::my-backup-bucket" },
            { "Effect": "Allow", "Action": ["s3:PutObject", "s3:GetObject", "s3:DeleteObject"], "Resource": "arn:aws:s3:::my-backup-bucket/*" }
          ]
        }
        ```
      </Step>
    </Steps>
  </Tab>

  <Tab title="Google Cloud Storage">
    <Steps>
      <Step title="Create a bucket">
        Create a GCS bucket. Set `backup.gcp.bucket` to its name.
      </Step>

      <Step title="Set up a Cloud Account">
        If you do not have one, [create a Cloud Account](https://docs.controlplane.com/guides/create-cloud-account) for your GCP project and grant its service account **Storage Object Admin** (`roles/storage.objectAdmin`) on the bucket. Set `backup.gcp.cloudAccountName` to its name.
      </Step>
    </Steps>
  </Tab>

  <Tab title="S3-compatible (MinIO)">
    <Steps>
      <Step title="Create a bucket">
        Create your bucket on the server. Set `backup.minio.bucket` to its name.
      </Step>

      <Step title="Set the endpoint and credentials">
        Set `backup.minio.endpoint` to the S3 API address including port, and `backup.minio.accessKey` / `backup.minio.secretKey` to credentials with access to the bucket. No Cloud Account is required — the keys authenticate directly.
      </Step>
    </Steps>
  </Tab>
</Tabs>

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](/template-catalog/templates/postgres) and [postgres-highly-available](/template-catalog/templates/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.
* **Change `admin.password` and the database password before installing.** Both are applied on the first boot only; editing them afterwards does not change the existing account or database.
* **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

<CardGroup cols={2}>
  <Card title="Listmonk Documentation" icon="book" href="https://listmonk.app/docs/">
    Official listmonk documentation
  </Card>

  <Card title="Configuration Reference" icon="sliders" href="https://listmonk.app/docs/configuration/">
    Settings, SMTP options, and filesystem or S3 media storage
  </Card>

  <Card title="Concepts" icon="list" href="https://listmonk.app/docs/concepts/">
    How lists, subscribers, campaigns, and templates fit together
  </Card>

  <Card title="API Reference" icon="code" href="https://listmonk.app/docs/apis/apis/">
    Manage lists, subscribers, and campaigns over HTTP
  </Card>

  <Card title="Listmonk Template" icon="github" href="https://github.com/controlplane-com/templates/tree/main/listmonk">
    View the source files, default values, and chart definition
  </Card>
</CardGroup>
