How to Install PostgreSQL in Vultr Kubernetes Engine (VKE) with Postgres Operator

Updated on 11 August, 2026
Learn how to install PostgreSQL in Vultr Kubernetes Engine (VKE) using Postgres Operator with step-by-step instructions for deployment and configuration.
How to Install PostgreSQL in Vultr Kubernetes Engine (VKE) with Postgres Operator header image

PostgreSQL is an open-source relational database system with strong support for extensibility, concurrency, and SQL standards compliance. PGO, the Postgres Operator from Crunchy Data, extends Kubernetes with custom resources that automate provisioning, high availability, backups, and upgrades for PostgreSQL clusters.

This guide explains how to install PostgreSQL in a Vultr Kubernetes Engine (VKE) cluster using PGO. It covers installing the operator with Helm, provisioning a cluster backed by Block Storage, configuring pgBackRest to archive write-ahead logs and backups to Object Storage, connecting to the database, and applying common customizations such as replicas, connection pooling, and scheduled backups.

Prerequisites

Before you begin, you need to:

Install the Postgres Operator

PGO is distributed as a Helm chart in an OCI registry. Installing the chart registers the PostgresCluster, PGUpgrade, and PGAdmin Custom Resource Definitions (CRDs) and starts the controller that reconciles them.

  1. Install the operator into a dedicated namespace.

    console
    $ helm install pgo oci://registry.developers.crunchydata.com/crunchydata/pgo \
        --namespace postgres-operator \
        --create-namespace
    
  2. Verify that the operator pod reports a Running status.

    console
    $ kubectl get pods -n postgres-operator
    

    The output displays a single pgo pod with a Running status and 1/1 containers ready.

  3. Confirm that the operator installed its Custom Resource Definitions.

    console
    $ kubectl get crd | grep crunchydata
    

    The output lists postgresclusters, pgupgrades, and pgadmins. The postgresclusters resource serves both the stable v1 API and the older v1beta1 API.

Store the Object Storage Credentials

pgBackRest reads its S3 credentials from a Kubernetes Secret rather than from the cluster manifest, which keeps the access keys out of the resource definition. The Secret must contain a key named s3.conf holding a pgBackRest configuration section.

  1. Retrieve your Object Storage access key and secret key from the Object Storage page in the Vultr Console.

  2. Create the pgBackRest configuration file.

    console
    $ nano s3.conf
    
  3. Add the following configuration. Replace S3-ACCESS-KEY and S3-SECRET-KEY with your Object Storage credentials.

    ini
    [global]
    repo1-s3-key=S3-ACCESS-KEY
    repo1-s3-key-secret=S3-SECRET-KEY
    

    Save and close the file.

  4. Create the Secret from the file.

    console
    $ kubectl create secret generic pgo-s3-creds -n postgres-operator --from-file=s3.conf
    

    The --from-file flag names the Secret key after the file, which produces the s3.conf key that pgBackRest expects.

  5. Verify that the Secret contains the s3.conf key.

    console
    $ kubectl get secret pgo-s3-creds -n postgres-operator -o jsonpath='{.data}' | jq 'keys'
    

    The output lists a single key, s3.conf.

  6. Delete the local file so that the credentials do not remain on disk.

    console
    $ rm s3.conf
    

Deploy a Postgres Cluster

