In-cluster databases — one release per instance

Decision (maintainer, 2026-09-15): "the database should run in the cluster and get its own helm ==> we probably need separate node pool for this." This page is the platform pattern. Deployment specifics (which instance runs where, the commands on the Systemorph cluster) live in the deployment repositories — for the Systemorph estate, Memex docs/in-cluster-databases.md.

Why the status quo does not serve

Today What is wrong with it for a client instance
A database on the shared Azure Flexible Server (<pg-server>, one database per instance) The client's data sits on a server that also holds Systemorph's own databases and every other client's. It cannot move with the instance, cannot be handed over, and its sizing, backups and maintenance window are shared.
The chart's bundled Postgres (postgres.enabled in the portal chart: one pgvector pod, one PVC) It lives inside the portal release: a portal helm uninstall, an --atomic rollback, or a re-render that flips the flag takes the database with it. One pod, no standby, no backup. It is right for self-host and local k3s, and wrong for anything with data someone depends on.

The options compared

Split the chart's own memex-postgres into a separate release CloudNativePG (operator + one Cluster per instance)
Lifecycle apart from the portal yes yes
Standby in a second zone, failover no — a StatefulSet of one; replication and promotion would have to be written yes — streaming replication, automated failover, switchover on update
Backups none — would have to be written (CronJob + pg_dump, no WAL, no point-in-time) WAL archiving + base backups to Blob through the Barman Cloud plugin, point-in-time recovery
pgvector the pgvector/pgvector image the operand's standard flavour carries pgvector
Credentials a password the chart must be given generated in-cluster into <release>-app; nothing to store in a vault
Cost to us writing and owning HA, backup and restore installing and upgrading one operator per cluster

Recommendation: CloudNativePG. The maintainer's shape — two nodes, one per zone, primary and standby — is replication with failover, and the only way the chart's own StatefulSet gets there is by re-implementing what CloudNativePG already is. A thin per-instance chart over a Cluster gives the separate release, and the operator gives HA, backups and generated credentials.

The shape

cluster  ── db node pool ─────── Standard_E4ds_v5 × 2, zones 1+2, fixed, workload=db:NoSchedule
         ── platform (once) ──── CloudNativePG operator · Barman Cloud plugin · StorageClass memex-db-premiumv2
namespace <id>
         ── release <id>-db ──── memex-db chart → Cluster <id>-db (primary + standby)
                                  → Services <id>-db-rw / -ro / -r, Secret <id>-db-app
         ── release <id> ─────── memex portal chart, database.release: <id>-db

Backups and restore

With backup.enabled the Cluster archives WAL continuously and takes a daily base backup to <destinationPath>/<namespace> through the Barman Cloud plugin, as the pods' workload identity (no storage key in the cluster). Recovery is a new Cluster bootstrapped from that object store, optionally to a point in time — CloudNativePG's bootstrap.recovery.

🚨 Not wired yet: the Hosting plugin's Backup, Restore, Suspend and Teardown-with-backup actions still take a pg_dump from a Flexible Server and refuse an in-cluster record rather than dump the wrong host. Their in-cluster form (a CloudNativePG Backup object, a recovery bootstrap) is the follow-up before an instance with data depends on this path.

Moving an existing instance

  1. Create the database release (the Provision's Deploy database release step) — an empty Cluster.
  2. Stop the portal (replicas: 0), pg_dump the Flexible Server database, pg_restore into <release>-rw as the owner.
  3. Switch the record to inClusterDatabase, drop databaseServer/databaseHost and the db-connection vault mapping, and Reconcile.
  4. Keep the Flexible Server database until the instance has run on the new one; then drop it.

Open decisions

Reconnecting…
The connection to the server was interrupted. Trying to restore it…
Trying again…
The connection could not be restored. Reloading the page…
The server was updated. Reloading the page to pick up the latest version.