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:
- A missing pod is recreated by the StatefulSet controller even when the operator itself is down, since Kubernetes keeps the pods alive.
- 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:



