Overview
PostgreSQL is a powerful open-source relational database. This template deploys a single-replica PostgreSQL instance with persistent storage, an optional PgBouncer connection pooler, and optional scheduled backups to AWS S3, GCS, or a self-hosted MinIO instance. Database credentials are not template values. PostgreSQL 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.PostgreSQL on Control Plane operates as a single-replica deployment. Do not scale up the replica count, as this would result in multiple isolated instances rather than a replicated cluster. For a highly available setup, use the PostgreSQL Highly Available template instead, or Postgres Multi Location to span regions.
What Gets Created
- Stateful Postgres Workload — (
RELEASE_NAME-postgres): a single-replica PostgreSQL container serving TCP on port5432, with configurable resources. - Volume Set — (
RELEASE_NAME-pg-vs): anext4volume holdingPGDATA, on general-purpose SSD with daily snapshots and 7-day retention, with optional autoscaling. - Identity & Policy — (
RELEASE_NAME-pg-identity,RELEASE_NAME-pg-policy): an identity bound to the database, pooler and backup workloads, and a policy granting itrevealon exactly the credential secrets you created — nothing else. When backups are enabled, the identity also carries the Cloud Account binding the backup job uses to reach your bucket. - PgBouncer Workload (optional) — (
RELEASE_NAME-pgbouncer): a connection pooler deployed as a separate workload in front of PostgreSQL, created whenpgbouncer.enabled: true. - Backup Cron Workload (optional) — (
RELEASE_NAME-postgres-backup): a scheduledpg_dumpthat writes compressed SQL dumps to AWS S3, GCS or MinIO, created whenbackup.enabled: true.
This template does not create a GVC. You must deploy it into an existing GVC.
Upgrading From 3.3.0 or Earlier
Template versions up to 3.3.0 took the database credentials as plain Helm values and shipped a working default password for them, and wrote those credentials into a chart-owned secret named after the release. Version 3.4.0 removes both.1
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:The keys are
username, password and database.2
Create the prerequisite secret with those same values
Follow Prerequisites, using the existing username, password and database name. Using different values here does not change the database — it just leaves the workload unable to authenticate.
3
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.4
Upgrade, then rotate the 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. Rotate inside PostgreSQL and update the secret to match:Use your own username in place of
appuser, then update the secret so the workload can still authenticate after a restart.aws::ReadOnlyAccess was removed from the backup identity, and no action is required from you. Control Plane attaches each policy reference as a managed policy on a per-identity derived IAM role, so the identity’s permissions are the union of those references. ReadOnlyAccess contains no write actions at all — its S3 portion is Get* and List* on * — so it was never carrying the backup upload; what it granted was read access to every bucket in the account. Backups continue to work without it. Existing bucket policies are also unaffected: a 30.9 MB multipart upload succeeded under the six-action policy documented by earlier template versions, because a multipart upload authorizes under s3:PutObject. The four additional actions in the AWS S3 policy below are defensive — they let a failed upload abort its own parts instead of leaving them to bill silently — not required.backup.schedule now accepts every cron form. In 3.3.0 and earlier, any schedule beginning with * — such as */15 * * * * — failed to render with a raw YAML parse error naming a template file, because an unquoted YAML scalar starting with * is an alias indicator. If you worked around this with a digit-leading schedule, you can now use the interval form directly.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.1
Create the database credentials secret
A dictionary secret holding exactly three keys — Set
username, password and database. PostgreSQL creates that user and that database on first boot:config.credentialsSecretName to the name you used.2
Read the secret back later
Pass
-o yaml. A bare cpln secret reveal prints only a summary table, not the values:accessKey and secretKey. Nothing else is required.
Installation
To install, follow the instructions for your preferred method:UI
Browse, install, and manage templates visually
CLI
Manage templates from your terminal
Terraform
Declare templates in your Terraform configurations
Pulumi
Declare templates in your Pulumi programs
Configuration
The defaultvalues.yaml for this template:
Credentials
config.credentialsSecretName— Name of the dictionary secret holdingusername,passwordanddatabase. PostgreSQL creates that user and that database on first boot, and this is the credential your applications put in their connection strings.
cpln://secret/... references, so the values appear in neither the Helm release nor the stored workload spec.
These credentials are only applied on first startup when the data directory is empty. Rotating the secret afterwards does not change the stored password; change it inside PostgreSQL with
ALTER ROLE ... WITH PASSWORD (and ALTER DATABASE ... RENAME for the database name) and update the secret to match.Resources
resources.minCpu/resources.minMemory— Minimum CPU and memory guaranteed to the workload.resources.maxCpu/resources.maxMemory— Maximum CPU and memory the workload can use.
On a stateful workload the ratio of
maxCpu to minCpu may not exceed 4:1. The shipped 500m / 200m is 2.5:1; raising maxCpu without raising minCpu can cross the limit and is rejected when the workload is applied.Storage
volumeset.capacity— Initial volume size in GiB (minimum 10).volumeset.autoscaling.enabled— Allow the volume to grow 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 PostgreSQL on port5432:
Firewall changes take 30 to 150 seconds to propagate. After changing
internalAccess, re-test rather than trusting the first response.
PgBouncer Connection Pooling
PgBouncer is an optional connection pooler that sits in front of PostgreSQL and multiplexes application connections into a smaller pool of real database connections. This reduces connection overhead and protects PostgreSQL from exhaustion under high concurrency. When enabled, PgBouncer is deployed as a separate workload and becomes the primary connection endpoint for your applications:pgbouncer.enabled— Enable or disable PgBouncer.pgbouncer.image— PgBouncer container image.pgbouncer.poolMode— Controls how connections are reused:
pgbouncer.defaultPoolSize— Number of real PostgreSQL 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.
PgBouncer reads the same credentials secret and identity as the PostgreSQL workload — no additional secrets or IAM configuration is required. Its
userlist.txt and pgbouncer.ini are generated at startup from the username, password and database keys in that secret.PgBouncer’s own admin console (
-d pgbouncer) rejects the database user with FATAL: not allowed. The image restricts admin access to the postgres user; this is upstream behavior, not a template setting.Backup
Backup is disabled by default. When enabled, a cron workload runspg_dump on the configured schedule and uploads compressed SQL dumps to AWS S3, GCS or MinIO. The job authenticates with the username and password from your credentials secret and dumps the database named in it.
Backup requires PostgreSQL 17 or later. Set
backup.image to match your PostgreSQL version: ghcr.io/controlplane-com/backup-images/postgres-backup:18.1.0 for Postgres 18, or ghcr.io/controlplane-com/backup-images/postgres-backup:17.1.0 for Postgres 17.backup.enabled— Enable scheduled backups.backup.image— Backup container image, matched to your PostgreSQL major version.backup.schedule— Cron expression for backup frequency (default: daily at 2am UTC). Interval forms such as*/15 * * * *are accepted.backup.provider—aws,gcp, orminio.backup.resources.cpu/backup.resources.memory— Resources for the backup cron container.backup.PROVIDER.prefix— Folder path within the bucket where backups are stored.
Backup Prerequisites
Only needed whenbackup.enabled: true.
AWS S3
-
Create an S3 bucket. Set
backup.aws.bucketto its name andbackup.aws.regionto its region. -
If you do not have a Control Plane Cloud Account set up, follow the Create a Cloud Account guide. Set
backup.aws.cloudAccountNameto its name. -
Create an IAM policy with the following JSON, replacing
YOUR_BUCKET_NAME:
-
Set
backup.aws.policyNameto the name of the policy created in step 3. The template attaches it to the workload’s identity, and attaches nothing else — the bucket in your policy is the only storage the backup job can reach. -
Set
backup.aws.prefixto the folder path where backups will be stored.
GCS
-
Create a GCS bucket. Set
backup.gcp.bucketto its name. -
If you do not have a Control Plane Cloud Account set up, follow the Create a Cloud Account guide. Set
backup.gcp.cloudAccountNameto its name. -
Add the Storage Admin role to the GCP service account associated with the Cloud Account. The template additionally binds the identity to
roles/storage.objectAdminon exactly the bucket inbackup.gcp.bucket. -
Set
backup.gcp.prefixto the folder path where backups will be stored.
MinIO
No Cloud Account is needed — credentials are supplied as a secret.-
Create a bucket in MinIO. Set
backup.minio.bucketto its name. -
Set
backup.minio.endpointto the MinIO S3 API address including the port. For the minio template deployed in the same GVC, usehttp://WORKLOAD_NAME:9000. -
Create a dictionary secret holding exactly the keys
accessKeyandsecretKey, and setbackup.minio.credentialsSecretNameto its name. For theminiotemplate these are itsadmin.usernameandadmin.password:
- Set
backup.minio.prefixto the folder path where backups will be stored.
The policy this template creates grants the workload
reveal on both the database credentials secret and the MinIO credentials secret, and on nothing else.Restoring a Backup
Run the following from a client with access to the backup bucket, using the username and password from your credentials secret. AWS S3:Connecting
This template exposes no public endpoint. Everything below is reachable from inside the GVC only.Important Notes
- Create the credentials secret before installing. A reference to a secret that does not exist wedges the workload with no log output at all; see Prerequisites for the one command that shows the reason.
- Credentials are read only when the volume is first initialized. Rotating the secret afterwards does not change the stored password — use
ALTER ROLEinside PostgreSQL and update the secret to match. - Do not scale past one replica. This is a single instance on a single volume, not a replicated cluster.
- Data lives on the volume set and survives redeploys;
cpln helm uninstalldeletes it, taking the database with it. Your credentials secret is yours and is left alone. - A
cpln helm upgraderestarts the server. Nothing serializes the rollout on a stateful workload, so treat every upgrade as a short planned write outage. The first upgrade after an install re-applies resources even when the values are byte-identical. - 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 3.3.0 or Earlier.
External References
PostgreSQL Documentation
Official PostgreSQL documentation
PostgreSQL Backup and Restore
Upstream guidance on logical dumps and restores
PgBouncer Documentation
PgBouncer configuration reference
Cloud Accounts
Create a Control Plane Cloud Account for backup storage access
Backup Image Source
Source code for the PostgreSQL backup container image
PostgreSQL Template
View the source files, default values, and chart definition