DatabaseServer¶
DatabaseServer is a namespaced kind that runs one PostgreSQL server. You create it. The operator runs the server as a CloudNativePG cluster and publishes its connection details as a DatabaseServerConfig. That is a contract: a resource that carries the address and credentials of the server for other resources to reference. With spec.archive, it also keeps a continuous archive of the server in an object storage bucket.
The server is the relational secondary storage of the clusters that use it. A Database creates a logical database and its users on the published contract, and a CamundaCluster connects through that Database. An archive lets a PointInTimeRestore roll the server back to a point in time.
The operator needs the CloudNativePG operator on the Kubernetes cluster. An archive also needs the Barman Cloud plugin and cert-manager. See Installation.
The smallest server names a PostgreSQL major, a volume size, and the contract to publish:
apiVersion: core.camunda.io/v1
kind: DatabaseServer
metadata:
name: my-db
namespace: my-cluster-ns
spec:
version: "17"
storageSize: "64Gi"
databaseServerConfig: my-db-server
The name of the server is also the name of its CloudNativePG cluster. It must start with a lowercase letter, hold only lowercase letters, digits, and -, and be 46 characters or shorter.
graph LR
DBS[DatabaseServer] -.->|presetRef| DBSP[DatabaseServerPreset]
DBS -.->|releaseRef| CR[CamundaRelease]
DBS -.->|archive.objectStorageRef| OSC[ObjectStorageConfig]
DBS -->|creates| PG["PostgreSQL instances"]
DBS -->|creates| ARC["Archive in the bucket"]
DBS -->|publishes| DBSC[DatabaseServerConfig]
DB[Database] -.->|serverRef| DBSC
Endpoints and credentials¶
The published contract carries the connection details. Its host is the read-write Service of the server, my-db-rw.my-cluster-ns.svc, on port 5432. Its adminCredentialsSecretRef names the Secret my-db-superuser, which CloudNativePG writes with the keys username and password.
The contract appears only after that Secret exists. Until then, ContractReady is False with reason Blocked.
Give the contract to a Database in the same namespace:
apiVersion: core.camunda.io/v1
kind: Database
metadata:
name: my-db-camunda
namespace: my-cluster-ns
spec:
serverRef: my-db-server
databaseName: camunda
# ... the rest of your database
If you change spec.databaseServerConfig, the server publishes the contract under the new name and removes the old one. A Database that still names the old contract reports Ready False with reason InvalidReference. Point it at the new name. The contract that asked for the last rollback stays, because it holds the answer. See Recovery.
Sizing and storage¶
spec.instances is the number of PostgreSQL instances. One instance has no failover. Two or more give CloudNativePG a standby to promote. spec.resources sets the CPU and memory of each instance.
spec.storageSize is the size of the data volume of each instance. spec.walStorageSize puts the write-ahead log on a separate volume of that size.
apiVersion: core.camunda.io/v1
kind: DatabaseServer
metadata:
name: my-db
namespace: my-cluster-ns
spec:
instances: 3
storageSize: "256Gi"
walStorageSize: "32Gi"
storageClassName: "ssd"
# ... the rest of your server
A volume cannot shrink. The API server rejects a smaller value on the server that replaces a value set there. A preset can still lower a size under a running server, and a first value on the server can be smaller than the current size. The server then keeps its current size and records a Warning event with reason StorageShrinkIgnored. If you raise a size, CloudNativePG grows the volumes in place when the StorageClass allows volume expansion.
You can add walStorageSize to a running server, but you cannot remove the volume again. If you or a preset clear the field, the server keeps the volume and records a Warning event with reason WALStorageKept. To get a smaller volume or no write-ahead log volume, create a new server.
status.volumes lists the volumes of the server and the capacity of each. A write-ahead log volume has the name of its data volume with the suffix -wal.
The archive¶
Without spec.archive, the server keeps no archive, and no point-in-time restore can reach it. With spec.archive, the server writes its write-ahead log to the bucket that an ObjectStorageConfig names, and takes base backups on a schedule. A restore starts from a base backup and replays the log up to the requested point.
apiVersion: core.camunda.io/v1
kind: DatabaseServer
metadata:
name: my-db
namespace: my-cluster-ns
spec:
archive:
objectStorageRef: my-backup-bucket
retentionPeriodDays: 30
baseBackupSchedule: "0 0 2 * * *"
# ... the rest of your server
The contract of a server with an archive declares pitr.enabled: true, pitr.recovery: operator, and pitr.retentionPeriodDays. A PointInTimeRestore reads these values.
Retention¶
retentionPeriodDays is how far into the past a restore can reach. The bucket keeps the archive for that number of days, and the contract publishes the same number.
A raised retention period does not bring back what the shorter one already removed. The window grows only as the archive writes new data. status.archive.reachableFrom is the oldest point that the bucket still holds, rounded down to the minute. The bucket can miss up to one minute after it.
Schedule¶
baseBackupSchedule is a six-field cron in UTC, with seconds first: seconds, minutes, hours, day of month, month, day of week. The default 0 0 2 * * * runs daily at 02:00. The descriptors @yearly, @annually, @monthly, @weekly, @daily, @midnight, @hourly, and @every <duration> are also accepted. CloudNativePG reads the schedule in the robfig/cron format.
The API server rejects these values:
- A five-field cron. CloudNativePG reads its first field as seconds, so
0 2 * * *runs every hour, not daily. - A value outside the range of its field, for example hour
24. - A step of more than three digits, or an
@everynumber of more than six digits on each side of the point.
A range that reads downward, such as FRI-MON, passes the API server. The server then reports Ready False with reason InvalidReference, and the message names the schedule.
Readiness of the archive¶
The first base backup runs as soon as the server is up, whatever the schedule says. ArchiveReady is False with reason Blocked until that backup completes, because no restore can start without a base backup.
If uploads of the write-ahead log fail for more than five minutes, ArchiveReady and Ready turn False with reason ArchiveFailing. The message says what CloudNativePG reports. The archive holds every point up to the last segment that arrived. Repair the bucket or its credentials. The plugin then uploads the held-back segments, and both conditions return to True.
The archive history¶
status.archive.history records each archive that the server has written. A restore can reach a point only inside one of these intervals.
status:
archive:
history:
- serverName: my-db
objectStorageRef: my-backup-bucket
location: s3://my-backup-bucket/clusters/databaseserver/my-cluster-ns/my-db-4c2a9f1e (region eu-west-1)
from: "2026-08-01T10:00:00Z"
to: "2026-08-20T15:02:11Z"
- serverName: my-db-r1
objectStorageRef: my-backup-bucket
location: s3://my-backup-bucket/clusters/databaseserver/my-cluster-ns/my-db-4c2a9f1e (region eu-west-1)
from: "2026-08-20T15:20:40Z"
reachableFrom: "2026-07-25T09:58:00Z"
serverNameis the directory in the bucket that holds the archive.objectStorageRefandlocationname the bucket and the path. Each server writes under a path of its own, so one bucket can hold the archives of many servers.fromis the earliest point a restore can reach in the archive, andtois the latest. The record withouttois the archive that the server writes now.unverifiedFromis set on the open record while uploads fail. A restore to a point after it can find no data.
The server closes the open record and opens a new one in these cases:
- A rollback replaces the cluster.
- You remove
spec.archive, then add it again. - The archive moves to another location. A change of
spec.archive.objectStorageRefto another bucket does this. So does an edit of theObjectStorageConfigto another bucket, path, endpoint, or region.
A new record starts at the first base backup of the new archive. No restore can reach a point between two records. A rollback reads only the location that the server archives to now. A point in a record of an earlier location is refused, and the message names both locations.
Base backups are not the backup model¶
The base backups belong to the archive. BackupSchedule and LogicalBackupRDBMS take logical dumps instead, and they never use these base backups. Run both on one cluster. The logical backups restore the Camunda data. The archive restores the server to a point in time.
Recovery¶
A server with an archive rolls itself back to any point in its archive history. For a Camunda cluster, use a PointInTimeRestore. It suspends the cluster, asks the server for the rollback, and restores the primary storage in step with it.
You can also ask for a rollback by hand, for a consumer outside the operator. Stop every writer first. Then write spec.recovery on the published contract:
apiVersion: core.camunda.io/v1
kind: DatabaseServerConfig
metadata:
name: my-db-server
namespace: my-cluster-ns
spec:
recovery:
requestID: 3f2b1c4d-5e6a-4b7c-8d9e-0f1a2b3c4d5e
requestedBy: my-cluster-ns/my-restore
targetTime: "2026-08-20T14:30:00Z"
# ... the rest of your contract
The answer arrives in spec.pitr.lastRecovery on the same contract. DatabaseServerConfig documents the request and the results.
CAUTION: A rollback erases everything that the server wrote after targetTime. It rolls back every logical database on the server. For this reason, a PointInTimeRestore refuses a server that more than one Database uses, with reason SharedServer.
What a rollback does¶
The operator builds a new CloudNativePG cluster from the archive. Its name is the server name, -r, and the number of records in the archive history. For example, the first rollback of my-db builds my-db-r1. status.recovery.cluster names the cluster that the rollback builds.
When the new cluster is healthy, the contract points at it, and the operator removes the old cluster and its volumes. The contract then carries a new host and a new superuser Secret. A CamundaCluster restarts its pods to use them. status.cluster names the cluster that the contract points at.
The new cluster writes an archive of its own in the same bucket. The old archive stays, so a later restore can reach a point before the rollback. The new cluster runs under the same ServiceAccount, my-db-postgres, so the bucket keeps its trust in the pods. See Authentication to the bucket.
If another owner already has a CloudNativePG cluster of that name, the rollback ends with result: Failed. The message names the cluster.
If another owner takes the new cluster after the contract points at it, the rollback also ends with result: Failed. The server keeps the contract until it gives this answer. Then the contract points at the old cluster again, and the server runs from it.
While a rollback runs, an edit of spec.databaseServerConfig, of spec.archive, or of the bucket waits until the rollback is answered. Until then, Ready reports InvalidReference, and the message says what to put back.
Refused requests¶
result |
Cause |
|---|---|
Failed |
The server is suspended. Unsuspend it, then ask again. |
Failed |
Another owner controls the ServiceAccount my-db-postgres. Remove that ServiceAccount, then ask again. |
Unavailable |
The server has no archive, or another owner controls its ObjectStore. |
Unavailable |
targetTime is in the future, or older than retentionPeriodDays. |
Unavailable |
targetTime is older than status.archive.reachableFrom. |
Unavailable |
No record of the archive history holds targetTime, or the record is in an earlier location. The message names the windows that the server holds. |
Authentication to the bucket¶
If the ObjectStorageConfig holds static credentials, the operator copies them into the Secret my-db-archive next to the server. Anyone who can read Secrets in that namespace can then read the bucket credentials. Use workload identity to keep them out of the namespace.
The instance pods run under the ServiceAccount my-db-postgres, the server name and -postgres. The operator creates it. A rollback does not change it, so a cloud binding that names it stays valid. The principal to bind is system:serviceaccount:my-cluster-ns:my-db-postgres. While the server archives, the operator also binds the Role of the Barman Cloud plugin to this ServiceAccount. The pods can then read the archive settings.
If the ObjectStorageConfig uses workload identity, the operator puts its annotation on that ServiceAccount. Add your own annotations with spec.serviceAccount.annotations. A value that you set wins over the derived value of the same key.
apiVersion: core.camunda.io/v1
kind: DatabaseServer
metadata:
name: my-db
namespace: my-cluster-ns
spec:
serviceAccount:
annotations:
eks.amazonaws.com/role-arn: "arn:aws:iam::123456789012:role/my-db-archive"
# ... the rest of your server
Monitoring¶
CloudNativePG serves Prometheus metrics on every instance pod. Set spec.monitoring.podMonitor.enabled to create a PodMonitor named my-db-metrics over them.
apiVersion: core.camunda.io/v1
kind: DatabaseServer
metadata:
name: my-db
namespace: my-cluster-ns
spec:
monitoring:
podMonitor:
enabled: true
interval: "30s"
labels:
release: prometheus
# ... the rest of your server
If the Kubernetes cluster does not serve the PodMonitor kind, the operator creates nothing and the server stays ready.
A monitoring block on the server replaces the monitoring block of the preset as a whole. The two blocks do not merge.
Suspend¶
spec.suspend: true stops the instances and keeps their volumes. ClusterReady reports Suspending, then Suspended. Ready stays True with reason Suspended.
The base backup schedule stops with the server. The write-ahead log of the last moments before the stop still reaches the bucket.
The published contract stays, but the server does not answer. Suspend a server only after every cluster that uses it is suspended.
Set the field back to false, and the instances start again on the same volumes.
Presets and releases¶
Three layers make the configuration of a server. Each layer wins over the one before it:
- The DatabaseServerPreset that
spec.presetRefnames holds the shape: the instance count, the volume sizes, the resources. - The CamundaRelease that
spec.releaseRefnames holds the version, inspec.databaseServer.version. - The
DatabaseServeritself.
A field that you set on the server replaces the value of the layer below. A field that you leave unset comes from the layer below.
apiVersion: core.camunda.io/v1
kind: DatabaseServer
metadata:
name: my-db
namespace: my-cluster-ns
spec:
presetRef: standard
releaseRef: camunda-8-9-4
databaseServerConfig: my-db-server
The PostgreSQL version¶
spec.version is a bare PostgreSQL major, such as 17. It selects the image tag. Camunda 8.9 supports PostgreSQL 14 and later. See the RDBMS version support policy.
The major of a running server cannot change, up or down. A change on the server or on its release is refused with Ready False, reason VersionChangeRefused, and a Warning event of the same name. The server keeps running its major. Set the version back to clear the refusal.
To run a later major, create a new DatabaseServer on that major and move the data to it. A point-in-time restore cannot do this, because only the major that wrote an archive can read it.
Images¶
The PostgreSQL image is ghcr.io/cloudnative-pg/postgresql:<version> by default. For an air-gapped cluster, name a CamundaPlatformConfig in spec.platformConfigRef, and set the repository there:
apiVersion: core.camunda.io/v1
kind: CamundaPlatformConfig
metadata:
name: my-platform-config
spec:
images:
postgres: "mirror.example.com/postgresql"
# ... the rest of your platform config
The tag is the major version, so the repository must publish the same tags.
If the mirror needs a pull Secret, create the ServiceAccount my-db-postgres with the Secret before you create the server. The operator takes over the ServiceAccount and does not change its imagePullSecrets. When you delete the server, the ServiceAccount goes with it, also one that you created. The pull Secret stays.
apiVersion: v1
kind: ServiceAccount
metadata:
name: my-db-postgres
namespace: my-cluster-ns
imagePullSecrets:
- name: my-mirror-pull
Kubernetes gives a pod the pull Secrets of its ServiceAccount only when it creates the pod. If you add the Secret to a server that runs, delete the instance pods that cannot pull the image. CloudNativePG then creates them again with the Secret.
Name collisions¶
The server never writes on an object that another owner holds under a name that the server derives. Each case shows on a condition, and the message names the owner:
- A CloudNativePG cluster of the server name that this server does not own:
ClusterReadyreportsClusterTaken. The server runs nothing, and it removes its contract, its base backup schedule, and itsPodMonitor. - A
DatabaseServerConfigof the name inspec.databaseServerConfigthat this server did not publish:ContractReadyreportsContractTaken. The server publishes nothing, and the contract keeps its endpoint and credentials. - A Barman Cloud
ObjectStoreof the server name that another owner controls:ArchiveReadyreportsArchiveTaken. The server writes no archive, its contract declarespitr.enabled: false, and it refuses rollbacks. - A ServiceAccount of the name
my-db-postgresthat another owner controls:ClusterReadyreportsBlocked. The server does not write its CloudNativePG cluster, removes its bindings to the Role of the Barman Cloud plugin, and refuses rollbacks. - A RoleBinding of the name
<cluster>-barman-cloud-postgresthat another owner controls:ClusterReadyreportsBlocked, and the server does not write its CloudNativePG cluster. The name ismy-db-barman-cloud-postgres, ormy-db-r1-barman-cloud-postgresafter the first rollback. - The archive Secret, the base backup schedule, or the
PodMonitorunder another owner:ArchiveReadyorMonitoringReadyreportsFalse.
While the server is suspended, a ServiceAccount or a RoleBinding under another owner does not change ClusterReady, which reports Suspended. The collision shows when you resume the server.
Remove the other object, or give this server a name of its own. The server then continues with the archive history that it had. While the ObjectStore name was held, the server wrote no archive, so no restore can reach a point in that time.
Deletion¶
Deleting a DatabaseServer removes the CloudNativePG cluster, its data volumes, the ServiceAccount of the instance pods, and the published contract. The objects in the bucket stay. If you no longer need them, remove them yourself.
Status¶
kubectl get databaseserver shows Ready, its reason, the PostgreSQL version, and the age.
status:
observedGeneration: 3
version: "17"
cluster: my-db-r1
systemIdentifier: "7370000000000000001"
archive:
# ... see The archive history
recovery:
requestID: 3f2b1c4d-5e6a-4b7c-8d9e-0f1a2b3c4d5e
contract: my-db-server
requestedBy: my-cluster-ns/my-restore
targetTime: "2026-08-20T14:30:00Z"
cluster: my-db-r1
result: Completed
completedAt: "2026-08-20T15:02:11Z"
volumes:
- name: my-db-r1-1
capacity: 256Gi
- name: my-db-r1-1-wal
capacity: 32Gi
conditions:
- type: Ready
status: "True"
reason: Healthy
- type: ClusterReady
status: "True"
reason: Healthy
message: 3 of 3 instances are ready
- type: ArchiveReady
status: "True"
reason: Healthy
- type: ContractReady
status: "True"
reason: Healthy
- type: MonitoringReady
status: "True"
reason: Disabled
| Type | Reason | Meaning | What to do |
|---|---|---|---|
Ready |
Healthy |
Every part of the server is in its desired state. | Nothing. |
Ready |
Blocked |
The archive holds no base backup yet, or ClusterReady is Blocked. |
Wait for the base backup. Otherwise read ClusterReady. |
Ready |
Creating, Updating, Failing, Degraded, Down |
ClusterReady holds Ready back. |
Read the row of ClusterReady with the same reason. |
Ready |
ArchiveFailing |
The write-ahead log does not reach the bucket. | Read ArchiveReady. |
Ready |
Suspended |
spec.suspend is true and the instances are stopped. |
Nothing. |
Ready |
ClusterTaken, ContractTaken, ArchiveTaken |
Another owner holds a name that the server derives. | See Name collisions. |
Ready |
CNPGNotInstalled |
The Kubernetes cluster did not serve the CloudNativePG kinds when the operator started. | Install CloudNativePG, then restart the operator. |
Ready |
BarmanPluginNotInstalled |
The server asks for an archive, and the Kubernetes cluster did not serve the Barman Cloud plugin when the operator started. | Install the plugin, then restart the operator. |
Ready |
InvalidReference |
A referenced resource does not exist, the merged spec lacks a field or breaks a rule, or an edit waits for a rollback. The message names it. | Create the resource, or fix the spec. |
Ready |
MissingSecret |
The credentials Secret of the bucket is missing or lacks a key. The message names it. | Create the Secret, or fix its keys. |
Ready |
VersionChangeRefused |
version names a major other than the one the server runs. The message names both. |
Set the version back. See The PostgreSQL version. |
ClusterReady |
Creating, Updating |
CloudNativePG is starting or changing the instances. | Wait. |
ClusterReady |
Healthy |
Every instance is ready. | Nothing. |
ClusterReady |
Failing |
CloudNativePG reports a phase that it does not leave on its own. The message names the phase. | Read the CloudNativePG cluster for the cause. |
ClusterReady |
Degraded |
At the end of the grace period, 30 minutes by default, the cluster is not in its desired state. At least one instance is ready. Not every instance is ready, or CloudNativePG reports a phase other than healthy. The message names the ready count and the phase. | Read the phase and the instances in the status of the CloudNativePG cluster. |
ClusterReady |
Down |
At the end of the grace period, no instance is ready, or a wait that Blocked reported still holds. The message says which. A blocked wait can stand while the existing instances still serve. |
Read the message. For no ready instance, read the CloudNativePG cluster and the pods of its instances. For a wait, read the Blocked row. |
ClusterReady |
Suspending, Suspended |
spec.suspend is true. |
Nothing. |
ClusterReady |
Blocked |
The ServiceAccount my-db-postgres or a RoleBinding <cluster>-barman-cloud-postgres belongs to another owner, or the cluster that a rollback moved to is gone. The message says which. At the end of the grace period, the reason changes to Down, and the message still names the owner. |
See Name collisions, or Recovery. |
ClusterReady |
ClusterTaken |
A CloudNativePG cluster of the server name belongs to another owner. | See Name collisions. |
ArchiveReady |
Disabled |
The server has no archive block. |
Nothing. |
ArchiveReady |
Blocked |
The archive holds no base backup yet. | Wait. If it never completes, read the CloudNativePG Backup for the cause. |
ArchiveReady |
ArchiveFailing |
Uploads of the write-ahead log have failed for more than five minutes. | Repair the bucket or its credentials. |
ArchiveReady |
ArchiveTaken |
A Barman Cloud ObjectStore of the server name belongs to another owner. |
See Name collisions. |
ArchiveReady |
Healthy |
The archive holds a base backup and receives the write-ahead log. | Nothing. |
ContractReady |
Blocked |
The superuser Secret does not exist yet. | Wait for the instances to start. |
ContractReady |
ContractTaken |
The contract name belongs to another owner. | See Name collisions. |
ContractReady |
Disabled |
The cluster name is taken, so the server removed the contract. | Read ClusterReady. |
ContractReady |
Healthy |
The contract is published. | Nothing. |
MonitoringReady |
Disabled |
Scraping is off. | Nothing. |
MonitoringReady |
Healthy |
The PodMonitor exists. |
Nothing. |
MonitoringReady never affects Ready. ArchiveReady affects Ready only on a server with an archive block.
status.version is the PostgreSQL major that the server runs. status.cluster is the CloudNativePG cluster that the contract points at, and it changes after a rollback.
status.recovery is the rollback that the server works on now, or the last one it answered. result and completedAt are empty while the rollback runs.
status.observedGeneration is the last generation that the operator reconciled.
Fields¶
The API reference lists every field of DatabaseServer, with its type, whether it is required, its default, and its validation.
Validation rules¶
metadata.namemust be a DNS-1035 label of 46 characters or fewer: lowercase letters, digits, and-, starting with a letter.databaseServerConfigis required on aDatabaseServerand must not be set in a preset.storageSizeandwalStorageSizecannot shrink. See Sizing and storage.versionis a bare major, such as17. A major below 14 is reported onReadywith reasonInvalidReference.versioncannot move to another major once the server runs. See The PostgreSQL version.archive.retentionPeriodDaysmust be from 1 to 36500.archive.baseBackupSchedulemust be a six-field cron or a descriptor. See Schedule.versionandstorageSizemust be present after the merge. Set them inline, or takestorageSizefrom a preset andversionfrom a release. A missing field is reported onReadywith reasonInvalidReference.
A production-shaped example¶
apiVersion: core.camunda.io/v1
kind: DatabaseServer
metadata:
name: my-db
namespace: my-cluster-ns
spec:
presetRef: standard
releaseRef: camunda-8-9-4
platformConfigRef: my-platform-config
databaseServerConfig: my-db-server
archive:
objectStorageRef: my-backup-bucket
retentionPeriodDays: 30
Related¶
- DatabaseServerPreset: the cluster-scoped baseline that
spec.presetRefnames. - CamundaRelease: the version that
spec.releaseRefnames. - DatabaseServerConfig: the contract this kind publishes.
- Database: creates the logical database and its users on the published contract.
- ObjectStorageConfig: the bucket that
spec.archive.objectStorageRefnames. - PointInTimeRestore: reads
pitr.enabledand the retention period from the published contract. - Secondary storage guide: how to choose and connect secondary storage.
- Installation: CloudNativePG, cert-manager, and the Barman Cloud plugin.