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

# Chatwoot

> Deploy Chatwoot, the open-source live chat, shared inbox and omnichannel agent desk (Community Edition) with a background worker, a single-node Redis and a PostgreSQL that is highly available by default or single-instance. Needs an existing GVC and a dictionary secret holding Chatwoot's four keys.

## Overview

Chatwoot is an open-source customer engagement platform — an embeddable live-chat widget, a shared email inbox, and an omnichannel agent desk in one application. This template deploys the **Community Edition** (`-ce` image, MIT core): a Rails web tier serving the dashboard, REST API, widget and WebSockets on port `3000`, a Sidekiq worker that also runs database migrations, a bundled single-node Redis, and a pgvector-capable PostgreSQL — highly available by default, or single-instance with one flag. Attachments go to a persistent volume or to object storage.

<Note>
  This template does not create a GVC. It deploys into an existing [GVC](/reference/gvc) that you already have; pass it with `--gvc GVC_NAME`.
</Note>

### What Gets Created

| Resource | Name | Purpose | Optional |
| - | - | - | - |
| Workload | `RELEASE_NAME-chatwoot` | Rails web tier — dashboard, API, chat widget and WebSockets (stateful, `chatwoot.replicas` replicas, port `3000`) | — |
| Workload | `RELEASE_NAME-chatwoot-worker` | Sidekiq worker; also runs migrations and the first-run bootstrap (standard, fixed at 1 replica) | — |
| Workload | `RELEASE_NAME-chatwoot-redis` | Bundled Redis — queues, live updates, cache (stateful, fixed at 1 replica, port `6379`) | — |
| Workload | `RELEASE_NAME-postgres-ha` | Patroni PostgreSQL cluster, pgvector included (stateful, `postgresHA.replicas` replicas) | `postgresHA.enabled: true` (default) |
| Workload | `RELEASE_NAME-etcd` | etcd consensus store for Patroni (stateful) | `postgresHA.enabled: true` (default) |
| Workload | `RELEASE_NAME-postgres-ha-proxy` | HAProxy leader endpoint Chatwoot connects to (standard) | `postgresHA.enabled: true` (default) |
| Workload | `RELEASE_NAME-postgres-ha-backup` | Scheduled logical database backup (cron) | `postgresHA.backup.enabled: true` with `mode: logical` |
| Workload | `RELEASE_NAME-postgres` | Single-instance PostgreSQL on a pgvector image (stateful) | `postgres.enabled: true` |
| Workload | `RELEASE_NAME-postgres-backup` | Scheduled logical database backup (cron) | `postgres.backup.enabled: true` |
| Volume Set | `RELEASE_NAME-chatwoot-storage` | Local attachments at `/app/storage` | `storage.type: local` (default) |
| Volume Set | `RELEASE_NAME-chatwoot-redis-vs` | Redis append-only file at `/data` | — |
| Volume Set | `RELEASE_NAME-postgres-ha-vs`, `RELEASE_NAME-etcd-vs` | Database and etcd data, one volume per replica | `postgresHA.enabled: true` (default) |
| Volume Set | `RELEASE_NAME-pg-vs` | Single-instance database data | `postgres.enabled: true` |
| Secret | `my-chatwoot-db-credentials` | Dictionary secret with the database `username`, `password` and `database`, built by this template from `postgres.credentials.*`; the name comes from `postgresHA.config.credentialsSecretName` or `postgres.config.credentialsSecretName` | — |
| Secret | `RELEASE_NAME-chatwoot-creds` | Dictionary secret holding the bundled Redis password | — |
| Secret | `RELEASE_NAME-chatwoot-web-start`, `RELEASE_NAME-chatwoot-worker-start` | Start scripts for the web and worker containers | — |
| Secret | `RELEASE_NAME-etcd-startup`, `RELEASE_NAME-patroni-startup`, `RELEASE_NAME-postgres-proxy-startup`, `RELEASE_NAME-wal-g-backup-script` | Start scripts of the highly available database | `postgresHA.enabled: true` (default) |
| Secret | `RELEASE_NAME-postgres-config` | Non-sensitive backup settings of the highly available database | `postgresHA.backup.enabled: true` |
| Identity | `RELEASE_NAME-chatwoot-identity` | Shared by the web and worker workloads; carries the AWS cloud account link when `storage.type: s3` | — |
| Policy | `RELEASE_NAME-chatwoot-policy` | Grants the identity `reveal` on exactly the secrets Chatwoot reads, including your prerequisite secrets | — |
| Identity, Policy | `RELEASE_NAME-postgres-ha-identity`, `RELEASE_NAME-postgres-ha-policy`, `RELEASE_NAME-etcd-identity`, `RELEASE_NAME-etcd-policy` | Database and etcd identities and their secret access | `postgresHA.enabled: true` (default) |
| Identity, Policy | `RELEASE_NAME-pg-identity`, `RELEASE_NAME-pg-policy` | Single-instance database identity and its secret access | `postgres.enabled: true` |

