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

# MongoDB Cluster

> Deploy a highly available MongoDB replica set (Percona Server for MongoDB 8.0) on Control Plane, in one location or stretched across several, with HAProxy routing to the primary and optional mongodump backups. Two prerequisite secrets and a GVC that already has your locations must exist first.

## Overview

MongoDB is a document-oriented NoSQL database. This template deploys a MongoDB replica set built on Percona Server for MongoDB 8.0 (`percona/percona-server-mongodb:8.0`): a configurable number of members per location, keyfile authentication between members, automatic replica set formation at boot, and automatic primary election when a member is lost. An HAProxy workload (on by default) gives clients one stable address that always routes to the current primary, and an optional cron job writes `mongodump` archives to AWS S3 or GCS.

Neither the database credentials nor the replica set keyfile are template values. Both come from secrets you create before installing, so no credential passes through Helm or lands in the release.

<Note>
  This template does not create a GVC. It deploys into an existing [GVC](/reference/gvc) that you already have, and every location you list in `locations` must already be on that GVC. Locations on the GVC that you do **not** list start nothing: no members, no proxy replicas and no volumes are created there.
</Note>

### What Gets Created

| Resource | Name | Purpose | Optional |
| - | - | - | - |
| Workload | `RELEASE_NAME-mongo` | Replica set members (stateful, `replicas` per listed location, port `27017`) | — |
| Workload | `RELEASE_NAME-mongo-proxy` | HAProxy routing clients to the current primary (standard, 2 replicas per location, port `27017`) | `proxy.enabled: true` (default) |
| Workload | `RELEASE_NAME-mongo-backup` | Cron job running `mongodump` in one location | `backup.enabled: true` |
| Volume set | `RELEASE_NAME-mongo-vs` | One volume per member mounted at `/data/db`; a final snapshot is kept 7 days on deletion | — |
| Identity | `RELEASE_NAME-mongo-identity` | Identity the workloads run as; carries the Cloud Account binding when backups are on | — |
| Policy | `RELEASE_NAME-mongo-policy` | Grants the identity `reveal` on your two prerequisite secrets and the startup-script secrets | — |
| Policy | `RELEASE_NAME-mongo-gvc-policy` | Grants the identity `view` on the install GVC so members can check its locations at boot | — |
| Secret | `RELEASE_NAME-mongo-startup` | MongoDB startup script; holds no credentials | — |
| Secret | `RELEASE_NAME-mongo-proxy-startup` | HAProxy startup script | `proxy.enabled: true` (default) |

The template creates no credential secret of its own. The password and the keyfile live only in the secrets you create.

## Prerequisites

**Two secrets and a GVC with your locations must exist before you install.** Secrets are org-level, so no GVC flag is involved.

<Steps>
  <Step title="Create the database credentials secret">
    A [dictionary secret](/guides/create-secret/dictionary) holding exactly three keys — `username`, `password` and `database`. The cluster is bootstrapped with this user (created in the `admin` database with the `root` role), and it is the credential your applications put in their connection strings. The `database` entry is for your own connection strings; MongoDB creates the database on first write.

    ```bash theme={null}
    cpln secret create-dictionary --name CREDENTIALS_SECRET_NAME \
      --entry username=admin \
      --entry password='YOUR-STRONG-PASSWORD' \
      --entry database=mydatabase
    ```

    Set `mongodb.credentialsSecretName` to this name.
  </Step>

  <Step title="Create the replica set keyfile secret">
    An [opaque secret](/guides/create-secret/opaque) with encoding `plain` holding the key that members authenticate to each other with. Generate it — never invent a passphrase:

    ```bash theme={null}
    openssl rand -base64 756 | cpln secret create-opaque --name KEYFILE_SECRET_NAME --encoding plain -f -
    ```

    Set `mongodb.keyfileSecretName` to this name. The two secrets stay separate so that an application granted `reveal` on the credentials cannot also read the key that lets a member join your replica set.
  </Step>

  <Step title="Check that the GVC has every location you will list">
    The GVC may have more locations than you list — nothing runs in those — but every entry in `locations` must be present. The locations are under `spec.staticPlacement.locationLinks`:

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

  <Step title="Read either secret back later">
    A bare `cpln secret reveal` prints only a summary table; pass `-o yaml` for the values:

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

