Skip to content

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
Hold "Alt" / "Option" to enable pan & zoom

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 @every number 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"
  • serverName is the directory in the bucket that holds the archive.
  • objectStorageRef and location name the bucket and the path. Each server writes under a path of its own, so one bucket can hold the archives of many servers.
  • from is the earliest point a restore can reach in the archive, and to is the latest. The record without to is the archive that the server writes now.
  • unverifiedFrom is 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.objectStorageRef to another bucket does this. So does an edit of the ObjectStorageConfig to 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:

  1. The DatabaseServerPreset that spec.presetRef names holds the shape: the instance count, the volume sizes, the resources.
  2. The CamundaRelease that spec.releaseRef names holds the version, in spec.databaseServer.version.
  3. The DatabaseServer itself.

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: ClusterReady reports ClusterTaken. The server runs nothing, and it removes its contract, its base backup schedule, and its PodMonitor.
  • A DatabaseServerConfig of the name in spec.databaseServerConfig that this server did not publish: ContractReady reports ContractTaken. The server publishes nothing, and the contract keeps its endpoint and credentials.
  • A Barman Cloud ObjectStore of the server name that another owner controls: ArchiveReady reports ArchiveTaken. The server writes no archive, its contract declares pitr.enabled: false, and it refuses rollbacks.
  • A ServiceAccount of the name my-db-postgres that another owner controls: ClusterReady reports Blocked. 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-postgres that another owner controls: ClusterReady reports Blocked, and the server does not write its CloudNativePG cluster. The name is my-db-barman-cloud-postgres, or my-db-r1-barman-cloud-postgres after the first rollback.
  • The archive Secret, the base backup schedule, or the PodMonitor under another owner: ArchiveReady or MonitoringReady reports False.

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.name must be a DNS-1035 label of 46 characters or fewer: lowercase letters, digits, and -, starting with a letter.
  • databaseServerConfig is required on a DatabaseServer and must not be set in a preset.
  • storageSize and walStorageSize cannot shrink. See Sizing and storage.
  • version is a bare major, such as 17. A major below 14 is reported on Ready with reason InvalidReference.
  • version cannot move to another major once the server runs. See The PostgreSQL version.
  • archive.retentionPeriodDays must be from 1 to 36500.
  • archive.baseBackupSchedule must be a six-field cron or a descriptor. See Schedule.
  • version and storageSize must be present after the merge. Set them inline, or take storageSize from a preset and version from a release. A missing field is reported on Ready with reason InvalidReference.

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