A PostgresCluster resource describes the PostgreSQL version, storage, and backup targets. PGO supplies the container images for Postgres, pgBackRest, and pgBouncer automatically, so the manifest does not need to pin image tags.

  1. Create the cluster manifest.

    console
    $ nano postgres.yaml
    
  2. Add the following configuration. Replace BUCKET-NAME with your bucket and OBJECT-STORAGE-HOSTNAME with your Object Storage hostname, such as ewr1.vultrobjects.com.

    yaml
    apiVersion: postgres-operator.crunchydata.com/v1
    kind: PostgresCluster
    metadata:
      name: hippo
      namespace: postgres-operator
    spec:
      postgresVersion: 17
      instances:
        - name: instance1
          replicas: 1
          dataVolumeClaimSpec:
            accessModes:
              - "ReadWriteOnce"
            resources:
              requests:
                storage: 10Gi
      backups:
        pgbackrest:
          configuration:
            - secret:
                name: pgo-s3-creds
          global:
            repo1-path: /pgbackrest/postgres-operator/hippo/repo1
            repo1-s3-uri-style: path
            repo1-retention-full: "14"
          repos:
            - name: repo1
              s3:
                bucket: "BUCKET-NAME"
                endpoint: "OBJECT-STORAGE-HOSTNAME"
                region: "us-east-1"
    

    Save and close the file.

    • apiVersion: Uses the stable v1 API served by current operator releases.
    • postgresVersion: Selects the major PostgreSQL version that PGO deploys. The operator ships images for versions 15 through 18, so set a different major version here to deploy one of the others.
    • dataVolumeClaimSpec: Requests a PersistentVolume from the vultr-block-storage StorageClass. Block Storage enforces a 10 GB minimum, so smaller requests fail to bind.
    • repo1-s3-uri-style: path: Required for Object Storage, which addresses buckets by path rather than by virtual host.
    • repo1-retention-full: Keeps the 14 most recent full backups and expires older ones.
    • region: Object Storage ignores the value but pgBackRest requires the field, so any valid region string works.
  3. Apply the manifest.

    console
    $ kubectl apply -f postgres.yaml
    
  4. Wait for the cluster pod to become ready.

    console
    $ kubectl wait --for=condition=Ready pod \
        -l postgres-operator.crunchydata.com/cluster=hippo \
        -n postgres-operator --timeout=300s
    
  5. Verify the running pods.

    console
    $ kubectl get pods -n postgres-operator
    

    The output displays the operator pod alongside a hippo-instance1 pod with a Running status and 4/4 containers ready, covering Postgres, pgBackRest, and the replication controller.

  6. Verify that the PersistentVolumeClaim bound to Block Storage.

    console
    $ kubectl get pvc -n postgres-operator
    

    Verify that the STATUS column shows Bound and the STORAGECLASS column shows vultr-block-storage.

Verify the Backups

PGO creates a pgBackRest stanza and runs an initial full backup as soon as the cluster starts. The backup and the archived write-ahead logs land in the bucket under the repo1-path prefix.

  1. Wait for the initial backup job to finish.

    console
    $ kubectl wait --for=condition=complete job \
        -l postgres-operator.crunchydata.com/pgbackrest-backup=replica-create \
        -n postgres-operator --timeout=300s
    
  2. Check the repository status reported by the operator.

    console
    $ kubectl get postgrescluster hippo -n postgres-operator \
        -o jsonpath='{.status.pgbackrest.repos}'
    

    Verify that the output reports "stanzaCreated":true and "replicaCreateBackupComplete":true.

  3. Identify the primary pod.

    console
    $ kubectl get pod -n postgres-operator -o name \
        -l postgres-operator.crunchydata.com/cluster=hippo,postgres-operator.crunchydata.com/role=master
    
  4. Query pgBackRest from inside the primary pod. Replace PRIMARY-POD-NAME with the pod name from the previous step.

    console
    $ kubectl exec -n postgres-operator PRIMARY-POD-NAME -c database -- pgbackrest info
    

    The output confirms the stanza status and lists the completed full backup:

    stanza: db
        status: ok
        cipher: none
    
        db (current)
            wal archive min/max (17): 000000010000000000000001/000000010000000000000005
    
            full backup: 20260811-143103F
                database size: 29.4MB, database backup size: 29.4MB
                repo1: backup set size: 3.8MB, backup size: 3.8MB

Connect to the Cluster