<Warning>
  **The keyfile must be valid base64, or the cluster will not start.** MongoDB accepts 6–1024 characters from the base64 alphabet only (`A-Z`, `a-z`, `0-9`, `+`, `/`, `=`). A hand-written passphrase with hyphens or punctuation will not boot. The container checks the key before starting MongoDB and fails with a message naming your secret, visible in `cpln logs`:

  ```text theme={null}
  FATAL: the keyfile in secret 'KEYFILE_SECRET_NAME' contains characters outside the base64 alphabet
  (A-Z a-z 0-9 + / =). A passphrase with hyphens or punctuation will NOT work. Regenerate it with:
  openssl rand -base64 756
  ```

  The keyfile cannot be changed after the cluster is initialized.
</Warning>

<Warning>
  **Create both secrets before installing, or the deployment wedges silently.** The template refuses to render when either name is blank, but a name that points at a secret which does not exist installs successfully and then never starts. No container runs, so `cpln logs` returns **zero lines**. The one place the reason appears is `status.versions[].message`:

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

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

  Use `get-deployments` — plain `cpln workload get` has no `versions` key. Once the secret exists the deployment recovers on its own after several minutes, or run `cpln workload force-redeployment RELEASE_NAME-mongo --gvc GVC_NAME` to skip the wait.
</Warning>

