Postgres on Kubernetes Is No Longer a Dare
On this page
For years, “never run a database on Kubernetes” was the one piece of Kubernetes advice that survived every hype cycle. It was repeated in conference talks, codified in architecture reviews, and enforced by that one senior engineer who had once lost a volume in 2017 and never forgot it. And here is the thing: the advice was correct. At the time.
It is not 2017 anymore. The machinery that made stateful workloads dangerous on Kubernetes has been rebuilt piece by piece, and the community survey data from the Data on Kubernetes Community has shown a solid majority of organisations running databases in production on Kubernetes for years now. The interesting question is no longer whether you can, but what changed, what it actually takes to do well, and where the old advice still applies. Let us take those in order, with Postgres as the worked example, because Postgres is where this argument gets settled for most teams.
Where the rule came from
The original advice encoded three real problems. Storage was the first: before the Container Storage Interface went GA, volumes were handled by in-tree drivers of wildly varying quality, and a rescheduled pod could genuinely end up waiting on a disk that refused to detach from its old node. The second was the abstraction gap: a StatefulSet gives you stable names and ordered startup, but it knows nothing about replication, quorum, or which replica is safe to promote. All of that knowledge lived in a human. Which was the third problem: when the primary died at 2 a.m., Kubernetes would cheerfully restart the pod, but deciding whether to promote a replica, fence the old primary, and rewind the timeline was a page, a runbook, and a sweaty half hour.
So the rule was never really “Kubernetes cannot run databases”. It was “Kubernetes brings nothing to the parts of running a database that are actually hard”. Fair, in 2017.
What changed underneath
The storage story changed first. CSI shipped, matured, and became the only path; volume snapshots went GA; topology-aware provisioning meant volumes got created in the zone where the pod actually landed. Node-level storage on NVMe became a first-class, boring choice rather than a hack. More recently the pod machinery caught up too: Kubernetes 1.33 graduated sidecar containers to stable, which cleans up the log shippers and metrics exporters that database pods always accumulate, and in-place pod resource resize reached beta and is enabled by default, which means bumping memory on a database pod no longer forces the restart-and-failover dance it used to.
But infrastructure was never the interesting half. The reason the calculus flipped is the operator pattern, and specifically how good the database operators have become. Strimzi runs Kafka this way. Vitess scales MySQL this way. And for Postgres, CloudNativePG, created by EDB and now a CNCF project, is the clearest expression of the idea: take everything the DBA-on-call knew, and encode it in a controller.
The operator is the DBA
CloudNativePG is worth studying even if you never adopt it, because its design answers the old objections one by one. It does not even use StatefulSets: the operator manages instance pods directly, because it understands Postgres roles in a way a generic workload controller never could. Each instance runs streaming replication from the primary. When the primary fails, the operator handles what the runbook used to: it selects the most advanced replica, promotes it, rewires the rest, and fences the old primary so a split brain cannot corrupt the timeline. The 2 a.m. page becomes a morning Slack message that a failover happened.
The whole thing is declarative, so a production-grade cluster reads like this:
apiVersion: postgresql.cnpg.io/v1kind: Clustermetadata: name: app-dbspec: instances: 3 imageName: ghcr.io/cloudnative-pg/postgresql:17.5 storage: size: 200Gi storageClass: fast-nvme resources: requests: cpu: "4" memory: 16Gi limits: memory: 16Gi affinity: topologyKey: topology.kubernetes.io/zoneThree instances, spread across zones, on fast local storage. Point the cluster’s backup configuration at an object store and continuous archiving ships WAL segments there, scheduled base backups land next to them, and point-in-time recovery means “restore to 14:32, just before the bad migration” is a supported operation rather than a prayer. A kubectl cnpg status app-db shows you replication lag, backup freshness, and which instance is primary, in one screen.
None of this makes Postgres expertise obsolete. It changes where the expertise goes: into choosing the topology, the resource envelope, and the recovery targets, once, in a reviewed file, instead of into being awake at the right moment.
Storage: let Postgres do the replicating
The one design decision that separates good deployments from cautionary tales is refusing to replicate twice. A distributed storage layer that replicates every block three ways, underneath a Postgres cluster that is also synchronously replicating, gives you the write amplification of both and the performance of neither. Postgres already knows how to keep replicas consistent; it has been doing it for longer than Kubernetes has existed.
So give each instance plain, fast, locally-attached storage, spread the instances across failure domains with anti-affinity, and let streaming replication be the redundancy. This is also where the economics get loud: three bare-metal nodes with NVMe drives, of the kind we priced in the Hetzner cost breakdown, will outrun a managed database instance costing several times as much. The managed-database markup was always the price of not having a DBA encoded in software. Now you have one.
Two guardrails complete the picture. A PodDisruptionBudget stops a routine node drain from taking down the primary and its synchronous replica in the same breath, which matters most mid cluster upgrade. And resource requests on database pods should be honest rather than optimistic: a database that gets evicted under node pressure is a database that fails over more often than the hardware justifies.
A backup you have not restored is a rumour
The operator will archive WAL and take base backups with a few lines of configuration, and that is precisely why backup complacency is the modern failure mode. Configuration is not verification. The teams that survive data incidents are the ones that treat restore as a drill: on a schedule, recover the latest backup into a scratch namespace, point a throwaway application at it, and check that the data is there and current. With declarative recovery this is twenty lines of YAML and an hour of compute, which is the cheapest insurance in infrastructure. If you clone environments from production anyway, restore-from-backup is the natural way to seed the clone, and the drill stops being a separate chore at all.
Monitor the boring numbers: replication lag, WAL archive age, backup freshness, disk headroom. The operator exposes all of them to Prometheus, and our monitoring stack comparison covers where to put them.
When the old advice still wins
A mature take has to include the cases where you should still hand your database to a cloud provider, and they are real. If you are a three-person team with one production database and no Kubernetes muscle yet, a managed database is a perfectly defensible call, though the early-stage playbook makes the case that a platform with an operator in the stack has lowered that bar further than most founders assume. If a compliance regime makes “the provider handles patching” a load-bearing sentence in your audit story, that is worth actual money. And if your database is small enough that its bill is a rounding error, the migration is a hobby, not a project.
The calculus flips when any of these arrive: many environments that each need a database, a bill that has become a line item executives ask about, data residency requirements that price egress and sovereignty into every decision, or hardware you already own. At that point the operator does not just match the managed offering; it beats it, because every environment gets the same reviewed, versioned, restorable database instead of a hand-configured instance per team.
Running it as part of the stack, not beside it
The failure mode that remains in 2026 is not technical, it is organisational: the database becomes the one component that lives outside the system that manages everything else. Someone installs the operator by hand, the manifests live in a personal repo, and two years later the cluster migration stalls because nobody is sure how the database got there.
The fix is to treat the database like every other addon. In Ankra, CloudNativePG is a pinned-version node in a stack alongside your monitoring and ingress, the credentials it needs live encrypted in Git with SOPS rather than in a Slack thread, and the cluster definition rolls out with dependency ordering and a Git record, so the database exists because a reviewed file says so. That is the actual end state of this whole argument: not “databases on Kubernetes are possible”, but “the database is no longer special”. It is declared, versioned, monitored, and restorable, exactly like everything else you run. The senior engineer from 2017 was right about the risk. The system finally absorbed it.
Get started: Create a free account on Ankra.
Join our community: Slack
Follow us on: LinkedIn | GitHub
Contact us: [email protected]
Get the next post in your inbox
Related Posts
The Spec Sheet Is Not a Benchmark: Testing Kubernetes Clusters in 15 Minutes
Two identical cloud VMs measured 2.8x apart on CPU; one failed etcd's fsync bar. A 15-minute in-cluster benchmark suite catches this before production does.
AWS vs GCP vs Hetzner: Six Clouds, One Kubernetes Benchmark
We ran the same k6, iperf3 and fio suite on EKS, GKE and Hetzner and lined the results up against UpCloud, OVH and DigitalOcean. The cheapest node in the test won more than it had any right to.