> ## Documentation Index
> Fetch the complete documentation index at: https://controlplanecorporation-majid-docs-content-expansion.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# TimescaleDB

> Deploy TimescaleDB — the PostgreSQL 18 time-series database — on Control Plane. Covers the prerequisite credentials secret, hypertables, compression, continuous aggregates, retention, PgBouncer pooling, and scheduled S3, GCS, or MinIO backups.

<Warning>
  **Version 1.2.0 moves the MinIO backup credentials out of values.** They were plain values that landed in your Helm release. They are now a `dictionary` secret you create before installing, holding `accessKey` and `secretKey`. Only affects installs backing up to MinIO.
</Warning>

## Overview

TimescaleDB is a time-series database built as a PostgreSQL extension. This template deploys a single-instance PostgreSQL 18 server with the TimescaleDB Community extension preloaded and auto-created, giving you hypertables, columnar compression, continuous aggregates, and retention policies alongside regular relational tables — all through any PostgreSQL client or ORM. Optional PgBouncer connection pooling and scheduled backups to AWS S3, GCS, or a self-hosted MinIO instance are included.

Database credentials are **not** template values. TimescaleDB reads its username, password and database name from a dictionary secret you create before installing, so no password passes through Helm or lands in the release.

<Warning>
  **Template version 1.1.0 is a breaking security change.** `config.username` and `config.password` were removed, and an install or upgrade that still sets either one now fails at render instead of silently falling back to a published default password. If you are running 1.0.0, read [Upgrading From 1.0.0 or Earlier](#upgrading-from-1-0-0-or-earlier) before you touch the release.
</Warning>

<Info>
  TimescaleDB is licensed under the Timescale License (TSL). It is free to self-host, including all Community features (compression, continuous aggregates, retention); the license only forbids reselling TimescaleDB itself as a managed database service.
</Info>

<Note>
  TimescaleDB on Control Plane operates as a single-instance deployment, pinned to one replica. Do not scale up the replica count — PostgreSQL is a single-writer database, and additional replicas would run as isolated instances rather than a cluster.
</Note>

### What Gets Created

* **Stateful TimescaleDB Workload** — A single-replica PostgreSQL 18 + TimescaleDB 2.28.3 container on port `5432`. The extension is preloaded, created automatically in your database, and auto-tuned to the container's resources at first boot.
* **Volume Set** — Persistent storage for the database data directory, with optional autoscaling and 7-day snapshots.
* **Backup Config Secret** *(optional)* — A dictionary secret holding the backup destination. Created only when `backup.enabled: true`, and it holds no database credentials.
* **Identity & Policy** — An identity bound to the TimescaleDB, PgBouncer and backup workloads, and a policy granting it `reveal` on exactly the credentials secret you created — plus the backup config secret and Cloud Account binding when backup is enabled.
* **PgBouncer Workload** *(optional)* — A PgBouncer connection pooler deployed as a separate workload in front of TimescaleDB.
* **Backup Cron Workload** *(optional)* — A scheduled `pg_dumpall` backup job that writes compressed SQL dumps to AWS S3, GCS, or MinIO.

The template creates **no credential secret of its own.** The username, password and database name live only in the prerequisite secret described below.

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

## Upgrading From 1.0.0 or Earlier

Template versions up to 1.0.0 took the database credentials as plain Helm values, shipped working defaults for them, and wrote those credentials into a chart-owned secret named after the release. Version 1.1.0 removes all of that.

|                          | 1.0.0 and earlier                                                     | 1.1.0                                                           |
| ------------------------ | --------------------------------------------------------------------- | --------------------------------------------------------------- |
| Database credentials     | `config.username`, `config.password`, `config.database` values        | `config.credentialsSecretName` → a dictionary secret you create |
| Where the password lives | Chart-created secret `RELEASE_NAME-tsdb-config`, and the Helm release | Only the secret you create                                      |

<Warning>
  **Carrying the old values forward stops the upgrade.** The removed keys are rejected at render, so `cpln helm upgrade` fails and your existing release is left untouched and running, rather than quietly restarting against a different password:

  ```text theme={null}
  Error: execution error at (timescaledb/templates/identity.yaml:1:4): timescaledb: config.username
  and config.password were REMOVED — they are now a `dictionary` secret you create, named by
  config.credentialsSecretName, holding the keys `username`, `password` and `database`. Delete
  them from your values. See Prerequisites in the README.
  ```
</Warning>

<Steps>
  <Step title="Read the credentials the database already uses">
    Credentials are written into the data directory the first time the volume is initialized, so an existing database keeps whatever it was created with. Recover them from your current values file, or from the secret the old version created — do this **before** upgrading:

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

    Pass `-o yaml`. A bare `cpln secret reveal` prints only a summary table, not the values.
  </Step>

  <Step title="Create the prerequisite secret with those same values">
    Follow [Prerequisites](#prerequisites), using the existing username, password and database name. Different values here do not change the database — they just 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.
  </Step>

  <Step title="Upgrade, then rotate the password">
    After the upgrade succeeds, change any password that came from a 1.0.0 default — those defaults were published in the public template repository, so treat them as compromised. Rotate inside TimescaleDB, then update the secret to match:

    ```sql theme={null}
    ALTER ROLE myuser WITH PASSWORD 'NEW-STRONG-PASSWORD';
    ```
  </Step>
</Steps>

<Warning>
  **`RELEASE_NAME-tsdb-config` no longer holds credentials.** It is now created only when `backup.enabled: true`, and holds nothing but the backup destination. Anything outside this release that read the username or password from it by name — another workload's environment, a policy, or a parent chart bundling TimescaleDB as a subchart — must be pointed at the secret you create instead. A reference to a key that no longer exists does not fail loudly; it wedges the referring workload silently.
</Warning>

## Prerequisites

**One secret must exist before you install.** It holds the credentials your applications put in their connection strings. The values never pass through Helm, so they do not land in the release. 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`. TimescaleDB creates that role and that database on first boot, and creates the TimescaleDB extension inside that database:

    ```bash theme={null}
    cpln secret create-dictionary --name my-timescaledb-credentials \
      --entry username=myuser \
      --entry password='YOUR-STRONG-PASSWORD' \
      --entry database=mydb
    ```

    Set `config.credentialsSecretName` to the name you used. Secret names are org-wide, so give each release its own.
  </Step>

  <Step title="Read the secret back later">
    Pass `-o yaml`. A bare `cpln secret reveal` prints only a summary table, not the values:

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

<Warning>
  **Create the secret before installing, or the deployment wedges silently.** The template refuses to render when `config.credentialsSecretName` is blank, but a name pointing at a secret that does not exist installs "successfully" and then never starts. The container never runs, so `cpln logs` returns **zero lines** — there is nothing to log, and every summary surface just looks like a slow deploy. The one place the reason appears is `status.versions[].message`:

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

  ```text theme={null}
  The secret my-timescaledb-credentials 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 and will show you nothing. Creating the missing secret repairs it on its own with no further action, in roughly **5.5 to 10.5 minutes** measured across five templates, or run `cpln workload force-redeployment RELEASE_NAME-timescaledb --gvc GVC_NAME` to clear it in about 90 seconds.
</Warning>

Backups need a bucket, and for AWS or GCP a Control Plane Cloud Account, before they can be enabled — see [Backup Prerequisites](#backup-prerequisites). Nothing else is required.

## Installation

Create the [prerequisite secret](#prerequisites) first, then install by whichever method you prefer:

<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

The default `values.yaml` for this template:

```yaml theme={null}
image: timescale/timescaledb:2.28.3-pg18 # PostgreSQL 18 + TimescaleDB Community edition

resources:
  minCpu: 200m
  minMemory: 512Mi
  maxCpu: 500m
  maxMemory: 1024Mi # timescaledb-tune sizes shared_buffers/workers from this at first boot

config:
  # REQUIRED PREREQUISITE SECRET — CREATE IT BEFORE YOU INSTALL.
  # A `dictionary` secret holding exactly three keys: `username`, `password`
  # and `database`. The TimescaleDB extension is created automatically in the
  # database that secret names. If the secret does not exist at install time
  # the deployment WEDGES silently — `cpln logs` returns nothing at all.
  credentialsSecretName: my-timescaledb-credentials

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

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

publicAccess:
  enabled: false # exposes 5432 via a TCP load balancer; connections are unencrypted — prefer internal access

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

backup:
  enabled: false
  image: ghcr.io/controlplane-com/backup-images/postgres-backup:18.1.0 # PG18 client, matches server major
  schedule: "0 2 * * *"   # daily at 2am UTC

  resources:
    cpu: 100m
    memory: 128Mi

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

  aws:
    bucket: my-backup-bucket
    region: us-east-1
    cloudAccountName: my-backup-cloudaccount
    policyName: my-backup-policy
    prefix: timescaledb/backups # folder name where your backups will be stored

  gcp:
    bucket: my-backup-bucket
    cloudAccountName: my-backup-cloudaccount
    prefix: timescaledb/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-backup-bucket
    accessKey: my-minio-username # matches the MinIO template's admin.username
    secretKey: my-minio-password # matches the MinIO template's admin.password
    prefix: timescaledb/backups # folder name where your backups will be stored
```

### Image and Resources

* `image` — The TimescaleDB image tag. Keep it in the default (Community) series; `-oss` tags remove compression, continuous aggregates, and retention.
* `resources.minCpu` / `resources.minMemory` — Minimum CPU and memory guaranteed to the workload.
* `resources.maxCpu` / `resources.maxMemory` — Maximum CPU and memory the workload can use. `timescaledb-tune` sizes `shared_buffers` and worker settings from `maxMemory` at first boot (for example, `shared_buffers` becomes \~25% of the memory limit).

<Note>
  Auto-tuning is captured only at first boot, when the data volume is empty. Raising `resources.maxMemory` on an existing deployment does not retune PostgreSQL — adjust settings manually with `ALTER SYSTEM`, or uninstall (which deletes the volume set) and reinstall.
</Note>

### Credentials

* `config.credentialsSecretName` — Name of the dictionary secret holding `username`, `password` and `database`. TimescaleDB creates that role and that database on first boot, and creates the TimescaleDB extension inside that database. These are the credentials your applications put in their connection strings.

The secret must exist before installing — see [Prerequisites](#prerequisites). The workload reads it through `cpln://secret/...` references, so the values appear in neither the Helm release nor the stored workload spec.

<Note>
  The credentials are applied only on first startup, when the data directory is empty. Changing the secret afterwards does **not** change the running database — it only changes what the workload presents when it authenticates, which will then fail. To rotate on an existing instance, run `ALTER ROLE ... WITH PASSWORD` inside PostgreSQL first, then update the secret to match.
</Note>

### Storage

* `volumeset.capacity` — Initial volume size in GiB (minimum 10).
* `volumeset.autoscaling.enabled` — Automatically expand the volume as it fills. When enabled:
  * `maxCapacity` — Maximum volume size in GiB.
  * `minFreePercentage` — Trigger a scale-up when free space drops below this percentage.
  * `scalingFactor` — Multiply the current capacity by this factor when scaling up.

### Internal Access

* `internalAccess.type` — Controls which workloads can connect to TimescaleDB on port `5432`:

| Type            | Description                                                     |
| --------------- | --------------------------------------------------------------- |
| `none`          | No internal access allowed                                      |
| `same-gvc`      | Allow access from all workloads in the same GVC                 |
| `same-org`      | Allow access from all workloads in the same organization        |
| `workload-list` | Allow access only from specific workloads listed in `workloads` |

* `internalAccess.workloads` — When `type` is `workload-list`, the list of workload links (e.g. `//gvc/GVC_NAME/workload/WORKLOAD_NAME`) allowed to connect.

### Public Access

* `publicAccess.enabled` — When `true`, exposes port `5432` through a TCP load balancer and assigns a public `*.cpln.app` canonical endpoint.

<Warning>
  Public access is unencrypted — the image ships no TLS certificates, so connections over the public endpoint are plaintext. Keep `publicAccess.enabled: false` and use internal access unless you accept plaintext connections.
</Warning>

### PgBouncer Connection Pooling

PgBouncer is an optional connection pooler that sits in front of TimescaleDB and multiplexes application connections into a smaller pool of real database connections. This reduces connection overhead and protects the database from exhaustion under high concurrency.

When enabled, PgBouncer is deployed as a separate workload and becomes the primary connection endpoint for your applications:

```text theme={null}
RELEASE_NAME-pgbouncer.GVC_NAME.cpln.local:5432
```

* `pgbouncer.enabled` — Enable or disable PgBouncer.
* `pgbouncer.poolMode` — Controls how connections are reused:

| Mode          | Description                                                                                                                                                                                                             |
| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `transaction` | Connection held only for the duration of a transaction, then returned to the pool. Best for most web and API workloads. Not compatible with session-level features (`SET` variables, temporary tables, advisory locks). |
| `session`     | Connection held for the entire client session. Compatible with all PostgreSQL features but provides less reuse.                                                                                                         |
| `statement`   | Connection returned after every statement. Transactions are not supported. Rarely used.                                                                                                                                 |

* `pgbouncer.defaultPoolSize` — Number of real database connections PgBouncer maintains per pool (default: `25`).
* `pgbouncer.maxClientConn` — Maximum number of client connections PgBouncer accepts (default: `1000`).
* `pgbouncer.replicas` — Number of PgBouncer instances. PgBouncer is stateless and can be scaled horizontally for high-throughput workloads.
* `pgbouncer.resources.cpu` / `pgbouncer.resources.memory` — Resources allocated to each PgBouncer replica.

<Note>
  PgBouncer shares the same credentials and identity as the TimescaleDB workload — no additional secrets or IAM configuration is required. Its `userlist.txt` and `pgbouncer.ini` are generated at container start from the same credentials secret, so PgBouncer stays in step with the database automatically.
</Note>

### Backup

Backup is disabled by default. When enabled, a cron workload runs `pg_dumpall` on the configured schedule and uploads a compressed SQL dump to AWS S3, GCS, or a MinIO-compatible endpoint.

* `backup.enabled` — Enable scheduled backups.
* `backup.image` — The backup container image. Match its tag to the server major version — `18.1.0` for the default PostgreSQL 18 image, `17.1.0` for a PostgreSQL 17 image.
* `backup.schedule` — Cron expression for backup frequency (default: daily at 2am UTC).
* `backup.provider` — `aws`, `gcp`, or `minio`.
* `backup.resources.cpu` / `backup.resources.memory` — Resources for the backup cron container.

Complete the [backup prerequisites](#backup-prerequisites) for your provider before enabling backup.

## Connecting

| What                           | Value                                                                                                |
| ------------------------------ | ---------------------------------------------------------------------------------------------------- |
| Internal (same GVC)            | `RELEASE_NAME-timescaledb.GVC_NAME.cpln.local:5432`                                                  |
| Via PgBouncer *(when enabled)* | `RELEASE_NAME-pgbouncer.GVC_NAME.cpln.local:5432` — use this as your application endpoint            |
| Public *(when enabled)*        | The `status.canonicalEndpoint` of the `RELEASE_NAME-timescaledb` workload, port `5432` (unencrypted) |
| Credentials                    | The `username` and `password` entries of the secret named by `config.credentialsSecretName`          |

## Using TimescaleDB

Any PostgreSQL client or ORM works unchanged. Turn a regular table into a hypertable (automatically partitioned by time) and query it with time buckets:

```sql theme={null}
CREATE TABLE metrics (time timestamptz NOT NULL, device text, value double precision);
SELECT create_hypertable('metrics', by_range('time'));

INSERT INTO metrics VALUES (now(), 'sensor-1', 23.5);

SELECT time_bucket('1 hour', time) AS bucket, device, avg(value)
FROM metrics
GROUP BY bucket, device
ORDER BY bucket;
```

From here you can add columnar compression, continuous aggregates, and retention policies — all Community features included in the default image.

## Backup Prerequisites

### AWS S3

Before enabling backup with `provider: aws`, complete the following in your AWS account:

<Steps>
  <Step title="Create a bucket">
    Create an S3 bucket. Set `backup.aws.bucket` to its name and `backup.aws.region` to its region.
  </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 an IAM policy">
    Create an IAM policy with the JSON below, replacing `YOUR_BUCKET_NAME`, then set `backup.aws.policyName` to the policy's name and `backup.aws.prefix` to the folder path for backups.
  </Step>
</Steps>

<Warning>
  **Version 1.1.1 narrows AWS backup permissions.** This version removes `aws::ReadOnlyAccess` from the backup identity. That AWS managed policy granted read access to **every bucket in your AWS account** and contains no write actions at all, so it was never carrying the backup itself — but it *was* silently supplying any read action your own bucket-scoped policy happened to omit.

  **Update your IAM policy to the full action list below before upgrading.** If it already matches, no action is needed. The identity now carries `cpln-connector` and your bucket-scoped policy only, which is strictly narrower than before. Nothing else changes.
</Warning>

```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/*"
            ]
        }
    ]
}
```

### GCS

Before enabling backup with `provider: gcp`, complete the following in your GCP account:

<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, with no stored credentials.
  </Step>
</Steps>

<Warning>
  Grant the **Storage Admin** role (`roles/storage.objectAdmin` scoped to the bucket also works) to the GCP service account created for the Cloud Account. Set `backup.gcp.prefix` to the folder path for backups.
</Warning>

### MinIO

Before enabling backup with `provider: minio`, ensure your MinIO instance (or any S3-compatible endpoint) is accessible:

<Steps>
  <Step title="Create a bucket">
    Create a 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 the port. For the `minio` marketplace template deployed in the same GVC, use `http://WORKLOAD_NAME:9000`.
  </Step>

  <Step title="Set credentials">
    Set `backup.minio.accessKey` and `backup.minio.secretKey` to credentials with access to the bucket. For the MinIO template, these match the `admin.username` and `admin.password` values. Set `backup.minio.prefix` to the folder path for backups.
  </Step>
</Steps>

<Note>
  MinIO backup requires no Control Plane Cloud Account — credentials are passed directly to the backup job.
</Note>

## Restoring a Backup

Restoring a TimescaleDB dump is **not** the vanilla PostgreSQL procedure. The target server must run the **same TimescaleDB extension version** as the dump, and the replay must be wrapped in `timescaledb_pre_restore()` and `timescaledb_post_restore()`. Run the following from a client with access to the backup bucket and to a fresh database:

<Warning>
  Restore replays a full-cluster dump. Run it against a fresh instance, not a database that already holds data you want to keep.
</Warning>

**AWS S3:**

```sh theme={null}
export PGPASSWORD="PASSWORD"

psql -h RELEASE_NAME-timescaledb.GVC_NAME.cpln.local -U USERNAME -d DATABASE \
  -c "SELECT timescaledb_pre_restore();"

aws s3 cp "s3://BUCKET_NAME/PREFIX/BACKUP_FILE.sql.gz" - \
  | gunzip \
  | psql -h RELEASE_NAME-timescaledb.GVC_NAME.cpln.local -p 5432 -U USERNAME -d postgres

psql -h RELEASE_NAME-timescaledb.GVC_NAME.cpln.local -U USERNAME -d DATABASE \
  -c "SELECT timescaledb_post_restore();"

unset PGPASSWORD
```

For **GCS**, replace the download with `gsutil cp "gs://BUCKET_NAME/PREFIX/BACKUP_FILE.sql.gz" -`. For **MinIO**, add `--endpoint-url "http://MINIO_ENDPOINT:9000"` to the `aws s3 cp` command and export the MinIO access and secret keys as `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`.

## Important Notes

* **Create the credentials secret before installing.** The deployment wedges silently without it, and `cpln logs` returns zero lines — see [Prerequisites](#prerequisites) for the one command that names the missing secret.
* **Do not scale this workload.** It is single-writer PostgreSQL, pinned to one replica. Additional replicas would run as isolated instances, not a cluster.
* **Auto-tuning is captured at first boot only.** `timescaledb-tune` sizes memory settings from the container limit when the volume is empty; raising `resources.maxMemory` later does not retune.
* **Public access is unencrypted.** The image ships no TLS certificates — keep `publicAccess.enabled: false` unless you accept plaintext connections.
* **Keep the image tag in the default (Community) series.** `-oss` tags remove compression, continuous aggregates, and retention policies. If you pin a different `pgXX` server image, match `backup.image` to the same major (`17.1.0` for PostgreSQL 17).
* **Uninstall deletes the volume set.** A final snapshot is kept for 7 days; enable backups if the data matters long-term.

## External References

<CardGroup cols={2}>
  <Card title="TimescaleDB Documentation" icon="database" href="https://docs.timescale.com/">
    Official TimescaleDB documentation
  </Card>

  <Card title="Hypertables" icon="table" href="https://docs.timescale.com/use-timescale/latest/hypertables/">
    Hypertables, compression, continuous aggregates, and retention
  </Card>

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

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