Backups need a bucket and a Control Plane Cloud Account before they can be enabled — see [Backing Up](#backing-up). Nothing else is required.

## Installation

Create the [prerequisite secrets](#prerequisites), then install with their names and the location your GVC has. The default `locations` entry is `aws-us-east-1` with three members; a `--set` on a list entry replaces the whole entry, so pass both fields. For more than one location, use a values file instead.

```bash theme={null}
cpln helm install RELEASE_NAME oci://ghcr.io/controlplane-com/templates/mongodb-cluster \
  --version 2.0.0 --gvc GVC_NAME \
  --set mongodb.credentialsSecretName=CREDENTIALS_SECRET_NAME \
  --set mongodb.keyfileSecretName=KEYFILE_SECRET_NAME \
  --set 'locations[0].name=LOCATION_NAME' --set 'locations[0].replicas=3'
```

<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 block below is the shipped default for that part of `values.yaml`.

### Locations

```yaml theme={null}
# Every location listed MUST already exist in the GVC you install into.
# One entry per location; `replicas` = mongod members in that location.
locations:
  - name: aws-us-east-1
    replicas: 3

# Spread each location's members across availability zones. Confirm your
# locations support multi-zone before enabling.
multiZone: false
```

The default is one location with three members — the smallest shape that installs on any single-location GVC and survives losing a member. Set it to your GVC's locations before installing. Listing the same location twice, or giving one `replicas: 0`, is refused at render; to stop running somewhere, remove the entry.

Every member this template creates is a **voting** member, so the total across all locations decides what the replica set survives:

| Total members | Majority | Survives | Notes |
| - | - | - | - |
| 1 | 1 | nothing | Valid and writable — a single member is always its own primary |
| 2 | 2 | nothing | **Refused by this template.** Losing either member leaves no primary, so it is strictly worse than 1 |
| 3 | 2 | 1 member | The usual choice |
| 5 | 3 | 2 members | |
| 7 | 4 | 3 members | **The maximum.** MongoDB allows at most 7 voting members |

A total above 7 is refused at render: MongoDB rejects the 8th voting member, and every member past it would run a `mongod` that reports ready while never being in the replica set.

<Warning>
  **At most 6 members in one location on a default org quota.** A 7-member roster in a single location passes render and is then rejected at install with `One or more containers exceed the autoscaling.maxScale of 6 (quota: replicas-per-replica-direct-workload)`. Reach 7 by splitting across locations (4 + 3 is proven), or request a quota increase.
</Warning>

Spreading members across locations buys survival of a whole location, at the cost of cross-region replication traffic, which is billed. Size it against the same table: 3 locations × 1 member survives losing one location; 2 locations × 2 members survives losing neither, because 2 of 4 is not a majority.

<Warning>
  **Never reorder `locations` after installing — only append.** The first entry is the bootstrap location: `replica-0` there is the member that initializes the replica set. Moving a different location into first position points that logic at a member with an empty data directory.
</Warning>

### Image and Resources

```yaml theme={null}
image: percona/percona-server-mongodb:8.0

resources:
  cpu: 1
  memory: 2Gi
```

### Credentials

```yaml theme={null}
mongodb:
  # `dictionary` secret holding exactly three keys: `username`, `password`,
  # `database`. This is the credential you put in your applications' connection
  # strings, and the root user the cluster is bootstrapped with.
  credentialsSecretName: my-mongodb-credentials

  # `opaque` secret (encoding: plain) holding the replica set keyfile — the shared
  # key cluster members authenticate to each other with. Anyone holding it can join
  # a member to the replica set, so it is kept SEPARATE from the credentials above.
  # CONTENT RULES (mongod rejects anything else): 6-1024 characters from the base64
  # alphabet only — A-Z a-z 0-9 + / = — after whitespace is stripped. Generate with
  # `openssl rand -base64 756`; do NOT invent a passphrase, hyphens alone break it.
  # It CANNOT be changed after the cluster is initialized.
  keyfileSecretName: my-mongodb-keyfile
```

Both secrets must exist before you install — see [Prerequisites](#prerequisites). The workloads read them through `cpln://secret/...` references, so the values appear in neither the Helm release nor the stored workload spec.

### Storage

```yaml theme={null}
volumeset:
  capacity: 10 # initial capacity in GiB per member (minimum is 10)
  autoscaling:
    enabled: false
    maxCapacity: 100
    minFreePercentage: 10
    scalingFactor: 1.2
```

There is no snapshot schedule: a final snapshot is taken when the volume set is deleted and kept for 7 days. Use [backups](#backing-up) for point-in-time copies.

### Access

```yaml theme={null}
# There is no public endpoint: MongoDB is reachable on the internal network only.
firewall:
  internalAllowType: same-gvc # options: same-gvc, same-org, workload-list
  workloads: [] # only used with workload-list; this release's own workloads are added automatically
  # - //gvc/GVC_NAME/workload/WORKLOAD_NAME
```

| Type | Who can connect on port `27017` |
| - | - |
| `same-gvc` | Every workload in the same GVC (default) |
| `same-org` | Every workload in the org |
| `workload-list` | Only the workloads listed under `workloads`, plus this release's own workloads |

The list also governs replication between the members, HAProxy's health checks and the backup job, so the template always adds its own workloads to it — list only your clients. A change to `firewall` can take several minutes to take effect; re-test rather than trusting the first response.

### HAProxy

```yaml theme={null}
# Only the primary accepts writes. The proxy gives clients one stable address and
# routes to whichever member is primary right now.
proxy:
  enabled: true
  image: haproxy:2.9
  resources:
    cpu: 100m
    memory: 128Mi
  minReplicas: 2 # per configured location
  maxReplicas: 2
```

The proxy health-checks every member and forwards to the one that is currently primary, so applications keep one connection string across failovers. Backups do not use it: the backup job connects to `replica-0` in `backup.location` directly, so `proxy.enabled: false` does not affect them.

### Backups

```yaml theme={null}
backup:
  enabled: false
  # `logical` is the only supported mode. Physical (Percona Backup for MongoDB)
  # was removed in 2.0.0: its restore must execute mongod, which the pbm-agent
  # image does not contain, so it wrote backups that could never be restored.
  mode: logical

  schedule: "0 2 * * *" # daily at 2am UTC

  # The ONE location the backup job runs in. Must be one of `locations` above —
  # the platform accepts a location that is not, stores it, and the job then never
  # runs anywhere with nothing to observe. (1.x derived this from backup.aws.region.)
  location: aws-us-east-1

  provider: aws # options: aws or gcp

  # Logical backup (mongodump)
  logical:
    image: ghcr.io/controlplane-com/backup-images/mongo-backup:8.0
    resources:
      cpu: 100m
      memory: 128Mi

  aws:
    bucket: my-backup-bucket
    region: us-east-1
    cloudAccountName: my-backup-cloudaccount
    policyName: my-backup-policy
    prefix: mongodb-cluster/backups

  gcp:
    bucket: my-backup-bucket
    cloudAccountName: my-backup-cloudaccount
    prefix: mongodb-cluster/backups
```

`backup.location` is validated against `locations` at render time, and `backup.mode: physical` fails at render even while `backup.enabled` is `false`. The bucket and Cloud Account must exist first — see [Backing Up](#backing-up) for the per-provider steps and the IAM policy.

## Connecting

| What | Address | Notes |
| - | - | - |
| Through the proxy (reads and writes) | `RELEASE_NAME-mongo-proxy.GVC_NAME.cpln.local:27017` | One stable address that follows the primary. Always use the fully qualified name. |
| A specific member | `replica-N.RELEASE_NAME-mongo.LOCATION_NAME.GVC_NAME.cpln.local:27017` | `N` is the member index within that location, starting at 0. |
| From your own machine | `cpln port-forward RELEASE_NAME-mongo-proxy 27017:27017 --gvc GVC_NAME`, then `127.0.0.1:27017` | There is no public endpoint. |
| Credentials | `cpln secret reveal CREDENTIALS_SECRET_NAME -o yaml` | The `username`, `password` and `database` entries of your prerequisite secret. |

The user lives in the `admin` database, so connection strings must carry `authSource=admin`:

```text theme={null}
mongodb://USERNAME:PASSWORD@RELEASE_NAME-mongo-proxy.GVC_NAME.cpln.local:27017/DATABASE?authSource=admin
```

To offload reads, connect to the members directly and add `readPreference=secondaryPreferred`; secondary reads can lag the primary slightly.

Verify the replica set from inside a member. The credentials are already in its environment, so nothing is typed on the command line:

```bash theme={null}
cpln workload exec RELEASE_NAME-mongo --gvc GVC_NAME --location LOCATION_NAME \
  --replica RELEASE_NAME-mongo-0 --container mongodb -- \
  sh -c 'mongosh --quiet -u "$MONGO_INITDB_ROOT_USERNAME" -p "$MONGO_INITDB_ROOT_PASSWORD" --authenticationDatabase admin --eval "rs.status().members.forEach(m => print(m.name, m.stateStr))"'
```

Every member should be listed, one `PRIMARY` and the rest `SECONDARY`. To use `mongosh` from your own machine, forward the proxy port and connect to `127.0.0.1`; `db.hello().primary` shows which member the proxy is routing to:

```bash theme={null}
cpln port-forward RELEASE_NAME-mongo-proxy 27017:27017 --gvc GVC_NAME
```

```bash theme={null}
mongosh "mongodb://USERNAME:PASSWORD@127.0.0.1:27017/?authSource=admin" --eval "db.hello().primary"
```

## Operations

### Backing Up

When `backup.enabled` is `true`, the `RELEASE_NAME-mongo-backup` cron workload runs `mongodump` on `backup.schedule` in `backup.location` only, authenticating with the username and password from your credentials secret. Each run writes one gzip-compressed archive of **every database in the cluster** to `PREFIX/mongo-YYYY-MM-DDTHH-MM-SSZ.archive.gz` in your bucket. Backups do not delete older archives; manage retention with a bucket lifecycle rule.

<Steps>
  <Step title="AWS S3">
    1. Create an S3 bucket. Set `backup.aws.bucket` to its name and `backup.aws.region` to its region.
    2. If you do not have a Control Plane Cloud Account for AWS, follow the [Create a Cloud Account](/guides/create-cloud-account) guide. Set `backup.aws.cloudAccountName` to its name.
    3. Create an IAM policy with the following JSON, replacing `YOUR_BUCKET_NAME`, and set `backup.aws.policyName` to its name. The template attaches this policy to the identity and nothing else, so the bucket in it is the only storage the job can reach:

    ```json theme={null}
    {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Action": [
                    "s3:GetObject",
                    "s3:PutObject",
                    "s3:DeleteObject",
                    "s3:ListBucket",
                    "s3:GetObjectVersion",
                    "s3:DeleteObjectVersion",
                    "s3:GetBucketLocation",
                    "s3:AbortMultipartUpload",
                    "s3:ListBucketMultipartUploads",
                    "s3:ListMultipartUploadParts"
                ],
                "Resource": [
                    "arn:aws:s3:::YOUR_BUCKET_NAME",
                    "arn:aws:s3:::YOUR_BUCKET_NAME/*"
                ]
            }
        ]
    }
    ```

    4. Set `backup.location` to one of your `locations`, `backup.aws.prefix` to the folder path for the archives, then `backup.enabled: true` and `backup.provider: aws`.
  </Step>

  <Step title="GCS">
    1. Create a GCS bucket. Set `backup.gcp.bucket` to its name.
    2. If you do not have a Control Plane Cloud Account for GCP, follow the [Create a Cloud Account](/guides/create-cloud-account) guide. Set `backup.gcp.cloudAccountName` to its name.
    3. Add the **Storage Admin** role to the GCP service account associated with the Cloud Account. The template additionally binds the identity to `roles/storage.objectAdmin` on exactly the bucket in `backup.gcp.bucket`.
    4. Set `backup.location` to one of your `locations`, `backup.gcp.prefix` to the folder path for the archives, then `backup.enabled: true` and `backup.provider: gcp`.
  </Step>
</Steps>

Check a run with the container logs of the cron workload; a successful run ends with `[INFO] Backup completed: mongo-....archive.gz`:

```bash theme={null}
cpln logs '{gvc="GVC_NAME", workload="RELEASE_NAME-mongo-backup"}' --limit 50 --since 24h
```

If you enable backups while `firewall.internalAllowType: workload-list` is in force, the first scheduled run can fail with `server selection error: context deadline exceeded` because the firewall change has not taken effect yet; the next run succeeds.

### Restoring a Backup

The archive is a `mongodump --archive --gzip` file, so it is restored with `mongorestore --gzip --archive`. The cluster has no public endpoint, so the restore runs through a `cpln port-forward` tunnel from a machine that has the [MongoDB Database Tools](https://www.mongodb.com/docs/database-tools/) and your cloud CLI installed. This procedure was verified end to end during the template's 2.0.0 test cycle: archive downloaded, data dropped, restored, and the restored documents read back from a secondary.

<Steps>
  <Step title="Download the archive">
    List what is in the bucket with `aws s3 ls s3://BUCKET_NAME/PREFIX/` (for GCS, `gcloud storage ls gs://BUCKET_NAME/PREFIX/`), then download the one you want:

    ```bash theme={null}
    aws s3 cp s3://BUCKET_NAME/PREFIX/mongo-TIMESTAMP.archive.gz ./backup.gz
    ```
  </Step>

  <Step title="Open a tunnel to the proxy">
    The proxy routes the tunnel to whichever member is primary right now, which is where a restore must write. Leave this running. If you disabled the proxy, forward to `RELEASE_NAME-mongo` instead.

    ```bash theme={null}
    cpln port-forward RELEASE_NAME-mongo-proxy 27017:27017 --gvc GVC_NAME
    ```
  </Step>

  <Step title="Restore through the tunnel">
    By default `mongorestore` adds the archive's documents to the existing collections and keeps any document whose `_id` already exists. Add `--drop` to replace each collection with the backup's contents, or `--nsInclude` to restore a single database or collection; see the [mongorestore reference](https://www.mongodb.com/docs/database-tools/mongorestore/) for both. In another terminal:

    ```bash theme={null}
    mongorestore --uri="mongodb://USERNAME:PASSWORD@127.0.0.1:27017/?authSource=admin" \
      --gzip --archive=./backup.gz
    ```

    Restored data replicates to the secondaries like any other write.
  </Step>

  <Step title="Verify">
    Through the same tunnel, list the databases and spot-check a collection count:

    ```bash theme={null}
    mongosh "mongodb://USERNAME:PASSWORD@127.0.0.1:27017/?authSource=admin" --eval "db.adminCommand({ listDatabases: 1 })"
    ```
  </Step>
</Steps>

### Rotating the Password

The password in the secret creates the root user on first boot; afterwards the members and the backup job present it when they authenticate. Rotate inside MongoDB first, then bring the secret and the running replicas in line:

<Steps>
  <Step title="Change it inside MongoDB">
    Through a [port-forward tunnel to the proxy](#connecting), which reaches the primary:

    ```bash theme={null}
    mongosh "mongodb://USERNAME:PASSWORD@127.0.0.1:27017/?authSource=admin" \
      --eval 'db.getSiblingDB("admin").changeUserPassword("USERNAME", "NEW-STRONG-PASSWORD")'
    ```
  </Step>

  <Step title="Update the secret">
    Re-apply your [dictionary secret](/guides/create-secret/dictionary) with the new `password` entry, keeping `username` and `database` unchanged:

    ```yaml theme={null}
    kind: secret
    name: CREDENTIALS_SECRET_NAME
    type: dictionary
    data:
      username: admin
      password: NEW-STRONG-PASSWORD
      database: mydatabase
    ```

    ```bash theme={null}
    cpln apply -f secret.yaml
    ```
  </Step>

  <Step title="Redeploy the members">
    A running replica keeps the environment it started with; updating the secret does not restart it, and the old value keeps working with no error until it does. Force a redeployment so every member — and the startup checks that use these credentials — picks up the new password. The template sets no limit on how many members restart at once, so this can restart every member in every location together (each steps a primary down in its pre-stop hook first); plan it as a short write outage with client retries:

    ```bash theme={null}
    cpln workload force-redeployment RELEASE_NAME-mongo --gvc GVC_NAME
    ```
  </Step>
</Steps>

### Migrating from 1.x

Template versions through 1.1.1 created their own GVC. Version 2.0.0 does not — and Helm deletes what a chart stops declaring, so **an in-place upgrade of a 1.x release onto 2.0.0 deletes the GVC the 1.x release created, with every workload, volume set and identity in it, including your data.** The chart refuses to render when your values still carry the 1.x `gvc` key, so the usual path fails safely:

```text theme={null}
Error: execution error at (mongodb-cluster/templates/identity.yaml:1:4): mongodb-cluster 2.0.0: the
`gvc` values key was REMOVED. This chart no longer creates a GVC -- it deploys into the GVC you
install into, and `gvc.locations` moved to the top-level `locations`. DO NOT `helm upgrade` a 1.x
release onto 2.0.0 ...
```

<Warning>
  **Do not run `cpln helm upgrade` on a 1.x release with the 2.0.0 chart under any circumstances**, including with no values at all — the guard can only see values you pass. Migrate to a new release instead.
</Warning>

<Steps>
  <Step title="Pick an existing GVC and create the prerequisite secrets">
    Choose a GVC that already has the locations you will list, and create both [prerequisite secrets](#prerequisites) with new values. The new cluster is bootstrapped from them on a fresh data directory, so this is the moment to choose a new password and generate a new keyfile.
  </Step>

  <Step title="Install 2.0.0 as a new release">
    Use a **different release name** — secret and policy names are org-wide. Re-check your roster against the [sizing table](#locations): 1.x defaulted to nine members, and only seven of them were ever in the replica set.
  </Step>

  <Step title="Move the data across">
    Dump from the old cluster and restore into the new one, tunnelling to each with `cpln port-forward` — the procedure is [Restoring a Backup](#restoring-a-backup), with `mongodump --uri="mongodb://USERNAME:PASSWORD@127.0.0.1:27017/?authSource=admin" --gzip --archive=./backup.gz` against the old proxy in place of downloading an archive.
  </Step>

  <Step title="Point your applications at the new release, then uninstall the old one">
    Run `cpln helm uninstall` against the GVC you installed the 1.x release into — that is where Helm tracks it. Uninstalling the 1.x release deletes the GVC it created and everything still in it.
  </Step>
</Steps>

Values that moved or were removed in 2.0.0. Each is named at render time rather than ignored:

| 1.x | 2.0.0 |
| - | - |
| `gvc.name` | Gone. The chart deploys into the GVC you install into |
| `gvc.locations` | `locations`, at the top level |
| Backup location derived from `backup.aws.region` | `backup.location`, explicit and validated against `locations` |
| `backup.mode: physical` and the `backup.physical` block | Gone. Its restore could never run — see [Backing Up](#backing-up) |
| `locations[].replicas: 0` | Not allowed. Remove the location instead |
| A total above 7 members, or exactly 2 | Not allowed. See the sizing table |

**Coming from 1.0.0 specifically.** That version took the credentials as plain values with a working default password, and shipped a working replica set keyfile as the `mongodb.replicaSetKey` default. Both were published in the public template repository, so treat an unchanged 1.0.0 password and keyfile as compromised, not merely outdated — do not carry either across. Version 1.1.0 replaced them with the two prerequisite secrets. While the old cluster is still running and you need its credentials to dump from it, read them from the secret 1.0.0 created (keys `username`, `password`, `database`), or from the values file you installed with:

```bash theme={null}
cpln secret reveal RELEASE_NAME-mongo-config -o yaml
```

### Scaling and Availability

**What the cluster survives.** Only the primary accepts writes; when it is lost, the remaining members elect a new primary and the proxy re-routes to it, so clients using the proxy address keep the same connection string. With three members in one location the set survives losing one member; spread across three locations with one member each it survives losing a whole location. A member that comes back rejoins as a secondary on its own volume. Members are voting members, so size against the [table under Locations](#locations).

**A `cpln helm upgrade` that changes the MongoDB tier, or a forced redeployment, restarts the whole tier.** The template sets no limit on how many members restart at once, so every member in every location can restart together; each runs `rs.stepDown` in its pre-stop hook before it stops, but there is no primary while the members are down. Plan it as a short write outage with client retries, and do not perform other maintenance or lose a member while the tier is restarting — a set that has not yet re-formed a majority cannot elect a primary.

**Scaling up.** Increase `replicas` for a location, or append a location the GVC already has, and upgrade the release. New members find the primary through the other members and register themselves into the replica set; no manual step is needed. Stay within 7 members in total and 6 per location.

**Scaling down.** Remove departing members from the replica set configuration **before** reducing `replicas`; otherwise the configuration keeps referencing hosts that no longer exist, which distorts elections and quorum. Through a [tunnel to the proxy](#connecting), for each departing member (highest index first):

```javascript theme={null}
rs.remove("replica-N.RELEASE_NAME-mongo.LOCATION_NAME.GVC_NAME.cpln.local:27017")
```

Then upgrade the release with the lower count. To stop running in a location entirely, remove its entry from `locations`.

**Storage.** Each member's data lives on its own volume, survives restarts and upgrades, and grows automatically when `volumeset.autoscaling.enabled` is `true`. `cpln helm uninstall` deletes the volume set; a final snapshot is retained for 7 days.

### Troubleshooting

<AccordionGroup>
  <Accordion title="The workload never becomes ready and cpln logs is empty">
    **Symptom:** `cpln helm install` reported success, `RELEASE_NAME-mongo` never reaches ready, and `cpln logs` returns no lines. `cpln workload get-deployments RELEASE_NAME-mongo --gvc GVC_NAME -o yaml` shows under `status.versions[].message`:

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

    **Cause:** `mongodb.credentialsSecretName` or `mongodb.keyfileSecretName` names a secret that does not exist in this org.

    **Fix:** Create the secret as shown in [Prerequisites](#prerequisites), then run `cpln workload force-redeployment RELEASE_NAME-mongo --gvc GVC_NAME` or wait several minutes for the deployment to recover on its own.
  </Accordion>

  <Accordion title="FATAL: the keyfile in secret ... contains characters outside the base64 alphabet">
    **Symptom:** The member container exits with code 1 before MongoDB starts, and `cpln logs '{gvc="GVC_NAME", workload="RELEASE_NAME-mongo"}' --limit 50 --since 10m` shows `FATAL: the keyfile in secret '...' contains characters outside the base64 alphabet` or `... is N characters; mongod requires 6-1024`.

    **Cause:** The keyfile secret holds a hand-written passphrase or a truncated value. MongoDB accepts only 6–1024 characters from `A-Z a-z 0-9 + / =`.

    **Fix:** On a cluster that has never started, recreate the secret with `openssl rand -base64 756 | cpln secret create-opaque --name KEYFILE_SECRET_NAME --encoding plain -f -` (delete the old one first) and force a redeployment. The keyfile cannot be changed once the cluster is initialized.
  </Accordion>

  <Accordion title="FATAL: refusing to bootstrap a replica set smaller than it was configured to be">
    **Symptom:** Member logs show `locations declared in values are not in GVC 'GVC_NAME': ...` followed by either `FATAL: refusing to bootstrap a replica set smaller than it was configured to be` (fresh members, which exit) or `This member holds data and keeps running rather than taking a live cluster down` (members that already hold data, which keep serving).

    **Cause:** `locations` names a location the GVC does not have. The platform accepts the placement without error, so the members there never start and the replica set would be smaller than the quorum you sized.

    **Fix:** Add the missing location to the GVC, or remove it from `locations` and upgrade the release. Check the GVC's locations with `cpln gvc get GVC_NAME -o yaml` under `spec.staticPlacement.locationLinks`.
  </Accordion>

  <Accordion title="This workload location is deactivated because maxScale is set to 0">
    **Symptom:** `cpln workload get-deployments RELEASE_NAME-mongo --gvc GVC_NAME -o yaml` shows a location with `This workload location is deactivated because maxScale is set to 0.` and no replicas.

    **Cause:** The GVC has a location that is not in `locations`. This is intended: nothing runs there and no volume is provisioned.

    **Fix:** None needed. To run members there, add the location to `locations` and upgrade the release.
  </Accordion>

  <Accordion title="Install rejected with exceed the autoscaling.maxScale of 6 (quota: replicas-per-replica-direct-workload)">
    **Symptom:** `cpln helm install` creates the proxy and then fails on `RELEASE_NAME-mongo` with `One or more containers exceed the autoscaling.maxScale of 6 (quota: replicas-per-replica-direct-workload). Request quota increase`.

    **Cause:** A single location lists 7 members. The chart allows 7 in total, but a default org quota caps this workload at 6 replicas per location.

    **Fix:** Split the roster across locations (for example 4 + 3), or request a quota increase. Uninstall the failed release before reinstalling.
  </Accordion>

  <Accordion title="Render fails with the gvc values key was REMOVED or backup.mode physical was REMOVED">
    **Symptom:** `cpln helm install` or `cpln helm upgrade` exits with a `mongodb-cluster 2.0.0:` render error naming the `gvc` key or `backup.mode 'physical'`, and nothing is changed.

    **Cause:** Your values file is from a 1.x release. The `gvc` block and physical backups were removed in 2.0.0, and an in-place upgrade from 1.x is refused deliberately.

    **Fix:** Follow [Migrating from 1.x](#migrating-from-1-x). For a 2.0.0 values file, remove the `gvc` block and set `backup.mode: logical`.
  </Accordion>
</AccordionGroup>

## Important Notes

* **Create both prerequisite secrets before installing.** A missing secret wedges the deployment with no log output; the only diagnostic is `status.versions[].message` from `cpln workload get-deployments`.
* **Generate the keyfile with `openssl rand -base64 756`**, never a passphrase, and never change it after the cluster is initialized.
* **Never `helm upgrade` a 1.x release onto 2.0.0.** It deletes the GVC the 1.x chart created and everything in it. Migrate to a new release.
* **The GVC must already have every location you list**, and may have more; unlisted locations run nothing. Never reorder `locations` after install — only append.
* **At most 7 members in total and 6 in one location.** A 2-member set is refused; 3 is the usual minimum.
* **Connection strings need `authSource=admin`**, and clients should use the proxy address so failover needs no client change.
* **Rotating a secret does not restart anything.** The old value keeps working with no error until you run `cpln workload force-redeployment`.
* **Remove members with `rs.remove` before scaling down.** An upgrade or forced redeployment restarts the whole MongoDB tier at once — plan a short write outage and let it finish before any other maintenance.
* **Backups are full-cluster dumps** with no retention policy of their own; use a bucket lifecycle rule. Restore runs through `cpln port-forward`.
* **Uninstalling deletes the volume set.** A final snapshot is retained for 7 days.

## External References

<CardGroup cols={2}>
  <Card title="Percona Server for MongoDB" href="https://docs.percona.com/percona-server-for-mongodb/" icon="database">
    Official documentation for the database image this template runs
  </Card>

  <Card title="MongoDB Replication" href="https://www.mongodb.com/docs/manual/replication/" icon="circle-nodes">
    Replica set concepts, elections and failover
  </Card>

  <Card title="Replica Set Members" href="https://www.mongodb.com/docs/manual/core/replica-set-members/" icon="users">
    Member roles and the 7-voting-member limit
  </Card>

  <Card title="mongorestore Reference" href="https://www.mongodb.com/docs/database-tools/mongorestore/" icon="box-archive">
    Options for restoring archives, including --drop and --nsInclude
  </Card>

  <Card title="MongoDB Connection String" href="https://www.mongodb.com/docs/manual/reference/connection-string/" icon="link">
    Connection string URI format and options
  </Card>

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