Your keys secret (see [Prerequisites](#prerequisites)) is **not** created by the template.

## Prerequisites

Chatwoot signs its sessions and encrypts sensitive columns with four keys that you supply through a [dictionary secret](/guides/create-secret/dictionary) created **before** installing. The values never pass through Helm values.

<Steps>
  <Step title="Create the keys secret">
    All four keys are required. Secrets are org-level, so no `--gvc` flag is involved:

    ```bash theme={null}
    cpln secret create-dictionary --name CHATWOOT_KEYS_SECRET_NAME \
      --entry SECRET_KEY_BASE="$(openssl rand -hex 64)" \
      --entry ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY="$(openssl rand -hex 16)" \
      --entry ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY="$(openssl rand -hex 16)" \
      --entry ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT="$(openssl rand -hex 16)"
    ```

    Set `secrets.name` to this name.
  </Step>

  <Step title="Back the four values up">
    Store a copy outside Control Plane. All four keys are write-once for the life of the installation: rotating `SECRET_KEY_BASE` logs out every user, and rotating any `ACTIVE_RECORD_ENCRYPTION_*` key makes stored two-factor secrets undecryptable, locking out every agent who enabled MFA.
  </Step>

  <Step title="Choose the two bundled passwords">
    The database password (`postgres.credentials.password`) and the Redis password (`redis.auth.password`) ship as `change-me-…` placeholders that the template uses as-is. Pick strong values and pass them at install time — both seed their component on first boot and are not updated by later value edits. The Redis password may contain only letters, digits, `-` and `_`.
  </Step>
</Steps>

<Warning>
  **If the keys secret does not exist at install time, the deployment wedges silently.** `cpln logs` returns zero lines — the container never starts, so it has nothing to log. The one place the reason appears is `status.versions[].message`:

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

  Note this is `get-deployments` — plain `cpln workload get` has no `versions` field. Creating the secret repairs the deployment on its own after several minutes, or force a redeployment to skip the wait.
</Warning>

Everything else works with the defaults. Three optional features need their own setup first:

* **Object-storage attachments** — an existing bucket plus either a Control Plane cloud account (AWS S3) or a static-key dictionary secret (S3-compatible servers). See [Attachment Storage](#attachment-storage). Required for more than one web replica.
* **Authenticated SMTP** — a dictionary secret holding `SMTP_USERNAME` and `SMTP_PASSWORD`. See [Email](#email).
* **Database backups** — a bucket and access setup for AWS S3, Google Cloud Storage or an S3-compatible server. See [Backing Up](#backing-up).

## Installation

Once the keys secret exists, install into your existing GVC with the two bundled passwords set:

```bash theme={null}
cpln helm install RELEASE_NAME oci://ghcr.io/controlplane-com/templates/chatwoot \
  --version 1.2.1 --gvc GVC_NAME \
  --set secrets.name=CHATWOOT_KEYS_SECRET_NAME \
  --set postgres.credentials.password='YOUR-STRONG-DB-PASSWORD' \
  --set redis.auth.password='YOUR-STRONG-REDIS-PASSWORD'
```

To install using another method, follow the instructions for it:

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

## Configuration

Each subsection shows the shipped defaults for one top-level values area.

### Chatwoot Web

```yaml theme={null}
chatwoot:
  image: chatwoot/chatwoot:v4.16.2-ce   # Community Edition (MIT core); pin a released tag, never latest
  replicas: 1                 # >1 REQUIRES storage.type s3 or s3-compatible — local attachments are per-replica
  frontendUrl: ""             # public base URL in widget snippets and email links; empty = the canonical endpoint; set WITH https:// for a custom domain
  resources:
    minCpu: 250m
    maxCpu: 1000m
    minMemory: 512Mi
    maxMemory: 2Gi            # a Rails process under boot migrations OOMs below this
```

Both the web and worker workloads run `chatwoot.image`. Leave `chatwoot.frontendUrl` empty unless you front Chatwoot with a custom domain; both tiers then advertise the web workload's public URL in links.

### Sidekiq Worker

```yaml theme={null}
worker:
  concurrency: 10             # SIDEKIQ_CONCURRENCY; also the worker's Postgres pool size
  resources:
    minCpu: 250m
    maxCpu: 1000m
    minMemory: 512Mi
    maxMemory: 2Gi
```

The worker is a fixed single replica because it also runs the database migrations and the first-run bootstrap. Scale background throughput with `worker.concurrency`, not replicas. It has no probes, so it reports `ready` as soon as it is scheduled, even while migrating — read its logs rather than its readiness:

```bash theme={null}
cpln logs '{gvc="GVC_NAME", workload="RELEASE_NAME-chatwoot-worker"}' --limit 50 --since 10m
```

### Prerequisite Secret

```yaml theme={null}
secrets:
  name: my-chatwoot-secrets   # dictionary secret with the four root-of-trust keys — MUST exist before install
```

### Attachment Storage

```yaml theme={null}
storage:
  # local | s3 | s3-compatible   (object storage required for replicas > 1)
  type: local
  local:
    volumeset:
      capacity: 10            # GiB (minimum 10); mounted at /app/storage
  s3:                         # AWS S3 — keyless only (no static keys accepted)
    bucket: my-chatwoot-bucket
    region: us-east-1
    cloudAccountName: my-s3-cloud-account       # Control Plane cloud account
    policyName: my-chatwoot-s3-policy           # your bucket-scoped IAM policy (JSON below)
  s3Compatible:               # SeaweedFS / MinIO / Spaces — static keys
    bucket: my-chatwoot-bucket
    region: us-east-1         # any value; most S3-compatible servers ignore it
    endpoint: http://my-minio-workload:9000     # S3 API address, with scheme and port
    forcePathStyle: true      # true for most S3-compatible servers
    auth:
      secretName: my-chatwoot-s3-keys           # dictionary secret with STORAGE_ACCESS_KEY_ID + STORAGE_SECRET_ACCESS_KEY
```

<Tabs>
  <Tab title="Local volume (default)">
    Attachments are written to a volume set mounted at `/app/storage` on the web workload and survive restarts, redeploys and upgrades under the same release name.

    The volume is attached to the web workload only, so the Sidekiq worker cannot read attachments in this mode — attachment emails and ActiveStorage analyze and purge jobs cannot succeed. Local storage also works only with a single web replica. Use object storage for production.
  </Tab>

  <Tab title="AWS S3 (keyless)">
    AWS S3 uses a Control Plane cloud account: no access keys exist anywhere, and the Chatwoot identity authenticates through the account at runtime. This is the only supported way to reach AWS S3; static keys are accepted for S3-compatible endpoints only.

    <Steps>
      <Step title="Create a bucket">
        Create an S3 bucket. Set `storage.s3.bucket` and `storage.s3.region` to match.
      </Step>

      <Step title="Set up a cloud account">
        If you do not have one, [create a cloud account](/guides/create-cloud-account) for your AWS account. Set `storage.s3.cloudAccountName` to its name.
      </Step>

      <Step title="Create a bucket-scoped IAM policy">
        Create an AWS IAM policy with the JSON below (replace `YOUR_BUCKET_NAME`), then set `storage.s3.policyName` to the policy's name:

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

      <Step title="Switch the storage type">
        Set `storage.type: s3`. The template attaches the cloud account and your policy to the Chatwoot identity.
      </Step>
    </Steps>
  </Tab>

  <Tab title="S3-compatible (MinIO and others)">
    S3-compatible servers use static access keys held in a dictionary secret you create before installing.

    <Steps>
      <Step title="Create a bucket">
        Create the bucket on your server. For the [MinIO](/template-catalog/templates/minio) template deployed in the same GVC, the S3 API address is `http://RELEASE_NAME-minio.GVC_NAME.cpln.local:9000`.
      </Step>

      <Step title="Set the endpoint">
        Set `storage.type: s3-compatible`, `storage.s3Compatible.endpoint` to the S3 API address including scheme and port, and keep `storage.s3Compatible.forcePathStyle: true`.
      </Step>

      <Step title="Create the key secret">
        Create a dictionary secret with the server's credentials and set `storage.s3Compatible.auth.secretName` to its name. The Chatwoot identity is granted `reveal` on exactly this secret.

        ```bash theme={null}
        cpln secret create-dictionary --name STORAGE_KEYS_SECRET_NAME \
          --entry STORAGE_ACCESS_KEY_ID=YOUR_ACCESS_KEY \
          --entry STORAGE_SECRET_ACCESS_KEY=YOUR_SECRET_KEY
        ```
      </Step>
    </Steps>

    <Warning>
      Chatwoot serves an attachment by redirecting the browser to the storage endpoint, so `storage.s3Compatible.endpoint` must be an address your agents' and visitors' browsers can reach. An address that resolves only inside the GVC makes attachments appear broken from outside it, even though the upload itself succeeded.
    </Warning>
  </Tab>
</Tabs>

### Email

```yaml theme={null}
smtp:
  enabled: false
  address: smtp.example.com   # SMTP_ADDRESS (note: not SMTP_HOST)
  port: 587
  domain: ""                  # HELO domain; empty = omitted
  authentication: login       # plain | login | cram_md5; empty = unauthenticated relay
  enableStarttlsAuto: true    # false for a plaintext mail catcher (e.g. Mailpit :1025)
  fromEmail: Chatwoot <no-reply@example.com>    # MAILER_SENDER_EMAIL
  auth:
    secretName: ""            # optional dictionary secret with SMTP_USERNAME + SMTP_PASSWORD; empty = no auth
```

SMTP is off by default and a default install works without it — the onboarding wizard creates the first account with no email confirmation. For an authenticated relay, create the credentials secret before installing and set `smtp.auth.secretName` to its name; the Chatwoot identity is granted `reveal` on exactly that secret:

```bash theme={null}
cpln secret create-dictionary --name SMTP_SECRET_NAME \
  --entry SMTP_USERNAME=YOUR_SMTP_USERNAME \
  --entry SMTP_PASSWORD=YOUR_SMTP_PASSWORD
```

<Warning>
  With SMTP disabled, Chatwoot falls back to `sendmail`, which is absent from the image, so nothing is delivered: agent invitations, password resets and email-channel replies all fail. Configure `smtp.*` before inviting agents.
</Warning>

### Access

```yaml theme={null}
publicAccess:
  enabled: true               # HTTPS UI, API, widget and WebSockets on the auto *.cpln.app endpoint
internalAccess:
  type: same-gvc              # none | same-gvc | same-org | workload-list
  workloads: []               # used only with workload-list
  # workloads:
  #   - //gvc/GVC_NAME/workload/WORKLOAD_NAME
```

Public access is on by default because the chat widget and your visitors live outside the GVC. Set `publicAccess.enabled: false` for an internal-only instance; in-GVC callers still reach it according to `internalAccess.type`:

| 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 org. |
| `workload-list` | Allow access only from the workloads listed in `internalAccess.workloads`. |

The worker and the bundled Redis are never publicly reachable: the worker accepts no inbound traffic, and Redis accepts only same-GVC traffic.

### Database

Enable exactly one of `postgresHA` (default) and `postgres` — the chart refuses to render with both or neither. Chatwoot is wired to the active database automatically: the HAProxy leader endpoint in HA mode, or the single instance directly. In **both** modes the database credentials come from `postgres.credentials.*`: this template writes them into a dictionary secret named by `postgresHA.config.credentialsSecretName` or `postgres.config.credentialsSecretName` and hands that name to the bundled database. There is nothing for you to create.

```yaml theme={null}
# Highly available PostgreSQL (default): 3 Patroni replicas (PostgreSQL 17.5), 3 etcd replicas,
# an HAProxy leader endpoint. Its image ships pgvector, which Chatwoot's schema REQUIRES.
postgresHA:
  enabled: true
  config:
    credentialsSecretName: my-chatwoot-db-credentials   # secret this chart CREATES from postgres.credentials.*; org-wide name — give each release its own
  replicas: 3
  resources:                  # per Patroni replica; the default install runs 3 of these
    minCpu: 250m
    maxCpu: 1000m
    minMemory: 512Mi
    maxMemory: 2Gi
  volumeset:
    capacity: 10              # GiB per replica (minimum 10)
  backup:                     # optional database backups — see Backing Up
    enabled: false
    mode: logical             # logical | wal-g
    logical:
      image: ghcr.io/controlplane-com/backup-images/postgres-backup:17.1.0   # 17.1.0 matches PostgreSQL 17
      schedule: "0 2 * * *"   # daily at 2am UTC
    walg:
      intervalSeconds: 21600
    provider: aws             # aws | gcp | minio
    aws:
      bucket: my-chatwoot-pg-backup-bucket
      region: us-east-1
      cloudAccountName: my-s3-cloud-account
      policyName: my-chatwoot-pg-backup-policy
      prefix: postgres/backups
    gcp:
      bucket: my-chatwoot-pg-backup-bucket
      cloudAccountName: my-gcs-cloud-account
      prefix: postgres/backups
    minio:
      endpoint: http://my-minio-workload:9000
      bucket: my-chatwoot-pg-backup-bucket
      credentialsSecretName: my-chatwoot-minio-credentials   # REQUIRED prerequisite dictionary secret with accessKey + secretKey when provider is minio
      prefix: postgres/backups

# Single-instance PostgreSQL (dev/lightweight): set postgres.enabled: true AND postgresHA.enabled: false.
postgres:
  enabled: false
  image: pgvector/pgvector:pg18   # MUST carry pgvector — the stock postgres:18 image does NOT (render is refused)
  credentials:                # bundled database credentials, used in BOTH modes
    username: chatwoot
    password: change-me-chatwoot-pg   # change before install
    database: chatwoot
  config:
    credentialsSecretName: my-chatwoot-db-credentials   # secret this chart CREATES; org-wide name — give each release its own
  resources:
    minCpu: 250m
    maxCpu: 1000m
    minMemory: 512Mi
    maxMemory: 1Gi
  volumeset:
    capacity: 10              # GiB (minimum 10)
  backup:                     # optional database backups — see Backing Up
    enabled: false
    image: ghcr.io/controlplane-com/backup-images/postgres-backup:18.1.0   # 18.1.0 matches PostgreSQL 18
    schedule: "0 2 * * *"     # daily at 2am UTC
    provider: aws             # aws | gcp | minio
    aws:
      bucket: my-chatwoot-pg-backup-bucket
      region: us-east-1
      cloudAccountName: my-s3-cloud-account
      policyName: my-chatwoot-pg-backup-policy
      prefix: postgres/backups
    gcp:
      bucket: my-chatwoot-pg-backup-bucket
      cloudAccountName: my-gcs-cloud-account
      prefix: postgres/backups
    minio:
      endpoint: http://my-minio-workload:9000
      bucket: my-chatwoot-pg-backup-bucket
      credentialsSecretName: my-chatwoot-minio-credentials   # REQUIRED prerequisite dictionary secret with accessKey + secretKey when provider is minio
      prefix: postgres/backups
```

The other keys of the bundled [postgres-highly-available](/template-catalog/templates/postgres-highly-available) and [postgres](/template-catalog/templates/postgres) templates are available under `postgresHA.*` and `postgres.*`, with one exception: `postgresHA.proxy.enabled` must stay `true`, because the HAProxy endpoint is the address Chatwoot connects to.

<Warning>
  Choose the database mode **before** installing — it cannot be switched later. Flipping `postgresHA.enabled` / `postgres.enabled` on a live release points Chatwoot at a different, empty database (a separate volume set and a different PostgreSQL major version), so the app re-runs onboarding and your existing conversations are orphaned rather than migrated.
</Warning>

<Warning>
  Secret names are org-wide, and the credentials secret is created by this template in both modes. If you run more than one release of this template in the same org, give each its own `postgresHA.config.credentialsSecretName` (or `postgres.config.credentialsSecretName`). A second release left on the default name is **refused at install** and creates nothing; the first release is unaffected.
</Warning>

### Redis

```yaml theme={null}
redis:
  image: redis:8
  auth:
    password: change-me-chatwoot-redis   # template-managed; wired into REDIS_URL — letters/digits/-/_ only
  resources:
    minCpu: 100m
    maxCpu: 400m
    minMemory: 256Mi
    maxMemory: 512Mi
  volumeset:
    capacity: 10              # GiB (minimum 10); AOF at /data
```

Redis is required and runs as a **single node by design**: it carries the pub/sub channel behind every live update, and Redis does not propagate published messages between replicas, so a multi-node topology would silently drop a share of the updates that make the dashboard and widget refresh in real time. Persistence is on and eviction is disabled, so a queued job is never dropped. Change `redis.auth.password` before installing; only letters, digits, `-` and `_` are accepted because the value is embedded in a connection URL.

## Connecting

| What | Address | Notes |
| - | - | - |
| Dashboard, API and chat widget | `https://<canonical>.cpln.app` | `status.canonicalEndpoint` of `RELEASE_NAME-chatwoot` — see the command below |
| WebSocket endpoint | `wss://<canonical>.cpln.app/cable` | Used by the dashboard and the widget; no separate configuration |
| Health checks | `/api` (readiness — reports PostgreSQL and Redis status) and `/health` (liveness) | Both unauthenticated |
| Internal (same GVC) | `http://RELEASE_NAME-chatwoot.GVC_NAME.cpln.local:3000` | Subject to `internalAccess.type` |
| Login | The account created by the onboarding wizard | See [First Run](#first-run) |
| PostgreSQL (HA mode) | `RELEASE_NAME-postgres-ha-proxy.GVC_NAME.cpln.local:5432` | Credentials in the secret named by `postgresHA.config.credentialsSecretName` |
| PostgreSQL (single-instance mode) | `RELEASE_NAME-postgres.GVC_NAME.cpln.local:5432` | Credentials in the secret named by `postgres.config.credentialsSecretName` |
| Redis | `RELEASE_NAME-chatwoot-redis.GVC_NAME.cpln.local:6379` | Password in the `redisPassword` key of `RELEASE_NAME-chatwoot-creds` |

Read the public hostname and the stored credentials:

```bash theme={null}
cpln workload get RELEASE_NAME-chatwoot --gvc GVC_NAME -o yaml      # status.canonicalEndpoint
cpln secret reveal DB_CREDENTIALS_SECRET_NAME -o yaml               # the name you set in *.config.credentialsSecretName
cpln secret reveal RELEASE_NAME-chatwoot-creds -o yaml              # redisPassword
```

To verify the web tier from your machine without going through the public endpoint, forward port `3000` and query the readiness route, which reports the PostgreSQL and Redis status:

```bash theme={null}
cpln port-forward RELEASE_NAME-chatwoot 3000:3000 --gvc GVC_NAME
curl -s http://localhost:3000/api
```

### First Run

Chatwoot ships no default account. The first browser session to reach the endpoint runs the onboarding wizard, and the user it creates is the super admin.

<Steps>
  <Step title="Wait for the web workload to report ready">
    On the default HA database path a first install takes several minutes: the database cluster elects a leader, the worker loads the schema and runs the migrations, and only then does the web tier pass its readiness probe. The worker may restart a few times while the database comes up — that is expected on a first HA install.

    ```bash theme={null}
    cpln workload get-deployments RELEASE_NAME-chatwoot --gvc GVC_NAME -o yaml
    ```
  </Step>

  <Step title="Complete the onboarding wizard">
    Browse to the canonical endpoint of `RELEASE_NAME-chatwoot`. It redirects to `/installation/onboarding`, where the form creates your account and the super admin user. Do this as soon as the workload is ready — the wizard is unauthenticated until it has been completed. For an install with `publicAccess.enabled: false`, reach the same page through `cpln port-forward RELEASE_NAME-chatwoot 3000:3000 --gvc GVC_NAME` at `http://localhost:3000`.
  </Step>

  <Step title="Create an inbox">
    In the dashboard, add an inbox — a website widget for live chat, or an API channel for a custom integration. The widget snippet and the API channel identifier are issued there.
  </Step>

  <Step title="Configure email before inviting agents">
    Agent invitations, password resets and email-channel replies are all delivered by mail. Set up [SMTP](#email) before you invite your team.
  </Step>
</Steps>

Instance-wide settings live in Chatwoot's database, not in Helm values. Sign in as the super admin at `https://<canonical>.cpln.app/super_admin`; under **Settings**, `ENABLE_ACCOUNT_SIGNUP` controls whether visitors can create their own accounts. Self-serve signup is off on a fresh install — accounts come from the onboarding wizard and from agent invitations until you turn it on.

## Operations

### Backing Up

Database backups are optional and off by default. They cover the PostgreSQL database — conversations, contacts, inboxes and users. Attachments are not included: in object-storage mode they live in your bucket, in local mode on the `RELEASE_NAME-chatwoot-storage` volume set.

Enable backups with `postgresHA.backup.enabled: true` or `postgres.backup.enabled: true` (matching your database mode) and complete the storage setup for your provider **before** installing. The keys below are shown as `backup.*`; set them inside the enabled database block.

<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](/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 AWS IAM policy with the JSON below (replace `YOUR_BUCKET_NAME`), then set `backup.aws.policyName` to the policy's name:

        ```json theme={null}
        {
          "Version": "2012-10-17",
          "Statement": [{
            "Effect": "Allow",
            "Action": [
              "s3:GetObject",
              "s3:PutObject",
              "s3:DeleteObject",
              "s3:ListBucket",
              "s3:GetObjectVersion",
              "s3:DeleteObjectVersion"
            ],
            "Resource": [
              "arn:aws:s3:::YOUR_BUCKET_NAME",
              "arn:aws:s3:::YOUR_BUCKET_NAME/*"
            ]
          }]
        }
        ```
      </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](/guides/create-cloud-account) for your GCP project. Set `backup.gcp.cloudAccountName` to its name — access is keyless.
      </Step>

      <Step title="Grant bucket access">
        Grant the `Storage Object Admin` role (`roles/storage.objectAdmin`) on that bucket to the GCP service account created for the cloud account.
      </Step>
    </Steps>
  </Tab>

  <Tab title="S3-compatible (MinIO and others)">
    <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">
        Set `backup.minio.endpoint` to the S3 API address including scheme and port. For the [MinIO](/template-catalog/templates/minio) template deployed in the same GVC, this is `http://RELEASE_NAME-minio.GVC_NAME.cpln.local:9000`.
      </Step>

      <Step title="Create the credentials secret">
        In both database modes the server's keys are a prerequisite [dictionary secret](/guides/create-secret/dictionary) holding exactly `accessKey` and `secretKey`. Create it before installing and set `backup.minio.credentialsSecretName` to its name:

        ```bash theme={null}
        cpln secret create-dictionary --name MINIO_BACKUP_SECRET_NAME \
          --entry accessKey=YOUR_ACCESS_KEY \
          --entry secretKey=YOUR_SECRET_KEY
        ```
      </Step>
    </Steps>
  </Tab>
</Tabs>

In HA mode, `backup.mode` selects `logical` (a scheduled dump run by the `RELEASE_NAME-postgres-ha-backup` cron workload) or `wal-g` (continuous WAL archiving from the database workload). Single-instance mode takes scheduled logical dumps through `RELEASE_NAME-postgres-backup`. `backup.*.prefix` is the key prefix within the bucket, and the schedule is a standard cron expression in UTC.

### Restoring a Backup

The database restore is the one documented for the bundled database: [Restoring a Backup](/template-catalog/templates/postgres-highly-available#restoring-a-backup) on the postgres-highly-available page (logical and WAL-G), or [Restoring a Backup](/template-catalog/templates/postgres#restoring-a-backup) on the postgres page. In HA mode, connect through `RELEASE_NAME-postgres-ha-proxy.GVC_NAME.cpln.local` so the restore targets the current leader; use the username and password from the secret named by `*.config.credentialsSecretName`.

Two Chatwoot-specific points:

* The web and worker workloads must not be writing to the database while a logical dump is replayed. After the restore, force a redeployment of both so they reconnect and the cache is rebuilt:

  ```bash theme={null}
  cpln workload force-redeployment RELEASE_NAME-chatwoot --gvc GVC_NAME
  cpln workload force-redeployment RELEASE_NAME-chatwoot-worker --gvc GVC_NAME
  ```

* Attachments are restored separately: copy them back into the bucket (object-storage mode) or onto the `RELEASE_NAME-chatwoot-storage` volume set (local mode).

<Warning>
  This restore path has **not** been exercised against a Chatwoot install. The database procedure is the bundled template's own; follow it with a test release first, and keep the four keys from your prerequisite secret with the backup — a dump restored under different `ACTIVE_RECORD_ENCRYPTION_*` keys cannot decrypt the two-factor secrets it contains.
</Warning>

### Upgrading From 1.0.0

Version `1.0.1` only updates the bundled highly available database to a release that compacts its etcd cluster. Without compaction, etcd's backend grows with time alone and eventually goes read-only, taking PostgreSQL failover with it. Upgrade; see [etcd History Compaction](/template-catalog/templates/postgres-highly-available#etcd-history-compaction) for the mechanism and the symptoms. Only installs running the HA database (`postgresHA.enabled`, the default) are affected. Compaction stops further growth but cannot shrink a backend that has already grown.

### Upgrading From 1.0.1 or Earlier

Version `1.1.0` changed the single-instance database path:

* `postgres.config.username`, `postgres.config.password` and `postgres.config.database` moved to `postgres.credentials.username`, `postgres.credentials.password` and `postgres.credentials.database`. The new `postgres.config.credentialsSecretName` names the dictionary secret this template now creates from those three values. Carrying the old keys fails the render with `config.username was REMOVED in postgres 3.4.0` — move the three keys and you are done, and ignore that message's advice to create a secret yourself.
* `postgres.backup.minio.accessKey` and `postgres.backup.minio.secretKey` were replaced by `postgres.backup.minio.credentialsSecretName`, a prerequisite dictionary secret holding `accessKey` and `secretKey` — see [Backing Up](#backing-up).

Keep `postgres.credentials.*` equal to the values your database was initialised with; the running database still enforces them.

### Upgrading From 1.1.0 or Earlier

Version `1.2.0` moved the highly available database to `postgres-highly-available` 2.5.0, which no longer creates its own credentials secret:

* `postgresHA.postgres.username`, `postgresHA.postgres.password` and `postgresHA.postgres.database` were removed. Carrying them fails the render with `the postgres block was REMOVED in 2.5.0`. Delete the block.
* In HA mode the credentials now come from `postgres.credentials.*` — the same keys the single-instance path uses — and this template writes them into the secret named by the new `postgresHA.config.credentialsSecretName`.

<Warning>
  **Use the credentials your cluster already has.** Set `postgres.credentials.username`, `postgres.credentials.password` and `postgres.credentials.database` to exactly the values your `postgresHA.postgres.*` block held. The database was initialised with them and still enforces them; putting new values in the secret does not change the running database — it gives Chatwoot a password that no longer works.
</Warning>

### Upgrading From 1.2.0 or Earlier

Version `1.2.1` replaced `postgresHA.backup.minio.accessKey` and `postgresHA.backup.minio.secretKey` with `postgresHA.backup.minio.credentialsSecretName`, a prerequisite dictionary secret holding `accessKey` and `secretKey`. Only installs backing up the HA database to an S3-compatible server are affected — see [Backing Up](#backing-up) for the secret.

### Scaling and Availability

* **Web tier** — `chatwoot.replicas` sets how many web replicas run. Replicas share the database and the bundled Redis, so a message received by one replica reaches a WebSocket client connected to another, and no sticky sessions are needed. Anything above `1` requires `storage.type` `s3` or `s3-compatible`; the chart refuses to render the combination with local storage.

  ```yaml theme={null}
  chatwoot:
    replicas: 2
  storage:
    type: s3
    s3:
      bucket: my-chatwoot-bucket
      region: us-east-1
      cloudAccountName: my-s3-cloud-account
      policyName: my-chatwoot-s3-policy
  ```

* **Worker and Redis** — both are fixed at one replica. Scale background throughput with `worker.concurrency`.

* **Database** — the default `postgresHA` path keeps serving through a replica failure and a rolling restart; the `postgres` path has no failover and is for development or light use. The mode is an install-time choice.

* **Helm upgrades** — a `helm upgrade` can restart the bundled datastores even when their values did not change, and the web tier is unready while Redis is down; upgrade in a quiet window.

* **After any Redis restart, restart the web workload.** Background jobs and the cache reconnect on their own, but the web tier's live-update subscription does not: nothing is logged, the health endpoint stays green, and agents and visitors stop seeing new messages until they refresh. A forced redeployment of the web workload restores it:

  ```bash theme={null}
  cpln workload force-redeployment RELEASE_NAME-chatwoot --gvc GVC_NAME
  ```

### Troubleshooting

<AccordionGroup>
  <Accordion title="The deployment never starts and cpln logs is empty">
    **Symptom:** `RELEASE_NAME-chatwoot` and `RELEASE_NAME-chatwoot-worker` never reach ready, and `cpln logs` returns zero lines.

    **Cause:** a secret referenced by name does not exist — usually the keys secret (`secrets.name`), or the storage or SMTP secret when those features are enabled. The container never starts, so there is nothing to log.

    **Fix:** read `status.versions[].message`, which names the missing secret, then create it. The deployment recovers on its own after several minutes, or force a redeployment to skip the wait.

    ```bash theme={null}
    cpln workload get-deployments RELEASE_NAME-chatwoot --gvc GVC_NAME -o yaml
    cpln workload force-redeployment RELEASE_NAME-chatwoot --gvc GVC_NAME
    ```
  </Accordion>

  <Accordion title="The web workload stays unready and logs waiting for the chatwoot schema">
    **Symptom:** the web log repeats `waiting for the chatwoot schema...` and the readiness probe never passes.

    **Cause:** the web tier deliberately stays down until the worker has created the schema, so this means the worker's migration has not completed. On a first HA install the worker restarts a few times with `PG::ConnectionBad` while the database cluster elects a leader, which is expected. A migration that keeps failing is not.

    **Fix:** read the worker's log — the worker reports `ready` even while crash-looping, so its readiness is not a signal.

    ```bash theme={null}
    cpln logs '{gvc="GVC_NAME", workload="RELEASE_NAME-chatwoot-worker"} |= "ERROR"' --limit 50 --since 10m
    ```
  </Accordion>

  <Accordion title="Live updates stopped after a Redis restart">
    **Symptom:** new messages appear only after a page refresh; `/api` still reports healthy and nothing is logged.

    **Cause:** the bundled Redis restarted (a redeploy, a reschedule or a Helm upgrade) and the web tier's live-update subscription did not re-establish.

    **Fix:** force a redeployment of the web workload.

    ```bash theme={null}
    cpln workload force-redeployment RELEASE_NAME-chatwoot --gvc GVC_NAME
    ```
  </Accordion>

  <Accordion title="Render fails with config.username was REMOVED in postgres 3.4.0">
    **Symptom:** `helm upgrade` fails before anything is applied, citing `config.username was REMOVED in postgres 3.4.0`.

    **Cause:** your values still carry the `1.0.x` single-instance keys `postgres.config.username`, `postgres.config.password` or `postgres.config.database`.

    **Fix:** move the three keys to `postgres.credentials.*` as described in [Upgrading From 1.0.1 or Earlier](#upgrading-from-1-0-1-or-earlier). Do not create a secret yourself — this template creates it.
  </Accordion>

  <Accordion title="Render fails with the postgres block was REMOVED in 2.5.0">
    **Symptom:** `helm upgrade` fails before anything is applied, citing `the postgres block was REMOVED in 2.5.0`.

    **Cause:** your values still carry `postgresHA.postgres.username`, `postgresHA.postgres.password` or `postgresHA.postgres.database` from version `1.1.0` or earlier.

    **Fix:** delete the `postgresHA.postgres` block and put the same three values under `postgres.credentials.*` — see [Upgrading From 1.1.0 or Earlier](#upgrading-from-1-1-0-or-earlier).
  </Accordion>

  <Accordion title="A second release is refused at install as managed by a different release">
    **Symptom:** installing a second Chatwoot release in the same org fails with `cannot be updated because it is being managed by a different release`, and nothing is created.

    **Cause:** both releases use the same `credentialsSecretName`. Secret names are org-wide, and the first release owns that secret.

    **Fix:** give the second release its own `postgresHA.config.credentialsSecretName` (or `postgres.config.credentialsSecretName`). The first release is unaffected.
  </Accordion>

  <Accordion title="Attachments upload but appear broken in the browser">
    **Symptom:** with `storage.type: s3-compatible`, uploads succeed but images and files fail to load for agents or visitors.

    **Cause:** Chatwoot serves an attachment by redirecting the browser to `storage.s3Compatible.endpoint`, and that address resolves only inside the GVC.

    **Fix:** point `storage.s3Compatible.endpoint` at an address your users' browsers can reach, and run `helm upgrade`.
  </Accordion>

  <Accordion title="Agent invitations and password-reset emails never arrive">
    **Symptom:** invitations, password resets and email-channel replies are not delivered.

    **Cause:** `smtp.enabled` is `false`. Chatwoot then falls back to `sendmail`, which the image does not contain.

    **Fix:** configure `smtp.*` as described in [Email](#email) and run `helm upgrade`.
  </Accordion>

  <Accordion title="Render fails because replicas above 1 require object storage">
    **Symptom:** `helm install` or `helm upgrade` fails with `chatwoot.replicas > 1 requires storage.type 's3' or 's3-compatible'`.

    **Cause:** local attachments live on a volume set attached to one web replica, so a file uploaded through one replica would be missing from another.

    **Fix:** switch to object storage as described in [Attachment Storage](#attachment-storage) before raising `chatwoot.replicas`.
  </Accordion>
</AccordionGroup>

## Important Notes

* **Create the keys secret before installing** — `secrets.name` must name an existing dictionary secret holding `SECRET_KEY_BASE` and the three `ACTIVE_RECORD_ENCRYPTION_*` keys; a missing secret wedges the deployment silently.
* **All four keys are write-once** — rotating `SECRET_KEY_BASE` logs out every user, and rotating an `ACTIVE_RECORD_ENCRYPTION_*` key makes stored two-factor secrets undecryptable. Back the values up outside Control Plane.
* **Set `postgres.credentials.password` and `redis.auth.password` at install time** — both ship as placeholders, seed their component on first boot, and are not updated by later value edits.
* **Complete the onboarding wizard right after install** — the first browser session to reach the endpoint creates the super admin, with no email confirmation.
* **Restart the web workload after any Redis restart** — live updates stop silently until it restarts.
* **Choose the database mode before installing** — switching `postgresHA` and `postgres` on a live release points Chatwoot at a different, empty database.
* **In single-instance mode the database image must carry pgvector** — keep `postgres.image` on a pgvector build; the chart refuses a stock `postgres:` image.
* **`chatwoot.replicas` above `1` requires object storage** — local attachments are per-replica.
* **In `local` storage mode the worker cannot read attachments** — attachment emails and ActiveStorage analyze and purge jobs fail. Use object storage for production.
* **AWS S3 is keyless only** — a cloud account plus a bucket-scoped IAM policy; static keys are accepted only for S3-compatible endpoints.
* **With SMTP off, no mail is delivered** — configure `smtp.*` before inviting agents.
* **Do not scale the worker or Redis** — both are fixed singletons; scale background throughput with `worker.concurrency`.
* **Give each release its own `credentialsSecretName`** — secret names are org-wide and a second release on the default name is refused at install.
* **Self-serve signup is managed in the app** — turn it on at `/super_admin` under **Settings**; it is off on a fresh install.
* **Enterprise features are not included** — the `-ce` image omits SSO/SAML, audit logs, agent capacity management, custom branding, SLA policies and Captain AI.
* **Data survives restarts and upgrades** — conversations live in the database volume sets and local attachments in `RELEASE_NAME-chatwoot-storage`. Uninstalling deletes those volume sets; your keys secret survives an uninstall.

## External References

<CardGroup cols={2}>
  <Card title="Chatwoot Self-Hosted Docs" icon="book" href="https://developers.chatwoot.com/self-hosted">
    Official self-hosting documentation
  </Card>

  <Card title="Environment Variables" icon="gear" href="https://developers.chatwoot.com/self-hosted/configuration/environment-variables">
    Every setting the Chatwoot application reads from its environment
  </Card>

  <Card title="Super Admin Console" icon="user-shield" href="https://developers.chatwoot.com/self-hosted/monitoring/super-admin-sidekiq">
    Instance settings and the Sidekiq dashboard at /super\_admin
  </Card>

  <Card title="Community vs Enterprise" icon="scale-balanced" href="https://developers.chatwoot.com/self-hosted/enterprise-edition">
    What the Community Edition image includes and excludes
  </Card>

  <Card title="Chatwoot on GitHub" icon="github" href="https://github.com/chatwoot/chatwoot">
    Source code and release notes
  </Card>

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