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 & Engines
| Mode | Engine | What the operator does | Backing operator |
|---|---|---|---|
shared (default) | Agnostic | Runs a pg-provision Job that CREATE ROLE + CREATE DATABASE on an existing PostgreSQL server | Any Postgres (StackGres, Percona, CloudNativePG, RDS, CloudSQL, etc.) |
dedicated | stackgres (default) | Provisions a per-site SGCluster with shared configuration and sizing profiles | StackGres Operator |
dedicated | percona (toggle) | 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 whole database cluster (dedicated) are kept when theFrappeSiteis deleted. GitOps-safe — an accidental CR delete or an ArgoCD prune never drops tenant data.Delete: in shared mode the operator runs apg-deleteJob (DROP DATABASE/DROP ROLE) and the site’s database is removed.
Dedicated clusters are never deleted automatically, in either policy. For safety, deleting a
FrappeSitedoes not tear down itsSGClusterorPerconaPGCluster, the generated database Secrets, or the underlying PVCs —deletionPolicy: Deletedoes not change this. The dedicated cluster CRs are created without anownerReferenceprecisely so that nothing garbage-collects them. Removing a dedicated database is a deliberate manual step:# StackGres kubectl delete sgcluster <site>-postgres -n <namespace> kubectl delete sgscript <site>-postgres-script -n <namespace> kubectl delete secret <site>-postgres-script-secret <site>-db-password -n <namespace> # Percona kubectl delete perconapgcluster <site>-postgres -n <namespace> kubectl delete secret <site>-db-password -n <namespace>Check for leftover PVCs afterwards (
kubectl get pvc -n <namespace>); they are retained too and are what actually holds the data.
Dedicated Engine Toggle (postgresEngine)
Choose the dedicated PostgreSQL operator via spec.dbConfig.postgresEngine:
stackgres(default for new dedicated sites): provisions via StackGres (SGCluster). Certified on OpenShift.percona: provisions via Percona (PerconaPGCluster).
spec:
dbConfig:
provider: postgres
mode: dedicated
postgresEngine: stackgres # "stackgres" (default) or "percona"
Backward Compatibility Guarantee: Existing dedicated-mode sites created before
postgresEngineexisted probe for<site>-postgresPerconaPGCluster. If found, they continue managing the Percona cluster indefinitely. Only new dedicated sites default to StackGres. Once provisioned,dbConfig.postgresEngineis immutable.
Prerequisites
Option A: StackGres (Default)
Install StackGres via OpenShift OperatorHub (stackgres-community package from community-operators) or upstream Helm chart:
helm repo add stackgres https://stackgres.io/downloads/stackgres-k8s/stackgres/helm
helm repo update
helm install stackgres-operator stackgres/stackgres-operator -n stackgres --create-namespace
Or enable INSTALL_STACKGRES=true ./install.sh.
Option B: Percona (Optional)
Install the Percona PostgreSQL Operator:
kubectl apply --server-side \
-f https://raw.githubusercontent.com/percona/percona-postgresql-operator/v2.3.1/deploy/bundle.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
Dedicated mode provisions a full, isolated PostgreSQL cluster per site.
StackGres (Default)
The operator creates a per-site SGCluster alongside shared configuration profiles:
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
postgresEngine: stackgres # default for new sites; can be omitted
storageSize: 5Gi # persistent volume size (default 2Gi)
resources: # optional: generates bespoke <site>-postgres-profile
requests:
cpu: "1"
memory: 2Gi
deletionPolicy: Delete
The generated StackGres resources:
SGCluster/<site>-postgres: runs PostgreSQL 16 with PgBouncer sidecar pooling.SGPostgresConfig/frappe-postgres-dedicated-defaults: shared namespace configuration (postgresql.conf).SGPoolingConfig/frappe-postgres-dedicated-pooling: shared namespace configuration (pgbouncer.iniin transaction pooling mode).SGInstanceProfile/frappe-postgres-dedicated-default(or<site>-postgres-profileif custom resources are set).SGScript/<site>-postgres-script: declarative database, role, and schema permissions script executed by StackGres controller.Secret/<site>-postgres-script-secret: script contents mounted bySGScript.- Credentials stored in
Secret/<site>-db-password. - Note: Site-level backups via
SiteBackup(bench backup) are fully supported. Cluster-level continuous PITR viaSGObjectStorageis an optional upcoming enhancement.
Percona (Toggle)
To provision via Percona Operator instead:
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
postgresEngine: percona
storageSize: 5Gi
deletionPolicy: Delete
The generated cluster is PerconaPGCluster/<site>-postgres with:
postgresVersion: 16, one instance (instance1)- Frappe connects to the direct
<site>-postgres-primaryservice - a PVC-backed pgBackRest repo (
repo1) for cluster-level PITR - credentials stored in
Secret/<site>-postgres-pguser-<user>
Schema Ownership & Initialization
On PostgreSQL 15+, the public schema is locked to the database owner. Frappe requires public access:
- StackGres: The operator declaratively provisions user creation, database creation, and
ALTER SCHEMA public OWNER TO ...using nativeSGScriptandspec.managedSql.scripts. The site checks script completion status before reportingDatabaseReady. - Percona: The operator connects as the cluster superuser and runs a one-time idempotent configure Job that hands the database to the app user and sets its
search_pathtopublic.
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 for Percona. StackGres uses standard database user identifiers matching the site name.
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.