PGO generates a Secret named CLUSTER-NAME-pguser-USER-NAME that holds the generated credentials and connection strings. For the cluster in this guide, the Secret is hippo-pguser-hippo.

  1. List the fields the Secret provides.

    console
    $ kubectl get secret hippo-pguser-hippo -n postgres-operator \
        -o jsonpath='{.data}' | jq 'keys'
    

    The Secret contains user, password, dbname, host, port, uri, jdbc-uri, and verifier.

  2. Decode the individual connection values.

    console
    $ kubectl get secret hippo-pguser-hippo -n postgres-operator -o go-template='{{.data.user | base64decode}}'
    $ kubectl get secret hippo-pguser-hippo -n postgres-operator -o go-template='{{.data.dbname | base64decode}}'
    $ kubectl get secret hippo-pguser-hippo -n postgres-operator -o go-template='{{.data.password | base64decode}}'
    
  3. Identify the primary pod.

    console
    $ kubectl get pod -n postgres-operator -o name \
        -l postgres-operator.crunchydata.com/cluster=hippo,postgres-operator.crunchydata.com/role=master
    
  4. Run a query directly against the primary to confirm the server version. Replace PRIMARY-POD-NAME with the pod name from the previous step.

    console
    $ kubectl exec -n postgres-operator PRIMARY-POD-NAME -c database -- psql -U postgres -c "SELECT version();"
    
  5. Forward the primary pod to your workstation to connect with an external client. Replace PRIMARY-POD-NAME with the pod name returned by the previous command.

    console
    $ kubectl -n postgres-operator port-forward PRIMARY-POD-NAME 5432:5432
    

    Connect to localhost:5432 using the decoded user, database, and password.

    Note
    Forward the pod rather than the hippo-primary or hippo-ha Services. Patroni manages those Services without a selector, and kubectl port-forward rejects them with Service is defined without a selector.

Customize the Cluster

The operator reconciles the cluster whenever the PostgresCluster resource changes. Each of the following customizations edits postgres.yaml and applies it again.

Add Replicas

Additional replicas run streaming replication against the primary, and PGO promotes one automatically if the primary fails.

  1. Open the cluster manifest.

    console
    $ nano postgres.yaml
    
  2. Set replicas under the instance set to the total number of Postgres pods.

    yaml
    instances:
      - name: instance1
        replicas: 3
        dataVolumeClaimSpec:
          accessModes:
            - "ReadWriteOnce"
          resources:
            requests:
              storage: 10Gi
    

    Save and close the file.

  3. Apply the manifest.

    console
    $ kubectl apply -f postgres.yaml
    
  4. Verify that the additional pods reach a Running status.

    console
    $ kubectl get pods -n postgres-operator -l postgres-operator.crunchydata.com/cluster=hippo
    

Enable Synchronous Replication

Synchronous replication suits workloads that cannot tolerate losing committed transactions. Commits take longer, and writes to the primary block when no synchronous replica is available.

Apply Add Replicas before this section. Enabling synchronous replication on a single-instance cluster leaves no synchronous replica available, which blocks writes to the primary.

  1. Open the cluster manifest.

    console
    $ nano postgres.yaml
    
  2. Add the patroni block under spec.

    yaml
    patroni:
      dynamicConfiguration:
        synchronous_mode: true
        postgresql:
          parameters:
            synchronous_commit: "on"
            archive_timeout: 60
    

    Save and close the file.

    The archive_timeout parameter forces a write-ahead log segment switch every 60 seconds so that Object Storage receives archives on a predictable interval.

  3. Apply the manifest.

    console
    $ kubectl apply -f postgres.yaml
    

Enable Connection Pooling

pgBouncer multiplexes client connections onto a smaller pool of backend connections, which helps applications that open many short-lived connections.

  1. Open the cluster manifest.

    console
    $ nano postgres.yaml
    
  2. Add the proxy block under spec.

    yaml
    proxy:
      pgBouncer:
        replicas: 1
    

    Save and close the file.

  3. Apply the manifest.

    console
    $ kubectl apply -f postgres.yaml
    
  4. Verify that the pooler pod is running.

    console
    $ kubectl get pods -n postgres-operator -l postgres-operator.crunchydata.com/role=pgbouncer
    
  5. Confirm that the pooler connection details were added to the user Secret.

    console
    $ kubectl get secret hippo-pguser-hippo -n postgres-operator -o jsonpath='{.data}' | jq 'keys'
    

    The Secret gains pgbouncer-host, pgbouncer-port, pgbouncer-uri, and pgbouncer-jdbc-uri.

  6. Forward the pooler Service to connect through it.

    console
    $ kubectl -n postgres-operator port-forward svc/hippo-pgbouncer 5432:5432
    

Schedule Automatic Backups

