PostgreSQL Integration
The Frappe operator supports PostgreSQL as a first-class database provider alongside MariaDB. Choose per-site with spec.dbConfig.provider: postgres.
Frappe version requirement. Frappe’s PostgreSQL support ships in the
developbranch (→ v17). The default stable v15 bench image does not support Postgres. You must run an operator-compatible Frappe develop/v17 bench image on any bench whose sites useprovider: postgres. See Building a Postgres bench image.
Modes
| Mode | What the operator does | Backing service |
|---|---|---|
shared (default) | Runs a pg-provision Job that CREATE ROLE + CREATE DATABASE on an existing PostgreSQL server | Any Postgres (a shared Percona cluster, a managed RDS/CloudSQL, etc.) |
dedicated | Provisions a per-site PerconaPGCluster (one Postgres instance per site) | Percona PostgreSQL Operator v2 |
Both modes honour spec.deletionPolicy:
Retain(default): the database, role, and credential Secret (shared) or the wholePerconaPGCluster(dedicated) are kept when theFrappeSiteis deleted. GitOps-safe — an accidental CR delete or an ArgoCD prune never drops tenant data.Delete: the operator runs apg-deleteJob (shared,DROP DATABASE/DROP ROLE) or deletes thePerconaPGCluster(dedicated).
Prerequisites
Install the Percona PostgreSQL Operator (needed for dedicated mode, and for the shared-cluster example below):
kubectl apply --server-side \
-f https://raw.githubusercontent.com/percona/percona-postgresql-operator/v2.3.1/deploy/bundle.yaml
CRDs only (e.g. for manifest validation without running the operator):
kubectl apply --server-side \
-f https://raw.githubusercontent.com/percona/percona-postgresql-operator/v2.3.1/deploy/crd.yaml
Shared mode
The operator provisions a database + role on an existing server via a Job. It needs superuser credentials to do so, supplied in a Secret with keys user and password:
apiVersion: v1
kind: Secret
metadata:
name: frappe-postgres-provisioner # default name; override with dbConfig.postgresRef.name
namespace: my-namespace
type: Opaque
stringData:
user: postgres
password: <superuser-password>
---
apiVersion: vyogo.tech/v1
kind: FrappeSite
metadata:
name: my-site
spec:
benchRef: { name: pg-bench }
siteName: my-site.example.com
dbConfig:
provider: postgres
mode: shared
# Optional. Defaults to host `frappe-postgres-pgbouncer` in the site
# namespace. When set, the host becomes `<name>-pgbouncer` and the
# provisioner Secret name becomes `<name>`.
postgresRef:
name: frappe-postgres
deletionPolicy: Retain
Host resolution:
- No
postgresRef→frappe-postgres-pgbouncer.<namespace>.svc.cluster.local:5432 postgresRef: {name: X, namespace: Y}→X-pgbouncer.Y.svc.cluster.local:5432
The per-site password is stored in Secret/<site>-db-password (no owner reference, so Retain survives site deletion).
Dedicated mode
The operator creates a complete, per-site PerconaPGCluster:
apiVersion: vyogo.tech/v1
kind: FrappeSite
metadata:
name: my-site
spec:
benchRef: { name: pg-bench }
siteName: my-site.example.com
dbConfig:
provider: postgres
mode: dedicated
storageSize: 5Gi # data + pgBackRest repo volume size (default 2Gi)
deletionPolicy: Delete
On PostgreSQL 15+, the public schema is locked to the database owner, and Percona seeds a per-user schema that would otherwise shadow public. Frappe requires public, so the operator exposes the Percona postgres superuser and runs a one-time, idempotent configure Job that hands the database to the app user (giving it public ownership) and sets its search_path to public. The site only reports Ready once both the cluster is ready and that Job succeeds.
The generated cluster is PerconaPGCluster/<site>-postgres with:
postgresVersion: 16, one instance (instance1)- a pgBouncer proxy (deployed for general use). Frappe itself connects to the direct
<site>-postgres-primaryservice, not pgBouncer: Percona’s pgBouncer defaults to transaction pooling, which breaksbench new-site/bench migrate(session-level DDL and prepared statements) - a PVC-backed pgBackRest repo (
repo1) so backups work out of the box - a role whose credentials the Percona operator writes to
Secret/<site>-postgres-pguser-<user>
The Percona CRD constrains the role name to a DNS label, so the operator uses a stable label-safe name (
u<hash>) derived from the site — distinct from the underscore-form database identifier used in shared mode.
Image overrides
Dedicated-cluster component images default to Percona v2.3.1 (PostgreSQL 16) and can be pinned/mirrored via Helm (or the equivalent operator env vars):
# values.yaml
postgres:
percona:
postgresImage: "myregistry/percona-postgresql-operator:2.3.1-ppg16-postgres"
pgBouncerImage: "myregistry/percona-postgresql-operator:2.3.1-ppg16-pgbouncer"
pgBackRestImage: "myregistry/percona-postgresql-operator:2.3.1-ppg16-pgbackrest"
Env vars: FRAPPE_PERCONA_POSTGRES_IMAGE, FRAPPE_PERCONA_PGBOUNCER_IMAGE, FRAPPE_PERCONA_PGBACKREST_IMAGE.
Building a Postgres bench image
provider: postgres needs a Frappe build from develop (→ v17). The frappe-erpnext-images-for-operator repo publishes an operator-compatible develop image; its develop base already ships psycopg2 + libpq + psql, so it is Postgres-capable out of the box. Build/publish it by running that repo’s container-build.yml workflow with frappe_version=develop (tag v17), then point the bench at it:
apiVersion: vyogo.tech/v1
kind: FrappeBench
metadata:
name: pg-bench
spec:
frappeVersion: "develop"
imageConfig:
repository: ghcr.io/vyogotech/erpnext-for-operator
tag: v17
pullPolicy: IfNotPresent
End-to-end example
A full both-modes manifest is in examples/kind-e2e-postgres-manifests.yaml.
Validation & selection summary
-
dbConfig.provider:mariadb(default)postgressqliteexternal postgresRefis only valid whenprovider: postgres;mariadbRefonly whenprovider: mariadb(the admission webhook rejects mismatches).dedicatedmode is supported formariadbandpostgresonly.- Switching an existing site’s provider is not supported — provider is fixed at creation.