> ## Documentation Index
> Fetch the complete documentation index at: https://controlplanecorporation-majid-docs-content-expansion.mintlify.site/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 a prerequisite Super Admin secret, 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).
* **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](#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.

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

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

<Steps>
  <Step title="Create the admin credentials secret">
    A [dictionary secret](/guides/create-secret/dictionary) holding exactly the keys `username` and `password`. Secrets are org-level, so no GVC flag is involved:

    ```bash theme={null}
    cpln secret create-dictionary --name my-listmonk-admin \
      --entry username=admin \
      --entry password="$(openssl rand -hex 24)"
    ```

    Set `admin.secretName` to the name you used.
  </Step>

  <Step title="Read the password back later">
    `-o yaml` is required; without it the command prints the secret's metadata table rather than its contents:

    ```bash theme={null}
    cpln secret reveal my-listmonk-admin -o yaml
    ```
  </Step>
</Steps>

| Key        | What it is                                                                                  |
| ---------- | ------------------------------------------------------------------------------------------- |
| `username` | The Super Admin's login name. **Minimum 3 characters** — an upstream requirement.           |
| `password` | Its password. **Minimum 8 characters.** The login form it guards is on the public endpoint. |

<Warning>
  **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:

  ```text theme={null}
  listmonk: the my-listmonk-admin secret must hold a username of at least 3 characters and a password
  of at least 8 (upstream install requirement)
  ```
</Warning>

<Warning>
  **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:

  ```bash theme={null}
  cpln workload get-deployments RELEASE_NAME-listmonk --gvc GVC_NAME -o yaml
  ```

  ```text theme={null}
  The secret my-listmonk-admin no longer exists. Workload updates are paused until
  the secret is added or the reference to the secret removed.
  ```

  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.
</Warning>

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](#post-install-setup)).
* **Database backups** *(optional)* — need a bucket and provider access set up beforehand (see [Backing Up](#backing-up)).

Once the admin secret exists, 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)

# REQUIRED PREREQUISITE SECRET — CREATE IT BEFORE YOU INSTALL. A `dictionary`
# secret holding exactly two keys, `username` (min 3 chars) and `password`
# (min 8 chars); the login form it guards sits on the public endpoint. The
# Super Admin is created on FIRST install only — editing the secret later does
# NOT update the account.
admin:
  secretName: my-listmonk-admin # name of your pre-created dictionary secret

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+
  credentials: # the chart writes these into the secret named below — nothing for you to create
    username: listmonk
    password: change-me-listmonk-db # change before installing
    database: listmonk
  config:
    credentialsSecretName: my-listmonk-db-credentials # secret names are org-wide — give each release its own
  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:
  secretName: my-listmonk-admin # your pre-created dictionary secret (see Prerequisites)
```

* `admin.secretName` — Name of the dictionary secret you created in [Prerequisites](#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](#prerequisites).

<Warning>
  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**.
</Warning>

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

| 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 roughly 30 seconds to a few minutes to propagate after an upgrade reports success — re-test rather than trusting the first response.
</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.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.

<Warning>
  **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](/template-catalog/templates/postgres-highly-available#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.
</Warning>

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

|                            | `1.0.x`                                                                     | `1.1.0`                                                                                |
| -------------------------- | --------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| Super Admin login          | `admin.username` and `admin.password` values                                | `admin.secretName` — a dictionary secret you create, holding `username` and `password` |
| Chart-created admin secret | Held the admin credentials                                                  | None; the template creates no admin secret at all                                      |
| Minimum-length check       | At render, from the values                                                  | At container startup, from the secret — with a message naming the secret               |
| Database password          | A value (`postgres.credentials.password` / `postgres.credentials.password`) | Unchanged — still a value, because it is bundled plumbing nobody logs in with          |

<Warning>
  **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:

  ```text theme={null}
  listmonk: admin.password was REMOVED in 1.1.0. Put it in a `dictionary` secret (key: `password`)
  together with `username`, and set admin.secretName to that secret's name. Create the secret BEFORE
  installing; see Prerequisites in the README.
  ```

  Leaving `admin.secretName` empty is refused the same way.
</Warning>

<Note>
  **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.
</Note>

To upgrade an existing install:

<Steps>
  <Step title="Create the admin credentials secret">
    Follow [Prerequisites](#prerequisites). Putting your current credentials in it keeps your values file honest, but it does not re-seed the account.
  </Step>

  <Step title="Drop the removed keys from your values">
    Remove `admin.username` and `admin.password`, and set `admin.secretName` instead.
  </Step>

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

## 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 `username` / `password` in your `admin.secretName` secret — `cpln secret reveal my-listmonk-admin -o yaml` |
| Database (single mode)   | `{release}-postgres.{gvc}.cpln.local:5432`                                                                     |
| Database (HA mode)       | `{release}-postgres-ha-proxy.{gvc}.cpln.local:5432` (HAProxy leader endpoint)                                  |

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:

<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. No Cloud Account is required — the credentials authenticate directly. The two backing stores take these differently. In HA mode (`postgresHA`), set `backup.minio.accessKey` and `backup.minio.secretKey` to credentials with access to the bucket. In single-instance mode (`postgres`), those two values were removed: create a [dictionary secret](/guides/create-secret/dictionary) holding the keys `accessKey` and `secretKey`, and set `backup.minio.credentialsSecretName` to its name — see [MinIO backup prerequisites](/template-catalog/templates/postgres#minio) for the exact command.
      </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.
* **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](#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

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