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

# PostgreSQL

> Single-replica PostgreSQL 18 on a persistent volume, with optional PgBouncer connection pooling and scheduled S3, GCS or MinIO backups. Deploys into an existing GVC and needs a dictionary credentials secret created before install.

## Overview

PostgreSQL is a widely used open-source relational database. This template deploys one PostgreSQL 18 server on a persistent volume, an optional [PgBouncer](https://www.pgbouncer.org/) connection pooler in front of it, and an optional cron job that writes compressed `pg_dumpall` backups to AWS S3, Google Cloud Storage or a MinIO (S3-compatible) bucket.

Database credentials are **not** template values. The server reads its username, password and database name from a [dictionary secret](/guides/create-secret/dictionary) you create before installing, so no password passes through Helm or lands in the release. The template creates no secret of its own.

<Note>
  This template deploys into an existing [GVC](/reference/gvc) that you already have. It does not create or manage a GVC.
</Note>

<Note>
  This is a single server, not a cluster — there is no replication or failover. For an automatically failing-over cluster in one location use [PostgreSQL Highly Available](/template-catalog/templates/postgres-highly-available); to span regions use [Postgres Multi Location](/template-catalog/templates/postgres-multi-location).
</Note>

### What Gets Created

| Resource | Name | Purpose | Optional |
| - | - | - | - |
| Workload | `RELEASE_NAME-postgres` | PostgreSQL 18 server (stateful, 1 replica, TCP `5432`) | — |
| Volume Set | `RELEASE_NAME-pg-vs` | `PGDATA` on an `ext4` general-purpose SSD volume; a final snapshot is taken when the volume set is deleted and kept 7 days | — |
| Identity | `RELEASE_NAME-pg-identity` | Shared by the database, pooler and backup workloads; carries the cloud account binding when backups use AWS or GCP | — |
| Policy | `RELEASE_NAME-pg-policy` | Grants the identity `reveal` on exactly the credential secrets you created | — |
| Workload | `RELEASE_NAME-pgbouncer` | PgBouncer connection pooler (serverless, TCP `5432`) | `pgbouncer.enabled: true` |
| Workload | `RELEASE_NAME-postgres-backup` | Scheduled `pg_dumpall` to S3, GCS or MinIO (cron) | `backup.enabled: true` |

## Prerequisites

One secret must exist before you install. It holds the credentials your applications put in their connection strings. 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`. PostgreSQL creates that user (as a superuser) and that database the first time the volume is initialized:

    ```bash theme={null}
    cpln secret create-dictionary --name SECRET_NAME \
      --entry username=postgres \
      --entry password='YOUR-STRONG-PASSWORD' \
      --entry database=mydb
    ```

    Set `config.credentialsSecretName` to this name. Read it back later with `cpln secret reveal SECRET_NAME -o yaml` — a bare `cpln secret reveal` prints only a summary table, not the values.
  </Step>
</Steps>

<Warning>
  **Create the secret before installing, or the deployment wedges silently.** A `config.credentialsSecretName` that points at a secret which does not exist installs successfully and then never starts. The container never 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-postgres --gvc GVC_NAME -o yaml
  ```

  ```text theme={null}
  The secret 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. Creating the missing secret repairs the deployment on its own within several minutes, or run `cpln workload force-redeployment RELEASE_NAME-postgres --gvc GVC_NAME` to skip the wait.
</Warning>

Backups are optional and need a bucket plus, for AWS or GCP, a Control Plane [cloud account](/guides/create-cloud-account) — see [Backing Up](#backing-up). Nothing else is required for a default install.

## Installation

Install the chart from the marketplace registry, pointing it at the secret you created:

```bash theme={null}
cpln helm install RELEASE_NAME oci://ghcr.io/controlplane-com/templates/postgres \
  --version 3.4.1 --gvc GVC_NAME \
  --set config.credentialsSecretName=SECRET_NAME
```

Or follow the instructions for 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>

## Configuration

Each block below is the shipped default for that part of `values.yaml`.

### Server and Resources

```yaml theme={null}
image: postgres:18  # versions before postgres:17 are compatible but do not support backup feature

resources:
  minCpu: 200m
  minMemory: 128Mi
  maxCpu: 500m
  maxMemory: 256Mi
```

<Note>
  On a stateful workload the ratio of `maxCpu` to `minCpu` may not exceed 4:1. Raising `maxCpu` without raising `minCpu` can cross that limit and is rejected when the workload is applied.
</Note>

### Credentials

```yaml theme={null}
config:
  # REQUIRED PREREQUISITE SECRET — CREATE IT BEFORE YOU INSTALL.
  # A `dictionary` secret holding exactly three keys: `username`, `password` and
  # `database`. If it does not exist at install time the deployment WEDGES
  # silently — `cpln logs` returns nothing at all. See Prerequisites in the README
  # for the exact `cpln secret create-dictionary` command.
  credentialsSecretName: my-postgres-credentials
```

The three keys are applied only when the volume is first initialized. Changing the secret afterwards does not change the stored password — see [Rotating the Password](#rotating-the-password).

### Storage

```yaml theme={null}
volumeset:
  capacity: 10 # initial capacity in GiB (minimum is 10)
  autoscaling:
    enabled: false # Set to true to enable autoscaling
    maxCapacity: 100 # Maximum capacity in GiB when autoscaling is enabled
    minFreePercentage: 10 # Minimum free percentage to trigger scaling when autoscaling is enabled
    scalingFactor: 1.2 # Scaling factor to determine how much to scale up when autoscaling is triggered
```

### Internal Access

```yaml theme={null}
internalAccess: # Sets the internal firewall scope - if set to none, replicas will not be able to reach each other
  type: same-gvc # options: none, same-gvc, same-org, workload-list
  workloads:  # Note: can only be used if type is same-gvc or workload-list
    #- //gvc/GVC_NAME/workload/WORKLOAD_NAME
    #- //gvc/GVC_NAME/workload/WORKLOAD_NAME
```

| Type | Who can reach port `5432` |
| - | - |
| `none` | No workload |
| `same-gvc` | Every workload in the same GVC |
| `same-org` | Every workload in the same org |
| `workload-list` | Only the workloads listed in `internalAccess.workloads` |

The same setting applies to the PgBouncer workload when it is enabled. A change to `internalAccess` can take several minutes to take effect — re-test before concluding it did not apply.

### PgBouncer

PgBouncer multiplexes many application connections into a small pool of real database connections. When enabled it becomes the endpoint your applications connect to, and it reads the same credentials secret and identity as the server — nothing extra to configure.

```yaml theme={null}
pgbouncer:
  enabled: false
  image: edoburu/pgbouncer:v1.25.1-p0
  poolMode: transaction # options: session, transaction, statement
  defaultPoolSize: 25   # number of real Postgres connections PgBouncer maintains
  maxClientConn: 1000   # maximum number of client connections PgBouncer accepts
  replicas: 1

  resources:
    cpu: 200m
    memory: 128Mi
```

| `poolMode` | Behaviour |
| - | - |
| `transaction` | The server connection is held for one transaction. Best for most web and API workloads; not compatible with `SET` variables, temporary tables or advisory locks. |
| `session` | Held for the whole client session. Compatible with everything, less reuse — raise `defaultPoolSize` to your expected concurrency. |
| `statement` | Returned after every statement. Transactions are not supported. |

### Backup

A cron workload runs `pg_dumpall` on the schedule and uploads a gzip-compressed SQL dump of every database and role to the bucket for the chosen `provider`. It authenticates with the `username` and `password` from your credentials secret. Each provider's bucket, cloud account and policy steps are under [Backing Up](#backing-up).

```yaml theme={null}
backup: # compatible with Postgres 17+
  enabled: false
  image: ghcr.io/controlplane-com/backup-images/postgres-backup:18.1.0 # tag 18.1.0 = Postgres 18, 17.1.0 = Postgres 17
  schedule: "0 2 * * *"   # daily at 2am UTC

  resources:
    cpu: 100m
    memory: 128Mi

  provider: aws # Options: aws, gcp, or minio

  aws:
    bucket: my-postgres-bucket
    region: us-east-1
    cloudAccountName: my-s3-cloud-account
    policyName: my-postgres-backup-policy # bucket-scoped IAM policy, see README
    prefix: postgres/backups # folder name where your backups will be stored

  gcp:
    bucket: my-postgres-bucket
    cloudAccountName: my-gcs-cloud-account
    prefix: postgres/backups # folder name where your backups will be stored

  minio: # Backup to a self-hosted MinIO workload (or any S3-compatible endpoint)
    endpoint: http://my-minio-workload:9000 # e.g. http://WORKLOAD_NAME:9000 for an internal MinIO template deployment
    bucket: my-postgres-bucket
    # REQUIRED PREREQUISITE SECRET when provider is `minio` — a `dictionary`
    # secret holding `accessKey` and `secretKey`. See Storage setup in the README.
    credentialsSecretName: my-postgres-minio-credentials
    prefix: postgres/backups # folder name where your backups will be stored
```

<Note>
  Backups require PostgreSQL 17 or later, and `backup.image` must match the server's major version: tag `18.1.0` for `postgres:18`, `17.1.0` for `postgres:17`.
</Note>

## Connecting

This template exposes no public endpoint. Everything below is reachable from inside the GVC, subject to `internalAccess.type`.

| What | Address | Notes |
| - | - | - |
| PostgreSQL, direct | `RELEASE_NAME-postgres.GVC_NAME.cpln.local:5432` | Always available. |
| PostgreSQL, pooled | `RELEASE_NAME-pgbouncer.GVC_NAME.cpln.local:5432` | Only when `pgbouncer.enabled: true`. Prefer it when the pooler is on. |
| Credentials | `cpln secret reveal SECRET_NAME -o yaml` | The `username`, `password` and `database` keys. Never stored in the Helm release. |

To verify from your own machine, tunnel to the server and connect with `psql`:

```bash theme={null}
cpln port-forward RELEASE_NAME-postgres 5432:5432 --gvc GVC_NAME
```

```bash theme={null}
psql "postgresql://USERNAME:PASSWORD@127.0.0.1:5432/DATABASE" -c 'select version()'
```

Or run `psql` inside the container, where the credentials are already present as environment variables:

```bash theme={null}
cpln workload exec RELEASE_NAME-postgres --gvc GVC_NAME --container postgresql -- psql -U USERNAME -d DATABASE -c 'select version()'
```

## Operations

### Backing Up

Backups are off by default. Enable them with `backup.enabled: true`, pick a `backup.provider`, and complete that provider's setup below first. The job writes `postgres-TIMESTAMP.sql.gz` objects under `backup.PROVIDER.prefix` in your bucket on every run of `backup.schedule`.

<Note>
  The AWS S3 and MinIO paths have been exercised end to end, including downloading a dump and restoring it. The GCS path renders and binds correctly but has not been exercised the same way — rehearse a backup and restore before relying on it.
</Note>

#### Backup Prerequisites

Complete the steps for your provider before enabling backups.

#### 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 one yet, create a Control Plane [cloud account](/guides/create-cloud-account) for the AWS account holding the bucket. Set `backup.aws.cloudAccountName` to its name.

3. Create an IAM policy scoped to exactly that bucket, replacing `YOUR_BUCKET_NAME`:

```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.aws.policyName` to that policy's name. The template attaches it to the workload identity and nothing else — the bucket in your policy is the only storage the backup job can reach.

#### GCS

1. Create a GCS bucket. Set `backup.gcp.bucket` to its name.

2. Create a Control Plane [cloud account](/guides/create-cloud-account) for the GCP project holding the bucket, and set `backup.gcp.cloudAccountName` to its name.

3. Grant the cloud account's service account the **Storage Admin** (`roles/storage.admin`) role. The template additionally binds the identity to `roles/storage.objectAdmin` on exactly the bucket in `backup.gcp.bucket`.

#### MinIO

No cloud account is needed — the job authenticates with a second prerequisite secret.

1. Create the bucket in MinIO. Set `backup.minio.bucket` to its name.

2. Set `backup.minio.endpoint` to the S3 API address including the port. For the [minio](/template-catalog/templates/minio) template deployed in the same GVC that is `http://WORKLOAD_NAME.GVC_NAME.cpln.local:9000`.

3. Create a [dictionary secret](/guides/create-secret/dictionary) holding exactly the keys `accessKey` and `secretKey`, and set `backup.minio.credentialsSecretName` to its name. For the `minio` template these are its admin username and password:

```bash theme={null}
cpln secret create-dictionary --name SECRET_NAME \
  --entry accessKey=MINIO_ACCESS_KEY \
  --entry secretKey=MINIO_SECRET_KEY
```

The policy this template creates grants the identity `reveal` on this secret as well as the database credentials secret, and on nothing else.

### Restoring a Backup

Each backup is a gzip-compressed `pg_dumpall` script, so it is restored by connecting to the `postgres` maintenance database as the superuser from your credentials secret; the script recreates the roles and databases it contains. Run the commands from a client that can reach both the bucket and the server — a workload inside the GVC, or your own machine with `cpln port-forward RELEASE_NAME-postgres 5432:5432 --gvc GVC_NAME` open and `--host=127.0.0.1` in place of the internal hostname.

<Tabs>
  <Tab title="AWS S3">
    ```bash theme={null}
    export PGPASSWORD="PASSWORD"

    aws s3 cp "s3://BUCKET_NAME/PREFIX/postgres-TIMESTAMP.sql.gz" - \
      | gunzip \
      | psql \
          --host=RELEASE_NAME-postgres.GVC_NAME.cpln.local \
          --port=5432 \
          --username=USERNAME \
          --dbname=postgres

    unset PGPASSWORD
    ```
  </Tab>

  <Tab title="GCS">
    ```bash theme={null}
    export PGPASSWORD="PASSWORD"

    gsutil cp "gs://BUCKET_NAME/PREFIX/postgres-TIMESTAMP.sql.gz" - \
      | gunzip \
      | psql \
          --host=RELEASE_NAME-postgres.GVC_NAME.cpln.local \
          --port=5432 \
          --username=USERNAME \
          --dbname=postgres

    unset PGPASSWORD
    ```
  </Tab>

  <Tab title="MinIO">
    ```bash theme={null}
    export PGPASSWORD="PASSWORD"
    export AWS_ACCESS_KEY_ID="MINIO_ACCESS_KEY"
    export AWS_SECRET_ACCESS_KEY="MINIO_SECRET_KEY"
    aws configure set default.s3.addressing_style path

    aws s3 cp "s3://BUCKET_NAME/PREFIX/postgres-TIMESTAMP.sql.gz" - --endpoint-url "http://MINIO_ENDPOINT:9000" \
      | gunzip \
      | psql \
          --host=RELEASE_NAME-postgres.GVC_NAME.cpln.local \
          --port=5432 \
          --username=USERNAME \
          --dbname=postgres

    unset PGPASSWORD AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY
    ```
  </Tab>
</Tabs>

`already exists` errors for the role and database that the server created at first boot are expected; the rest of the script loads normally. Restore into an empty database (a fresh install, or one whose databases you have dropped) to avoid duplicate-object errors on existing tables.

### Rotating the Password

The server reads the secret only when the volume is first initialized, so change the password inside PostgreSQL first, then update the secret to match:

```sql theme={null}
ALTER ROLE USERNAME WITH PASSWORD 'NEW-STRONG-PASSWORD';
```

Workloads resolve the secret when a replica starts, so an updated secret is not picked up by a running replica. The backup job reads it fresh on its next scheduled run; if PgBouncer is enabled, force a redeployment so it reconnects with the new password:

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

### Upgrading From Earlier Versions

Template versions up to 3.3.0 took the database credentials as plain Helm values with a working default password, and wrote them into a chart-owned secret named after the release. Version 3.4.0 removed both, and an upgrade that still carries the old keys is refused at render — your existing release is left running rather than restarted against a different password.

| | 3.3.0 and earlier | 3.4.0 and later |
| - | - | - |
| Database credentials | `config.username`, `config.password`, `config.database` values | `config.credentialsSecretName` → a dictionary secret you create |
| Credential secret | `RELEASE_NAME-pg-config`, created by the chart | Not created — credentials live only in your secret |
| MinIO backup credentials | `backup.minio.accessKey`, `backup.minio.secretKey` values | `backup.minio.credentialsSecretName` → a dictionary secret you create |
| Backup identity permissions | Bucket-scoped policy plus `aws::ReadOnlyAccess` | Bucket-scoped policy only — no action needed, backups keep working |
| `backup.schedule` | Any cron beginning with `*` failed to render | Accepts every cron form, including `*/15 * * * *` |

<Steps>
  <Step title="Read the credentials the database already uses">
    An existing database keeps the credentials it was initialized with. Recover them from your current values file, or from the secret the old version created — **before** upgrading:

    ```bash theme={null}
    cpln secret reveal RELEASE_NAME-pg-config -o yaml
    ```
  </Step>

  <Step title="Create the prerequisite secret with those same values">
    Follow [Prerequisites](#prerequisites) using the existing `username`, `password` and `database`. Different values here do not change the database — they only leave the workload unable to authenticate.
  </Step>

  <Step title="Remove the old keys from your values">
    Delete `config.username`, `config.password` and `config.database`, and set `config.credentialsSecretName` to your secret's name. If you back up to MinIO, replace `backup.minio.accessKey` and `backup.minio.secretKey` with `backup.minio.credentialsSecretName`.
  </Step>

  <Step title="Upgrade, then rotate any default password">
    After the upgrade succeeds, change any password that came from a 3.3.x default — those defaults were published in the public template repository, so treat them as compromised. Follow [Rotating the Password](#rotating-the-password).
  </Step>
</Steps>

<Warning>
  **The chart no longer creates `RELEASE_NAME-pg-config`.** Anything outside this release that referenced that secret by name — another workload's environment, a policy, or a parent chart that bundles PostgreSQL as a subchart — must be pointed at the secret you created instead. A reference to a secret that no longer exists does not fail loudly; it wedges the referring workload silently, exactly as described in [Prerequisites](#prerequisites).
</Warning>

### Scaling and Availability

* The server is pinned to one replica. Do not raise it — a single volume set backs a single server, and a second replica cannot mount it. Choose [PostgreSQL Highly Available](/template-catalog/templates/postgres-highly-available) or [Postgres Multi Location](/template-catalog/templates/postgres-multi-location) when you need failover.
* PgBouncer is stateless; `pgbouncer.replicas` scales it horizontally for high connection counts.
* Every `cpln helm upgrade` restarts the server, including the first upgrade after an install even when nothing changed. Plan each one as a short write outage.
* A node failure reschedules the server and reattaches the same volume. The exposure is minutes of downtime, not data loss. The volume set is not snapshotted on a schedule — it takes one final snapshot when it is deleted, kept for seven days — so use the backup job for point-in-time copies.

### Troubleshooting

<AccordionGroup>
  <Accordion title="Workload never becomes ready and cpln logs returns nothing">
    **Symptom:** the install succeeds, the workload stays not ready indefinitely, and `cpln logs '{gvc="GVC_NAME", workload="RELEASE_NAME-postgres"}' --limit 50 --since 10m` returns zero lines.

    **Cause:** `config.credentialsSecretName` names a secret that does not exist, so the container is never started. The message is only visible in `status.versions[].message`:

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

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

    **Fix:** create the secret as in [Prerequisites](#prerequisites). The deployment recovers on its own within several minutes, or run `cpln workload force-redeployment RELEASE_NAME-postgres --gvc GVC_NAME`.
  </Accordion>

  <Accordion title="Upgrade fails with 'config.username was REMOVED in postgres 3.4.0'">
    **Symptom:** `cpln helm upgrade` stops before touching any resource:

    ```text theme={null}
    Error: execution error at (postgres/templates/workload-postgres.yaml:1:4): config.username was
    REMOVED in postgres 3.4.0. Database credentials are no longer values: create a `dictionary` secret
    holding the keys `username`, `password` and `database`, and set config.credentialsSecretName to its
    name. See Prerequisites in the README.
    ```

    `config.password`, `config.database`, and `backup.minio.accessKey` / `backup.minio.secretKey` each produce the same kind of message under their own name.

    **Cause:** your values still carry keys that were removed in 3.4.0.

    **Fix:** follow [Upgrading From Earlier Versions](#upgrading-from-earlier-versions). Your running release is unaffected until the upgrade succeeds.
  </Accordion>

  <Accordion title="Password changed in the secret but the old one still works">
    **Symptom:** you updated the `password` key in the credentials secret, but PostgreSQL still accepts the previous password and rejects the new one.

    **Cause:** the secret is applied only when the volume is first initialized. Afterwards the stored password lives in the database, not the secret.

    **Fix:** change it inside PostgreSQL and then update the secret to match — see [Rotating the Password](#rotating-the-password).
  </Accordion>

  <Accordion title="PgBouncer admin console rejects the database user with 'FATAL: not allowed'">
    **Symptom:** `psql -d pgbouncer` against the pooler fails with `FATAL: not allowed`.

    **Cause:** the PgBouncer image restricts its admin console to the `postgres` user. This is upstream behaviour, not a template setting.

    **Fix:** connect to your application database through the pooler as normal; use the admin console only if your credentials secret's `username` is `postgres`.
  </Accordion>

  <Accordion title="Apply rejected with 'The ratio between cpu and minCpu must be less than 4:1'">
    **Symptom:** an install or upgrade fails and the server workload is not created or updated.

    **Cause:** `resources.maxCpu` is more than four times `resources.minCpu`, which the platform rejects on a stateful workload.

    **Fix:** raise `resources.minCpu` or lower `resources.maxCpu` so the ratio is at most 4:1.
  </Accordion>
</AccordionGroup>

## Important Notes

* **Create the credentials secret before installing.** A reference to a secret that does not exist wedges the workload with no log output; see [Prerequisites](#prerequisites) for the one command that shows the reason.
* **Credentials are read only when the volume is first initialized.** Rotate with `ALTER ROLE` inside PostgreSQL, then update the secret — see [Rotating the Password](#rotating-the-password).
* **Do not scale the server past one replica.** It is a single instance on a single volume, not a replicated cluster.
* **`cpln helm uninstall` deletes the volume set** and the database with it. Your credentials secret is yours and is left alone.
* **Every `cpln helm upgrade` restarts the server.** Treat each one as a short planned write outage.
* **Upgrading from 3.3.0 or earlier is a breaking change** — the removed keys stop the upgrade rather than resetting a password. See [Upgrading From Earlier Versions](#upgrading-from-earlier-versions).

## External References

<CardGroup cols={2}>
  <Card title="PostgreSQL Documentation" icon="database" href="https://www.postgresql.org/docs/">
    Official PostgreSQL documentation
  </Card>

  <Card title="PostgreSQL Backup and Restore" icon="book" href="https://www.postgresql.org/docs/current/backup-dump.html">
    Upstream guidance on `pg_dumpall` dumps and restores
  </Card>

  <Card title="PgBouncer Documentation" icon="database" href="https://www.pgbouncer.org/config.html">
    PgBouncer configuration reference
  </Card>

  <Card title="Backup Image Source" icon="github" href="https://github.com/controlplane-com/backup-images/tree/main/postgres-backup">
    Source for the PostgreSQL backup container image
  </Card>

  <Card title="PostgreSQL Template" icon="github" href="https://github.com/controlplane-com/templates/tree/main/postgres">
    Source files, default values, and chart definition
  </Card>
</CardGroup>
