Minikube, one operator, 14 lines of YAML

Leader election, streaming replicas, automatic failover, health checks, careful wiring — that is the usual bill of materials for "highly available PostgreSQL." The CYBERTEC PG Operator (CPO) compresses it into a 14-line YAML file.

1

2

3

4

5

6

7

8

minikube start -p cpo-deploy --driver=docker --cpus=2 --memory=4096

helm repo add cpo https://cybertec-postgresql.github.io/CYBERTEC-operator-tutorials

helm repo update cpo

kubectl create namespace cpo

helm install cpo cpo/postgres-operator -n cpo --version 0.9.2 \

  --set configKubernetes.enable_pod_antiaffinity=false

kubectl -n cpo rollout status deploy/postgres-operator

One flag trips people up here: enable_pod_antiaffinity=false. Production clusters benefit from the operator's default behaviour of spreading replicas over separate Kubernetes nodes. Minikube, though, is a single node, and leaving anti-affinity on would park the replica in Pending indefinitely. Keep it enabled on any real multi-node cluster.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

kubectl -n cpo apply -f - <<'EOF'

apiVersion: cpo.opensource.cybertec.at/v1

kind: postgresql

metadata:

  name: pg-cluster

spec:

  dockerImage: 'containers.cybertec.at/cybertec-pg-container/postgres:rocky9-18.4-1'

  numberOfInstances: 2

  postgresql:

    version: '18'

  resources:

    requests: { cpu: 250m, memory: 1Gi }

    limits:   { cpu: '1',  memory: 1Gi }

  teamId: acid

  volume:

    size: 1Gi

EOF

numberOfInstances: 2 is the whole HA story: one leader, one streaming replica. Wait for them:

bash

kubectl -n cpo wait --for=condition=Ready pod \

  -l cluster.cpo.opensource.cybertec.at/name=pg-cluster --timeout=360s

# pod/pg-cluster-0 condition met

# pod/pg-cluster-1 condition met

Checking with Patroni

Every database pod runs Patroni inside it, and Patroni is the thing to ask:

1

2

3

4

5

6

7

8

9

10

kubectl -n cpo exec pg-cluster-0 -- patronictl list

+ Cluster: pg-cluster (...) -----------------------+----+-------------+-----+------------+-----+

| Member       | Host        | Role    | State     | TL | Receive LSN | Lag | Replay LSN | Lag |

+--------------+-------------+---------+-----------+----+-------------+-----+------------+-----+

| pg-cluster-0 | 10.244.0.32 | Leader  | running   |  1 |             |     |            |     |

| pg-cluster-1 | 10.244.0.33 | Replica | streaming |  1 |   0/3000060 |   0 |  0/3000060 |   0 |

+--------------+-------------+---------+-----------+----+-------------+-----+------------+-----+

Zero lag between the streaming replica and its leader. Replication can be verified directly — writes on the leader, reads on the replica:

1

2

3

4

5

6

7

8

kubectl -n cpo exec pg-cluster-0 -- psql -U postgres \

  -c "create table t(id int); insert into t values (1),(2),(3);"

# CREATE TABLE / INSERT 0 3

kubectl -n cpo exec pg-cluster-1 -- psql -U postgres -c 'select count() from t;'

#  count

# -------

#      3

Writes on the replica are rejected, which is what a read-only standby owes you:

1

2

kubectl -n cpo exec pg-cluster-1 -- psql -U postgres -c 'insert into t values (4);'

# ERROR:  cannot execute INSERT in a read-only transaction

Where the reliability actually lives

The setup is production-shaped rather than a toy for two reasons:

  1. A missing pod is recreated by the StatefulSet controller even when the operator itself is down, since Kubernetes keeps the pods alive.
  2. Leader election, failover and promotion are Patroni's job inside the pods, and they continue while the operator is offline. Changing the cluster is what the operator is for; keeping it running is not.

Resizing, scaling and downtime-free upgrades come next, and multi-site stretching across regions after that. The repository is on GitHub: https://github.com/cybertec-postgresql/CYBERTEC-pg-operator

And a cluster and an operator were created — ZSH: