## What this PR does Fixes #3322. An in-place Postgres (CNPG) restore decided whether to skip deleting the live `Cluster` by comparing its `creationTimestamp` with the RestoreJob's `StartedAt`. The timestamp says the Cluster is new, not which restore rendered it, so two cases let a restore skip its purge and report `Succeeded` on data from another restore: 1. Two RestoreJobs into the same application at once. The second sees the Cluster the first produced, created after its own `StartedAt`, and skips its purge. 2. The live Cluster is deleted while the restore waits for the WAL archive. The still-active HelmRelease re-renders it from the previous restore's values, again created after `StartedAt`. ### Refuse a restore into an application another restore holds The RestoreJob controller now refuses, for every driver, a RestoreJob that has not started yet when another unfinished RestoreJob in the namespace writes into the same application and goes first. The refused job fails with reason `ConcurrentRestore`, and its message names the job that goes first. It fails instead of waiting, so the application is never restored a second time later, after the first restore, by a job its author may no longer expect to run. A started restore goes before one that has not started. Between two that have not, the older goes first, by creation time and then by name; creation times have one-second precision, so two jobs created in the same second are ordered by name. A running restore is never failed by a newer one. The target application is resolved by `restoreJobTarget`, the same helper the BackupJob hold uses, so a Velero restore into another namespace does not collide with an in-place one. The gate lists RestoreJobs through the manager's API reader, not the informer cache. A job reconciled before the cache holds its peer's `StartedAt` would otherwise see that peer as not started and, on a same-second name tie, proceed alongside it. The controller runs a single worker, so a peer's `StartedAt` written by an earlier reconcile is already on the API server when the next job reads it; `SetupWithManager` now sets `MaxConcurrentReconciles: 1` explicitly, since the guarantee depends on it. ### Skip the CNPG purge only for this restore's Cluster The restore patch already sets `bootstrap.newServerName` to `restoredServerName(cluster, RestoreJob UID)`, a value unique to each RestoreJob. The postgres chart now annotates the `Cluster` with it as `postgres.cozystack.io/restored-server-name`. The annotation is rendered even when the barman plugin is not; the plugin's `serverName` also carries the value, but the plugin renders only with chart-level S3 coordinates. The driver skips the purge only when the annotation names this RestoreJob. A Cluster produced by a concurrent restore, or re-rendered from an earlier restore's values, carries another value and is purged. The mark lives on the rendered Cluster rather than in RestoreJob status. That keeps it valid in the case the skip exists for: the purge succeeded, but the `TargetPurged` status write failed. A UID recorded in status was rejected in the code for exactly that reason. A Cluster rendered by a chart that predates the annotation has none and keeps the creation-time check, so a restore in flight across an upgrade behaves as before. ### Tests - `TestRestoreAhead` covers order, a started job going first, the name tie-break, finished and terminating jobs, other applications, retargeted restores and a Velero restore into another namespace. - `TestReconcileRestore_ConcurrentRestoreIsRefused` checks that the newer job fails with `ConcurrentRestore` and the running one is left alone. - `TestReconcileRestore_ConcurrentRestoreReadsPastTheCache` gives the cache a stale, not-started peer and the API reader the started one, and requires the job to fail with `ConcurrentRestore`. - `TestCNPGClusterFreshlyRecovered` adds both cases from the issue: a Cluster marked by another restore and created after `StartedAt` must be purged. The timestamp-only cases are kept for the unmarked fallback. - `TestReconcileCNPGRestore_RepeatInPlacePurgesStaleRecoveryCluster` gains two reconcile-level cases. One purges a Cluster that another restore marked, even though it was created after `StartedAt`. The other runs a full purge, takes the mark from the `bootstrap.newServerName` the restore patch wrote, drops `TargetPurged` as a lost status write would, and requires a Cluster carrying that mark to survive, although it predates `StartedAt`. That pins the purge decision to the value the chart actually renders. - `packages/apps/postgres/tests/restore_mark_test.yaml` checks that the annotation is rendered with and without the barman plugin, and is absent on a Cluster that was never restored. Each new test was checked against the code it guards: disabling the gate call, listing through the cache, flipping the started-job precedence, ignoring the annotation, or deriving the expected mark from the app name instead of the cluster name each turns one red. `go test ./internal/backupcontroller/...` and `helm unittest packages/apps/postgres` (152 tests) pass. The restore flow was not run against a live cluster. `docs/operations/backup-classes.md` documents the one-restore-at-a-time rule and the annotation. ### Screenshots N/A — no UI changes. ### Downstream repositories Walked the trigger map in `docs/agents/contributing.md` against the diff: Go changes in `internal/backupcontroller` (no CRD or API type change), one annotation added to the postgres chart's Cluster (no `values.yaml` or `values.schema.json` change), and a docs paragraph. No package is added or renamed, no platform values, ApplicationDefinition, RestoreJob CRD, metric, label or namespace changes, so no trigger is reached. - [x] No downstream repository is affected by this change - [ ] [cozystack/website](https://github.com/cozystack/website) - follow-up: - [ ] [cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack) - follow-up: - [ ] [cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack) - follow-up: - [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up: - [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up: - [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) - follow-up: - [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) - follow-up: - [ ] [cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server) - follow-up: - [ ] [cozystack/external-apps-example](https://github.com/cozystack/external-apps-example) - follow-up: - [ ] [cozystack/examples](https://github.com/cozystack/examples) - follow-up: - [ ] [cozystack/community](https://github.com/cozystack/community) - follow-up: ### Release note ```release-note fix(backups): a RestoreJob now fails with reason ConcurrentRestore when another unfinished RestoreJob is already restoring the same application, instead of running alongside it; create it again once the first restore has finished. An in-place Postgres restore now skips deleting the live Cluster only when that Cluster was rendered for this RestoreJob, identified by the new postgres.cozystack.io/restored-server-name annotation, so it no longer reports Succeeded on data from a concurrent or earlier restore. ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Restores targeting the same application are now handled one at a time. A competing restore is refused rather than queued, with started restores taking priority. * PostgreSQL now calculates a default `shared_buffers` value when the memory limit is below 512 MiB, while respecting explicitly configured values. * **Bug Fixes** * Recovery clusters are more reliably distinguished from manual recoveries and clusters created by other restores, reducing the risk of purging the wrong cluster. * **Documentation** * Clarified restore ordering and how recovery-cluster cleanup is determined. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
Managed PostgreSQL Service
PostgreSQL is currently the leading choice among relational databases, known for its robust features and performance. The Managed PostgreSQL Service takes advantage of platform-side implementation to provide a self-healing replicated cluster. This cluster is efficiently managed using the highly acclaimed CloudNativePG operator, which has gained popularity within the community.
Deployment Details
This managed service is controlled by the CloudNativePG operator, ensuring efficient management and seamless operation.
Operations
PostgreSQL backups have two layers, and the recommended setup uses both together rather than picking one:
| Layer | What it does | Configured via |
|---|---|---|
| Archive plumbing (chart, legacy) | Renders spec.plugins (referencing a barman-cloud ObjectStore) on the cnpg.io Cluster from helm-install onwards. CNPG runs the barman-cloud plugin as a WAL-archiver sidecar, every WAL switch ships to object storage, and any backup driver gets a complete WAL chain to start replay from. Renders only when backup.enabled=true, useSystemBucket=false, destinationPath is non-empty, AND inline-or-external creds are supplied. Skipped in the platform useSystemBucket=true mode — the CNPG backup driver SSA-applies the ObjectStore and patches spec.plugins onto the live Cluster at first BackupJob time; until that first BackupJob fires, plugin WAL archiving is not active and WAL accumulates on the PVC. Fire an ad-hoc BackupJob immediately after enabling the flag on existing releases. |
Legacy: backup.enabled=true plus backup.destinationPath, backup.endpointURL, and either backup.s3AccessKey+backup.s3SecretKey or backup.s3CredentialsSecret. Platform: backup.enabled=true plus backup.useSystemBucket=true — no S3 fields. |
| Backup orchestration (recommended) | Drives ad-hoc and scheduled backups, retention, and restores from backups.cozystack.io resources that can span multiple Postgres apps in a tenant. The driver SSA-applies its own barman-cloud ObjectStore and patches spec.plugins with ForceOwnership (so compression / target settings flow from the strategy template, while the live Cluster's serverName — its WAL-archive S3 prefix — is always preserved); chart-managed values cover the same destination so there is no tug-of-war on the live Cluster. |
strategy.backups.cozystack.io/CNPG + BackupClass + Plan (recurring) or BackupJob (ad-hoc), restores via RestoreJob |
| Legacy chart-emitted scheduled backup | The chart can also emit a cnpg.io/ScheduledBackup directly. Superseded by the BackupClass + Plan path, kept around for clusters that did not migrate. Off by default - rendered only when both backup.enabled and backup.schedule are non-empty. With backup.schedule="" plugin WAL archiving still runs; only the chart-emitted scheduled backup is silent. |
backup.enabled=true plus backup.schedule (CNPG 6-field cron, e.g. "0 2 * * * *") |
The canonical setup depends on whether the cluster ships the platform cozy-default BackupClass.
Platform-managed flow (recommended for new clusters) — opt in via backup.useSystemBucket: true and reference cozy-default from BackupJob/Plan/RestoreJob. The chart leaves S3 coordinates blank; the CNPG driver SSA-applies a barman-cloud ObjectStore and patches spec.plugins from the platform-managed bucket coordinates at first BackupJob time.
spec:
backup:
enabled: true
useSystemBucket: true # platform projects cozy-backups-creds + driver SSA-applies ObjectStore + patches spec.plugins
Legacy chart-managed flow — for clusters that pre-date the platform BackupClass or use a tuned non-default bucket. Provide all S3 coordinates inline (or via s3CredentialsSecret.name); spec.plugins and the barman-cloud ObjectStore render from helm-install onwards.
spec:
backup:
enabled: true # archive WAL to object storage
destinationPath: s3://my-bucket/pg-src/
endpointURL: https://seaweedfs-s3.tenant-foo:8333
s3CredentialsSecret:
name: pg-src-cnpg-backup-creds
endpointCA:
name: pg-src-cnpg-backup-ca
# backup.schedule intentionally left empty - the BackupClass / Plan
# below drives recurring backups, no chart-emitted ScheduledBackup
# should compete with that schedule.
paired with a strategy.backups.cozystack.io/CNPG + BackupClass + Plan
in the same tenant. The end-to-end e2e fixture under
examples/backups/postgres/ is the
canonical reference (05-postgres-src.yaml shows the chart side,
10-cnpg-strategy.yaml and 15-backupclass.yaml show the orchestration
side, 25-backupjob-adhoc.yaml and 40-restorejob-to-copy.yaml show
ad-hoc backup and restore).
Why both layers and not one? WAL archiving is handled by the barman-cloud plugin's WAL-archiver sidecar - CNPG can attach or detach the plugin at runtime, but any WAL that closed before the plugin began archiving is gone for good. A backup taken from such a cluster is missing the WAL its
begin_walpoints at, and recovery later fails withWAL not found. Letting the chart wire the plugin from helm-install removes the race.
How to enable backups (preferred: BackupClass + Plan)
End-to-end manifests live under examples/backups/postgres/.
Briefly, the moving parts are:
- A
strategy.backups.cozystack.io/CNPGdescribing the destination bucket and templating the barman-cloudObjectStore(including a Secret reference to S3 credentials - the credentials never appear on the Postgres CR.spec; see Security note below). - A
backups.cozystack.io/BackupClassthat names the strategy and is selected by anapplicationRefmatching the Postgres app'sKind/Name. - A
backups.cozystack.io/Plan(recurring) orBackupJob(ad-hoc) that references the BackupClass. The controller materialises aBackupartifact when the cnpg.io Backup completes; restores then reference that Backup viaRestoreJob.
Both in-place restores (overwrite the source app's data) and to-copy
restores (restore into a separate target Postgres app in the same
namespace) are supported via the RestoreJob.spec.targetApplicationRef
field.
Security: With the BackupClass path, S3 credentials live in a tenant-readable Secret referenced from the strategy template. The CNPG driver forwards that Secret reference into the Postgres app's
spec.backup.s3CredentialsSecreton restore, so access keys never land in the Postgres CR.spec, etcd object store, orkubectl get -o yamloutput. Prefer this over the chart-managed path whenever possible.
How to enable chart-managed scheduled backups (legacy)
The chart can also emit a cnpg.io/ScheduledBackup directly, without a
BackupClass. Superseded by the BackupClass + Plan path above and kept
around for clusters that did not migrate. It does not run by default -
backup.schedule defaults to an empty string, which gates the chart's
ScheduledBackup template off. To turn it on, fill in a CNPG 6-field cron
expression:
## @param backup.enabled Enable plugin WAL archiving + render the chart-managed ScheduledBackup
## @param backup.schedule Cron schedule (CNPG 6-field). Empty means no chart-managed ScheduledBackup
## @param backup.retentionPolicy Retention policy
## @param backup.destinationPath Path to store the backup (i.e. s3://bucket/path/to/folder)
## @param backup.endpointURL S3 Endpoint used to upload data to the cloud
## @param backup.s3AccessKey Access key for S3, used for authentication
## @param backup.s3SecretKey Secret key for S3, used for authentication
backup:
enabled: true
retentionPolicy: 30d
destinationPath: s3://bucket/path/to/folder/
endpointURL: http://minio-gateway-service:9000
schedule: "0 2 * * * *" # opt in - empty (the default) means no chart-managed schedule
s3AccessKey: oobaiRus9pah8PhohL1ThaeTa4UVa7gu
s3SecretKey: ju3eum4dekeich9ahM1te8waeGai0oog
How to recover a backup (preferred: RestoreJob)
For BackupClass-managed backups, create a backups.cozystack.io/RestoreJob
that references the desired Backup. See
examples/backups/postgres/35-restorejob-in-place.yaml
and
examples/backups/postgres/40-restorejob-to-copy.yaml.
On a to-copy restore the controller replaces the target app's
spec.databases and spec.users with the source-spec snapshot persisted
in Backup.status.underlyingResources, so the chart's post-install
init-job does not drop the recovered roles or databases.
e2e coverage: the same-namespace to-copy restore (Steps 0-7) leaves the source running and is the deterministic end-to-end signal; it stays a manual / dev-cluster reference flow, not part of the automated e2e suite. The cross-tenant variant (target Postgres in a different tenant from the source's seaweedfs) stays a manual / dev-cluster exercise — reachability is blocked by the per-tenant Cilium egress policy. The in-place restore code path is shipped and is covered at the unit level by
TestClusterHasRecoveryBootstrap_TerminatingCluster,TestCNPGBackupWALArchived,TestCNPGPurgeNeeded, and the rest of theinternal/backupcontroller/cnpgstrategy_controller_test.gosuite.
How to recover a backup (chart-managed bootstrap)
CloudNativePG supports point-in-time-recovery. Recovering a backup is done by creating a new database instance and restoring the data in it.
Create a new PostgreSQL application with a different name, but identical configuration.
Set bootstrap.enabled to true and fill in the name of the database instance to recover from and the recovery time:
## @param bootstrap.enabled Restore database cluster from a backup
## @param bootstrap.recoveryTime Timestamp (PITR) up to which recovery will proceed, expressed in RFC 3339 format. If left empty, will restore latest
## @param bootstrap.oldName Name of database cluster before deleting
##
bootstrap:
enabled: false
recoveryTime: "" # leave empty for latest or exact timestamp; example: 2020-11-26 15:22:00.00000+00
oldName: "<previous-postgres-instance>"
How to switch primary/secondary replica
See:
storageClassis annotated as immutable in the chart schema — seedocs/storage-immutability.mdfor the contract and which consumers enforce it.
TLS for server connections
CNPG manages the cert chain end-to-end. The operator auto-generates a self-signed CA, signs server, client, and replication leaf certs from it, and rotates them as needed. The chart does not render any cert-manager Issuer/Certificate objects — that path is mutually exclusive with the operator-managed chain on the CNPG admission webhook.
What the chart contributes: when TLS is on and external: true, the chart sets spec.certificates.serverAltDNSNames on the CNPG Cluster CR to inject the external hostnames <release>.<_namespace.host> and <release>.postgres.<_namespace.host> into the auto-generated server certificate's SAN list. The second is the per-engine name meant for SNI routing through a TLS-passthrough listener on the tenant Gateway. The certificate covers that name for any client, but routing through such a listener works only when the TLS handshake is the first thing on the wire. With the default negotiation a PostgreSQL client first sends a plaintext SSLRequest, so the listener can route only clients using direct TLS negotiation: PostgreSQL 17 or later on the server and a client connecting with sslnegotiation=direct, such as libpq 17 or pgx 5.7.5. CNPG's default SAN coverage already includes the three built-in ClusterIP services (-rw, -r, -ro) across the four DNS forms (<svc>, <svc>.<ns>, <svc>.<ns>.svc, <svc>.<ns>.svc.<cluster-domain>); only the external hostnames need to be added.
The tri-state tls.enabled controls whether the chart injects serverAltDNSNames:
tls.enabled: null(the default) — TLS posture inherits fromexternal. Whenexternal: true, the chart injects the external hostnames into the operator-managed cert.tls.enabled: truewithexternal: true— same effect as the default.tls.enabled: truewithexternal: false— noserverAltDNSNamesinjection is needed (there is no external hostname to add); CNPG's auto-generated cert covers internal services.tls.enabled: false— the chart skipsserverAltDNSNamesinjection. Note: CNPG keeps its built-in TLS on the wire regardless of this flag; this toggle only controls whether the external hostnames are added to the cert. To force PostgreSQL to drop TLS entirely you would need to setpostgresql.parameters.ssl = "off"at the CNPG layer, which is out of scope for this flag.
Refusing plaintext sessions. CNPG offers TLS but its built-in last pg_hba rule, host all all all, also accepts plaintext, so a client using sslmode=disable gets an unencrypted session. The tri-state tls.required closes that:
tls.required: null(the default) — followsexternal. Withexternal: truethe chart rendersspec.postgresql.pg_hbaashostssl all all all scram-sha-256followed byhost all all all reject. CNPG puts these after its fixed rules and before its default rule, and the first matching rule wins, so only TLS sessions are authenticated.tls.required: true— the same rules regardless ofexternal.tls.required: false— nopg_hbarules are rendered and CNPG's default applies, which accepts plaintext.
The rules cover every client, in-cluster ones included, because the pod network cannot be told apart from an external client. libpq's default sslmode=prefer already negotiates TLS, so only clients that disable TLS explicitly are refused. Instances that were external before this setting existed refuse such clients after the upgrade; set tls.required: false to keep accepting them. See the CloudNativePG pg_hba documentation.
Retrieving the CA bundle for client verification:
The trust anchor is published as postgres-<name>.tenant-ca, where <name> is the name of the Postgres resource: an object holding ca.crt and nothing else, created for every release and delivered to tenants through the core.cozystack.io/tenantsecrets API that the base tenant roles already grant. The postgres- prefix comes from the release naming this application definition applies, so a Postgres named foo gets postgres-foo.tenant-ca.
kubectl --context <ctx> --namespace <tenant> \
get tenantsecret postgres-<name>.tenant-ca \
--output jsonpath='{.data.ca\.crt}' | base64 --decode
That object is the only one that hands over the CA certificate without also handing over a private key, which is why it exists. CNPG creates its own <release>-ca Secret, but that one stores the CA private key (ca.key) alongside the certificate — read access to it would let the holder issue certificates for anything, so it is never granted to a tenant.
It is reached through tenantsecrets rather than by reading the Secret directly, and that is deliberate: tenantsecrets surfaces only objects the platform has vouched for (those the application's definition selects), whereas a direct grant on the name would convey whatever happens to occupy that name.
Connecting with full verification (psql example):
psql "host=<host> port=5432 dbname=app user=app \
sslmode=verify-full sslrootcert=ca.crt"
For sslmode=verify-full to work, the CA bundle retrieved above must be saved to ca.crt. Without it, use sslmode=require (encrypts but does not verify the server certificate).
Parameters
Common parameters
| Name | Description | Type | Value |
|---|---|---|---|
replicas |
Number of Postgres replicas. Under active autoscaling this is the resting/seed count, not the live count — KEDA drives the live count via /scale. Disabling autoscaling does NOT automatically bring the live count back to replicas: the client-side merge writes instances only when the rendered value changes, so a cluster KEDA grew past replicas stays there with no autoscaler left to reduce it — lower replicas to the count you want and it rebases down (shedding the surplus replicas + PVCs). Lowering it below the live count, or disabling/dry-running while it sits below the live count, sheds in one step past the scaleDown pacing; stage it to the live count first (suspend the HelmRelease and kubectl scale if needed) to avoid a transient. Must stay greater than quorum.maxSyncReplicas. |
int |
2 |
resources |
Explicit CPU and memory configuration for each PostgreSQL replica. When omitted, the preset defined in resourcesPreset is applied. |
object |
{} |
resources.cpu |
CPU available to each replica. | quantity |
"" |
resources.memory |
Memory (RAM) available to each replica. | quantity |
"" |
resourcesPreset |
Default sizing preset used when resources is omitted. |
string |
t1.micro |
size |
Persistent Volume Claim size available for application data. | quantity |
10Gi |
storageClass |
StorageClass used to store the data. | string |
"" |
external |
Enable external access from outside the cluster. | bool |
false |
version |
PostgreSQL major version to deploy | string |
v18 |
flavor |
PostgreSQL image flavor, fixed when the cluster is created. postgresql uses the CloudNativePG PostgreSQL image. postgis uses the CloudNativePG PostGIS image, which adds postgis, postgis_raster, postgis_topology, postgis_sfcgal, postgis_tiger_geocoder and address_standardizer; enable them per database in databases.<name>.extensions. Changing the flavor of an existing cluster is refused, and a backup restores only into a cluster of the flavor it was taken from: to change flavor, create a new cluster and move the data with a logical dump (pg_dump / pg_restore). |
string |
postgresql |
TLS configuration
| Name | Description | Type | Value |
|---|---|---|---|
tls |
TLS configuration for server connections. | object |
{} |
tls.enabled |
Tri-state switch controlling whether the chart injects the external hostnames into the operator-managed CNPG cert via spec.certificates.serverAltDNSNames. When omitted, the chart injects them if external: true and skips them otherwise. Set explicitly to true to inject regardless of external (no-op when external: false since there is no external hostname to add). Set to false to skip injection. Note that CNPG keeps its built-in TLS on the wire regardless of this flag — this toggle only controls the chart-side SAN injection; to disable PostgreSQL TLS entirely set postgresql.parameters.ssl = "off" at the CNPG layer. |
*bool |
null |
tls.required |
Tri-state switch controlling whether the server refuses sessions that are not encrypted. When omitted, it is on if external: true and off otherwise. When on, the chart renders spec.postgresql.pg_hba with hostssl all all all scram-sha-256 (md5 on PostgreSQL 13, matching CloudNativePG's default rule) followed by host all all all reject, so a client that connects with sslmode=disable is refused instead of getting a plaintext session; in-cluster clients are held to the same rule, since the pod network cannot be told apart from an external client. Set to false to keep accepting plaintext sessions on an external instance, or to true to require TLS on an in-cluster one. Rendering fails if postgresql.parameters.ssl is off while this is in effect. |
*bool |
null |
Application-specific parameters
| Name | Description | Type | Value |
|---|---|---|---|
postgresql |
PostgreSQL server configuration. | object |
{} |
postgresql.sharedPreloadLibraries |
Libraries loaded at server start, restricted to an allowlist of modules already shipped in the PostgreSQL image. CloudNativePG also preloads pg_stat_statements, auto_explain and pgaudit by itself whenever parameters sets a key under their prefix (e.g. pg_stat_statements.track), so removing one of them from this list does not unload it while such a key is set; pg_prewarm is preloaded only through this list. Changing the list restarts PostgreSQL on every instance, the primary included, so writes pause briefly. |
[]string |
[] |
postgresql.parameters |
PostgreSQL server parameters. Values may be strings or integers; integers are coerced to strings by the template (e.g. both max_connections: 100 and max_connections: "100" are accepted). BLOCKED (enable arbitrary code execution): archive_command, restore_command, ssl_passphrase_command, archive_cleanup_command, recovery_end_command, dynamic_library_path, local_preload_libraries, session_preload_libraries, shared_preload_libraries. Do NOT override CloudNativePG-managed parameters: archive_mode, primary_conninfo, wal_level, max_replication_slots. |
map[string]intOrString |
{} |
Quorum-based synchronous replication
| Name | Description | Type | Value |
|---|---|---|---|
quorum |
Quorum configuration for synchronous replication. | object |
{} |
quorum.minSyncReplicas |
Minimum number of synchronous replicas required for commit. | int |
0 |
quorum.maxSyncReplicas |
Maximum number of synchronous replicas allowed (must be less than total replicas). Under autoscaling it also sets the scale-down floor (the effective minimum is max(minReplicas, maxSyncReplicas+1, 2)); the chart fails to render if that floor exceeds autoscaling.maxReplicas. |
int |
0 |
Autoscaling
| Name | Description | Type | Value |
|---|---|---|---|
autoscaling |
Read-replica autoscaling configuration. | object |
{} |
autoscaling.enabled |
Enable horizontal autoscaling of read replicas. Requires the platform keda package to be enabled by an administrator first; the chart fails to render (rather than break the release) if KEDA is not installed. Prerequisite: the queried vmselect must hold BOTH the CNPG metrics (cnpg_backends_total / container CPU) AND kube_pod_labels (kube-state-metrics). This holds for a tenant on the shared root monitoring stack (monitoring: false, both land in tenant-root). A tenant running its OWN Monitoring app (monitoring: true) has the CNPG metrics but not kube_pod_labels (KSM is root-only), so it must set serverAddress to a vmselect carrying both series or autoscaling never actuates. |
bool |
false |
autoscaling.serverAddress |
Override the Prometheus/vmselect URL the ScaledObject queries. Empty (default) derives http://vmselect-shortterm.<monitoring-ns>.svc:8481/select/0/prometheus. Set it when the derived vmselect does not hold both the CNPG metrics and kube_pod_labels (e.g. monitoring: true tenants, or a non-default metricsStorages name). SSRF constraint: the host must be an in-cluster Service DNS name (<svc>.<ns>.svc[.<clusterDomain>]) whose namespace is within the tenant's own subtree (its namespace, its parents, or tenant-root) — the cluster-privileged KEDA operator fetches this URL every poll, so it is refused a metadata endpoint, an external host, or a sibling/system namespace. Residual surface (accepted trade-off): tenant-root is always allowed because the derived default points there, so a tenant can still aim the operator at any Service in tenant-root on any port/path (a blind in-cluster reachability probe — the operator reads no response body back to the tenant); the egress NetworkPolicy does not narrow that. |
string |
"" |
autoscaling.minReplicas |
Minimum total instances; raised to the synchronous-quorum floor (maxSyncReplicas + 1) and to 2 when either is higher. |
int |
2 |
autoscaling.maxReplicas |
Maximum total instances. Must be >= minReplicas and >= the synchronous-quorum floor (maxSyncReplicas+1); the chart fails to render (rather than silently run an unbounded instance count) if the effective floor exceeds it - raise maxReplicas. HAZARD: lowering maxReplicas below the live count makes the HPA cut straight to the new maximum in one step (the current>max branch bypasses the scale-down pacing), shedding the surplus replicas + PVCs at once; to lower it deliberately, suspend the HelmRelease and pin the live count with kubectl scale first. |
int |
6 |
autoscaling.metric |
Read-load signal to scale on. | string |
ReadConnections |
autoscaling.target |
Target read load per read-serving replica (unit depends on metric: active connections, or CPU millicores). Dimensionless integer - a resource.Quantity here would accept 150m, which PromQL reads as 150 minutes and KEDA cannot use as a threshold. |
int |
150 |
autoscaling.maxReplicationLagSeconds |
Freeze scaling while replication lag exceeds this (seconds) and the primary is writing. 0 disables the brake. Default 0: the freeze branch has only been validated on a live cluster in its non-braking (pass-through) form, so the lag brake is opt-in until the braking path is exercised under load; set a positive value (e.g. 30) to enable it. | int |
0 |
autoscaling.dryRun |
Recommendation mode: keep the static count and render the ScaledObject with both scaling directions paused, so KEDA keeps the HPA and keeps serving its trigger metric but actuates neither way. Note both scaling policies are disabled, so the HPA's desiredReplicas is frozen to the current count - read the recommendation from the matching read-load dashboard panel for your metric (connections or CPU): desired = 1 + ceil(read load / target), clamped to [effectiveMin..maxReplicas] where effectiveMin = max(minReplicas, maxSyncReplicas+1, 2), not from the desired-replicas line. Like disabling, flipping a LIVE autoscaled cluster into dryRun re-renders instances: replicas (the raw static value, which can be below the autoscaling floor); if replicas is below the live count, stage it to the live count first (see the enablement note) or the cluster sheds down to replicas. No shed occurs when the rendered value is unchanged (replicas already equals the active seed) — client-side merge is then a no-op and the live count is preserved. Permanent, unlike transition. |
bool |
false |
autoscaling.transition |
Migration phase 1: render .spec.instances from replicas (which the operator stages to the current live count) AND pause the ScaledObject in both directions, so KEDA stands up with its HPA present but not actuating. Flip to false (phase 2) to hand the runtime count to KEDA. Because the both-direction pause keeps the HPA (not the HPA-deleting full pause), KEDA is genuinely warm at phase 2 — no HPA recreate — and because the active seed is max(replicas, effectiveMin), phase 2 re-renders the same instance count phase 1 did (when replicas is staged at/above the floor), so the flip is a merge no-op with no dip. The HPA's scaleUp stabilization window still smooths the first actuation. |
bool |
false |
Users configuration
| Name | Description | Type | Value |
|---|---|---|---|
users |
Users configuration map. Passwords are always auto-generated and stored in the <release>-credentials Secret; they cannot be set from values — read a user's password from that Secret. A password left over in values from before the field was removed is ignored by the render and draws an admission warning, but it is not inert on an upgraded release: the chart preserves whatever password is already in the Secret, which on the first upgrade is the value that was set before removal, so that value stays the live credential until it is rotated (dedicated rotation is tracked in cozystack/community#72). The live password lives only in the Secret. |
map[string]object |
{} |
users[name].replication |
Whether the user has replication privileges. | bool |
false |
users[name].createRole |
Whether the user can create and manage roles (CREATEROLE). Requires PostgreSQL 16 or newer. Setting it back to false withdraws the attribute only: the roles the user created, and its ADMIN and SET memberships in them, stay. Once any user has held it, the chart no longer adopts an existing role it did not create, so a user or database declared later under the name of such a role fails the init job until a platform administrator drops that role. |
bool |
false |
users[name].bypassRls |
Whether the user can bypass row-level security policies (BYPASSRLS). A role can only create other roles carrying BYPASSRLS if it holds the attribute itself, so this is required alongside createRole to provision a privileged service role. |
bool |
false |
Databases configuration
| Name | Description | Type | Value |
|---|---|---|---|
databases |
Databases configuration map. | map[string]object |
{} |
databases[name].roles |
Roles assigned to users. | object |
{} |
databases[name].roles.admin |
List of users with admin privileges. | []string |
[] |
databases[name].roles.readonly |
List of users with read-only privileges. | []string |
[] |
databases[name].extensions |
List of enabled PostgreSQL extensions. | []string |
[] |
Backup parameters
| Name | Description | Type | Value |
|---|---|---|---|
backup |
Backup configuration. | object |
{} |
backup.enabled |
Enable regular backups. | bool |
false |
backup.useSystemBucket |
Opt-in: when true, the chart-emitted <release>-s3-creds Secret is skipped AND spec.plugins (plus the barman-cloud ObjectStore) is left UNSET in the chart-rendered Cluster — the cozy-default BackupClass driver SSA-applies an ObjectStore (carrying destinationPath/endpointURL/credentials) and patches spec.plugins on the live Cluster when the first BackupJob runs. Consequence: plugin WAL archiving is NOT active until that first BackupJob fires; WAL accumulates on the PVC in the meantime, so fire an ad-hoc BackupJob immediately after enabling the flag on existing releases. Use together with the platform cozy-default BackupClass — tenants do not need to fill s3AccessKey/s3SecretKey or destinationPath/endpointURL. The destination path automatically scopes to s3://cozy-backups/<namespace>/<release>/. |
bool |
false |
backup.schedule |
Legacy. Cron schedule (CNPG 6-field format) for the chart-emitted ScheduledBackup. Empty means no chart-managed schedule, which is the recommended setup when a BackupClass from backups.cozystack.io already drives backup orchestration. In the legacy chart-managed flow spec.plugins plus the barman-cloud ObjectStore is rendered when backup.enabled=true AND useSystemBucket=false AND destinationPath is non-empty AND inline-or-external creds are supplied; in the platform useSystemBucket=true flow the chart skips emitting spec.plugins and the CNPG driver SSA-applies the ObjectStore and patches spec.plugins onto the live Cluster at first BackupJob time. |
string |
"" |
backup.retentionPolicy |
Retention policy (e.g. "30d"). | string |
30d |
backup.destinationPath |
DEPRECATED. Per-tenant S3 configuration is superseded by the platform-managed cozy-default BackupClass and the cozy-backups system bucket. Leave empty for new installations; the BackupClass driver picks up the system-managed coordinates. Kept for in-place upgrade compatibility. |
string |
s3://bucket/path/to/folder/ |
backup.endpointURL |
DEPRECATED. See destinationPath. |
string |
http://minio-gateway-service:9000 |
backup.s3AccessKey |
DEPRECATED. Tenants no longer supply S3 keys; the system Bucket Secret is projected into the tenant namespace by the backup controller. Ignored when s3CredentialsSecret.name is set or useSystemBucket is true. The chart skips materialising <release>-s3-creds whenever this field is empty so a default install does not leak placeholder credentials into the tenant namespace. |
string |
"" |
backup.s3SecretKey |
DEPRECATED. See s3AccessKey. |
string |
"" |
backup.s3CredentialsSecret |
DEPRECATED. Pre-existing Secret with S3 credentials. Use the platform-managed cozy-default BackupClass instead. When set, the chart references this Secret directly (legacy chart-managed flow). The CNPG backup driver writes this field on restore so credentials never land in the CR .spec. |
object |
{} |
backup.s3CredentialsSecret.name |
Name of the Secret in the application namespace. Empty means the chart materialises <release>-s3-creds from s3AccessKey/s3SecretKey. |
string |
"" |
backup.s3CredentialsSecret.accessKeyIDKey |
Key in the Secret holding the access key ID. Defaults to AWS_ACCESS_KEY_ID. |
string |
"" |
backup.s3CredentialsSecret.secretAccessKeyKey |
Key in the Secret holding the secret access key. Defaults to AWS_SECRET_ACCESS_KEY. |
string |
"" |
backup.endpointCA |
DEPRECATED. Pre-existing Secret with the CA bundle the barman-cloud plugin should trust when reaching a self-signed S3 endpoint. Used for both backup and bootstrap recovery in the legacy chart-managed flow. | object |
{} |
backup.endpointCA.name |
Name of the Secret in the application namespace. Empty means no endpointCA is emitted (the plugin uses the system trust store). | string |
"" |
backup.endpointCA.key |
Key within the Secret containing the CA bundle. Defaults to ca.crt. |
string |
"" |
Bootstrap (recovery) parameters
| Name | Description | Type | Value |
|---|---|---|---|
bootstrap |
Bootstrap configuration. | object |
{} |
bootstrap.enabled |
Whether to restore from a backup. | bool |
false |
bootstrap.recoveryTime |
Timestamp (RFC3339) for point-in-time recovery; empty means latest. | string |
"" |
bootstrap.backupID |
Barman ID of the base backup to recover from, passed as recoveryTarget.backupID. Empty lets the barman-cloud plugin pick one: the newest backup when recoveryTime is empty, otherwise the newest one ending at or before it, on any timeline. The CNPG backup driver sets this on restore. |
string |
"" |
bootstrap.oldName |
Previous cluster name before deletion. | string |
"" |
bootstrap.serverName |
Server name (S3 path prefix) used by the original cluster when writing backups; passed to the barman-cloud plugin via externalClusters[].plugin.parameters.serverName. Defaults to bootstrap.oldName. Set this only when the original cluster wrote backups under an explicit server name that differed from its Kubernetes resource name. |
string |
"" |
bootstrap.newServerName |
WAL-archive server name (S3 path prefix) the RESTORED cluster writes to. Must differ from serverName (the recovery source) so a restored cluster archives to a fresh, empty prefix and barman-cloud-check-wal-archive passes while it recovers from the source's prefix. The CNPG backup driver sets this on restore; empty means the cluster archives under its own name. |
string |
"" |
Parameter examples and reference
resources and resourcesPreset
resources sets explicit CPU and memory configurations for each replica.
Every resource it leaves unset is taken from the preset defined in resourcesPreset.
resources:
cpu: 4000m
memory: 4Gi
resourcesPreset sets named CPU and memory configurations for each replica.
This setting is ignored if the corresponding resources value is set.
Presets follow a cloud-style <series>.<size> naming convention. Five series cover the full CPU-to-memory ratio range (t1 1:0.5, c1 1:1, s1 1:2, u1 1:4, m1 1:8) and each series ships eight sizes (nano through 4xlarge). The legacy flat names (nano, micro, small, medium, large, xlarge, 2xlarge) remain accepted as deprecated aliases and keep their original sizes, which do not follow one series: nano through small equal the t1 sizes of the same name, while medium equals c1.small rather than c1.medium.
See docs/operations/resource-presets.md for the full size matrix and the legacy-to-instance-type mapping.
users
users:
user1: {}
user2: {}
airflow: {}
debezium:
replication: true
Passwords cannot be set here — they are auto-generated and stored in the <release>-credentials Secret. Read a user's password with kubectl get secret <release>-credentials -o json | jq -r '.data["user1"]' | base64 -d (a username may contain a ., which jsonpath would treat as a path step and return nothing).
databases
databases:
myapp:
roles:
admin:
- user1
- debezium
readonly:
- user2
airflow:
roles:
admin:
- airflow
extensions:
- hstore
A database name may contain only letters, digits and -._, and must be at most 54 characters. Renaming a database by changing its key in values makes the init-job drop the old database with its data. To keep the data, a platform administrator first renames it as the CNPG superuser (ALTER DATABASE "<old>" RENAME TO <new>, ALTER ROLE "<old>_admin" RENAME TO <new>_admin and the same for _readonly), and only then is the key changed; the chart then adopts the renamed database on the next reconcile.