A schedules block runs full and incremental backups on a cron expression instead of only on demand.

  1. Open the cluster manifest.

    console
    $ nano postgres.yaml
    
  2. Add a schedules block to the repository. Replace BUCKET-NAME and OBJECT-STORAGE-HOSTNAME with the same values used in the cluster manifest.

    yaml
    repos:
      - name: repo1
        schedules:
          full: "0 1 * * *"
          incremental: "0 */4 * * *"
        s3:
          bucket: "BUCKET-NAME"
          endpoint: "OBJECT-STORAGE-HOSTNAME"
          region: "us-east-1"
    

    Save and close the file.

    This schedule runs a full backup daily at 1 a.m. and an incremental backup every four hours.

  3. Apply the manifest.

    console
    $ kubectl apply -f postgres.yaml
    
  4. Verify that the operator created the backup CronJobs.

    console
    $ kubectl get cronjobs -n postgres-operator
    

Run a Manual Backup

A manual backup target defines the options PGO uses when you trigger a one-off backup with an annotation.

  1. Open the cluster manifest.

    console
    $ nano postgres.yaml
    
  2. Add a manual block under pgbackrest.

    yaml
    backups:
      pgbackrest:
        manual:
          repoName: repo1
          options:
            - --type=full
    

    Save and close the file.

  3. Apply the manifest.

    console
    $ kubectl apply -f postgres.yaml
    
  4. Annotate the cluster to trigger the backup.

    console
    $ kubectl annotate -n postgres-operator postgrescluster hippo --overwrite \
        postgres-operator.crunchydata.com/pgbackrest-backup="$(date)"
    
  5. Verify that the backup job completed.

    console
    $ kubectl get jobs -n postgres-operator
    

Add a Second Backup Repository

A second repository backed by a PersistentVolume keeps a local copy of backups alongside the Object Storage copy.

  1. Open the cluster manifest.

    console
    $ nano postgres.yaml
    
  2. Add a second entry to repos. Replace BUCKET-NAME and OBJECT-STORAGE-HOSTNAME with the same values used in the cluster manifest.

    yaml
    repos:
      - name: repo1
        s3:
          bucket: "BUCKET-NAME"
          endpoint: "OBJECT-STORAGE-HOSTNAME"
          region: "us-east-1"
      - name: repo2
        volume:
          volumeClaimSpec:
            accessModes:
              - "ReadWriteOnce"
            resources:
              requests:
                storage: 10Gi
    

    Save and close the file.

    Block Storage enforces a 10 GB minimum, so a smaller storage request leaves the claim unbound.

  3. Apply the manifest.

    console
    $ kubectl apply -f postgres.yaml
    
  4. Verify that the second repository's PersistentVolumeClaim bound.

    console
    $ kubectl get pvc -n postgres-operator
    

Troubleshoot the Cluster

The operator records reconciliation problems as Kubernetes events and cluster conditions, and pgBackRest writes its own logs inside the instance pod.

  1. Inspect the cluster resource and its events.

    console
    $ kubectl describe postgrescluster hippo -n postgres-operator
    
  2. List namespace events in chronological order.

    console
    $ kubectl get events -n postgres-operator --sort-by='.metadata.creationTimestamp'
    
  3. Review the operator log when a cluster never becomes ready.

    console
    $ kubectl logs -n postgres-operator deploy/pgo --tail=100
    
  4. Validate the pgBackRest configuration from inside the primary pod. Replace PRIMARY-POD-NAME with your primary pod name.

    console
    $ kubectl exec -n postgres-operator PRIMARY-POD-NAME -c database -- pgbackrest check --stanza=db
    

    The command produces no output and exits with status 0 when the repository and archive settings are valid. A misconfigured repository prints the failing check instead.

  5. Read the pgBackRest logs. Replace PRIMARY-POD-NAME with your primary pod name.

    console
    $ kubectl exec -n postgres-operator PRIMARY-POD-NAME -c database -- ls /pgdata/pgbackrest/log
    

Conclusion

You have successfully installed PostgreSQL in a Vultr Kubernetes Engine cluster using PGO, with data stored on Block Storage and write-ahead logs and backups archived to Object Storage. Extend the deployment by adding replicas for high availability, enabling connection pooling, or scheduling recurring backups. For more information, visit the official PGO documentation.

Comments