Skip to content

PostgreSQL

Production-ready PostgreSQL deployment with support for standalone and streaming replication architectures, structured access controls, TLS, External Secrets Operator, optional replication slots, and production hardening controls.

Key Features

  • Standalone and replication — Single instance or primary with streaming replicas
  • Automatic initialization — Init scripts, extensions, and custom configuration
  • S3 backup — Scheduled backups to S3-compatible storage via CronJob
  • Metrics — Prometheus exporter with ServiceMonitor
  • Security — Non-root containers, TLS support with private-key permission normalization, structured pg_hba.conf CIDRs, NetworkPolicy controls, and ServiceAccount token control
  • Replication slots — Optional physical slots with WAL retention limits for safer replica catch-up behavior
  • External Secrets Operator — Optional external-secrets.io/v1 resources for auth, TLS, and backup credentials
  • Persistent storage — Configurable PVCs with storage class selection

Security Scan

Framework Score
MITRE + NSA + SOC2 93%

Security posture is strong. Remaining findings are documented product exceptions for opt-in NetworkPolicy and PostgreSQL’s writable runtime filesystem.

Architecture

Standalone

Single PostgreSQL instance with persistent storage and optional S3 backup.

ApplicationclientTCP:5432PostgreSQLStatefulSet (1 pod)primaryPVC (data)Backup CronJobpg_dump → S3

Replication

Primary with streaming replicas, read-only service, and backup CronJob.

Applicationread/writePrimaryread + writeStatefulSet pod-0PVC (data)WALReplicaread-onlyStatefulSet pod-1PVC (data)Read Clientread-onlysvc-readload-balancedBackup CronJobpg_dump → S3

Installation

HTTPS repository:

helm repo add helmforge https://repo.helmforge.dev
helm repo update
helm install my-pg helmforge/postgresql

OCI registry:

helm install my-pg oci://ghcr.io/helmforgedev/helm/postgresql

Deployment Examples

# values.yaml
architecture: standalone

auth:
  postgresPassword: 'my-secret-password'
  database: myapp
  username: myuser
  password: 'user-password'

standalone:
  persistence:
    enabled: true
    size: 20Gi

metrics:
  enabled: true
  serviceMonitor:
    enabled: true
# values.yaml
architecture: replication

auth:
  postgresPassword: 'my-secret-password'
  database: myapp
  username: myuser
  password: 'user-password'
  replicationPassword: 'repl-password'

replication:
  primary:
    persistence:
      enabled: true
      size: 20Gi
  readReplicas:
    replicaCount: 2
    persistence:
      enabled: true
      size: 20Gi
# values.yaml
architecture: standalone

auth:
  postgresPassword: 'my-secret-password'
  database: myapp

standalone:
  persistence:
    enabled: true
    size: 20Gi

backup:
  enabled: true
  schedule: '0 3 * * *'
  s3:
    endpoint: https://s3.amazonaws.com
    bucket: my-pg-backups
    accessKey: '<set-me>'
    secretKey: '<set-me>'
# values.yaml
architecture: standalone

auth:
  postgresPassword: 'my-secret-password'
  database: myapp

standalone:
  persistence:
    enabled: true
    size: 20Gi

tls:
  enabled: true
  existingSecret: postgresql-tls
  sslMode: require
  volumePermissions:
    enabled: true
# values.yaml - production baseline with replication, TLS, slots, backup, and policies
architecture: replication

auth:
  existingSecret: postgresql-auth
  database: app
  username: app

config:
  localAuthMethod: scram-sha-256
  allowedClientCIDRs:
    - 10.42.0.0/16
  allowedReplicationCIDRs:
    - 10.42.0.0/16

replication:
  primary:
    persistence:
      size: 100Gi
    resources:
      requests:
        cpu: 500m
        memory: 1Gi
      limits:
        cpu: '2'
        memory: 2Gi
  readReplicas:
    replicaCount: 2
    persistence:
      size: 100Gi
  wal:
    keepSize: 2GB
    maxReplicationSlots: 10
    maxSlotWalKeepSize: 8GB
    walSenderTimeout: 60s
  slots:
    enabled: true

tls:
  enabled: true
  existingSecret: postgresql-tls
  sslMode: require
  volumePermissions:
    enabled: true

metrics:
  enabled: true
  serviceMonitor:
    enabled: true

networkPolicy:
  enabled: true
  egress:
    enabled: true
    allowDNS: true
    allowSameNamespacePostgreSQL: true
    allowHTTPS: true

serviceAccount:
  create: true
  automountServiceAccountToken: false

backup:
  enabled: true
  s3:
    endpoint: https://s3.example.com
    bucket: postgresql-backups
    existingSecret: postgresql-backup
# values.yaml - render ExternalSecret resources for existing ESO clusters
auth:
  existingSecret: postgresql-auth

tls:
  enabled: true
  existingSecret: postgresql-tls

backup:
  enabled: true
  s3:
    existingSecret: postgresql-backup

externalSecrets:
  enabled: true
  apiVersion: external-secrets.io/v1
  secretStoreRef:
    name: platform-secrets
    kind: ClusterSecretStore
  auth:
    enabled: true
    targetName: postgresql-auth
    postgresPasswordRemoteRef:
      key: prod/postgresql
      property: postgres-password
    userPasswordRemoteRef:
      key: prod/postgresql
      property: user-password
    replicationPasswordRemoteRef:
      key: prod/postgresql
      property: replication-password
  tls:
    enabled: true
    targetName: postgresql-tls
    certRemoteRef:
      key: prod/postgresql-tls
      property: tls.crt
    keyRemoteRef:
      key: prod/postgresql-tls
      property: tls.key
    caRemoteRef:
      key: prod/postgresql-tls
      property: ca.crt
  backup:
    enabled: true
    targetName: postgresql-backup
    accessKeyRemoteRef:
      key: prod/postgresql-backup
      property: access-key
    secretKeyRemoteRef:
      key: prod/postgresql-backup
      property: secret-key
External Secrets Operator is optional

The chart does not install External Secrets Operator and does not create a SecretStore or ClusterSecretStore. Enable this only when the operator and referenced store already exist.

Production Path

The default install remains intentionally suitable for local development and disposable test clusters. A production deployment should explicitly set existing Secrets or External Secrets, storage, resources, config.allowedClientCIDRs, config.allowedReplicationCIDRs, TLS, metrics, backup, and NetworkPolicy behavior.

Use the structured CIDR lists first. config.pgHbaEntries and raw config.pgHba remain escape hatches for advanced cases, but production baselines should not depend on a full raw pg_hba.conf override.

Replication slots are disabled by default for compatibility. If replication.slots.enabled=true, also set WAL limits such as replication.wal.maxSlotWalKeepSize so a stalled replica cannot retain unbounded WAL.

Operational Notes

  • The chart keeps internal health checks, metrics, and built-in backup on PostgreSQL’s standard postgres database.
  • If a reused PVC contains a valid data directory but is missing the default postgres database, the primary pod repairs that database during startup before normal readiness checks rely on it.
  • auth.database remains the application bootstrap database. It is created only on first initialization of a fresh data directory.
  • Set initdb.runDefaultScript=false when custom or externally managed init scripts should run without the chart-generated application and replication user bootstrap script.
  • docker-entrypoint-initdb.d scripts only run when the data directory is empty. Existing PVCs keep their current roles and databases during upgrades.

Dual-stack Networking

PostgreSQL Services accept Kubernetes dual-stack configuration. By default, both service.ipFamilyPolicy and service.ipFamilies are unset and the chart inherits whatever the cluster advertises (matching prior behavior). Setting them propagates to every chart-managed Service: client, primary, replicas, metrics, and the headless StatefulSet services.

service:
  ipFamilyPolicy: PreferDualStack
  ipFamilies:
    - IPv4
    - IPv6

PreferDualStack is the safer choice for clusters that may be single- or dual-stack: omit ipFamilies and the cluster auto-populates whatever it supports. Set ipFamilies explicitly only when the cluster is configured for both families — the Kubernetes API rejects an explicit family the cluster does not advertise. RequireDualStack enforces both.

Configuration Reference

This reference covers the complete values.yaml surface for the PostgreSQL chart.

Core

Parameter Type Default Description
architecture string standalone Deployment architecture: standalone or replication.
nameOverride string "" Override the chart name.
fullnameOverride string "" Override the full release name.
commonLabels object {} Extra labels added to all rendered resources.
clusterDomain string cluster.local Kubernetes cluster domain used in generated DNS names.

Image

Parameter Type Default Description
image.repository string docker.io/library/postgres PostgreSQL runtime image repository.
image.tag string "18.4-trixie" PostgreSQL runtime image tag.
image.pullPolicy string IfNotPresent Image pull policy for PostgreSQL containers.
imagePullSecrets array [] Optional image pull secrets for private registries.

Authentication

Parameter Type Default Description
auth.postgresPassword string "" PostgreSQL superuser password when not using an existing secret.
auth.database string app Application database created on first bootstrap.
auth.username string app Application username created on first bootstrap.
auth.password string "" Application user password when not using an existing secret.
auth.replicationUsername string replicator Replication username used by read replicas.
auth.replicationPassword string "" Replication password when not using an existing secret.
auth.existingSecret string "" Existing secret containing PostgreSQL passwords.
auth.existingSecretPostgresPasswordKey string postgres-password Secret key holding the PostgreSQL superuser password.
auth.existingSecretUserPasswordKey string user-password Secret key holding the application user password.
auth.existingSecretReplicationPasswordKey string replication-password Secret key holding the replication user password.

PostgreSQL Configuration

Parameter Type Default Description
config.localAuthMethod string scram-sha-256 Authentication method for local Unix-socket connections.
config.allowedClientCIDRs array ["0.0.0.0/0", "::/0"] CIDRs allowed to connect to regular PostgreSQL databases.
config.allowedReplicationCIDRs array ["0.0.0.0/0", "::/0"] CIDRs allowed to connect to the replication pseudo-database.
config.preset string none Optional PostgreSQL tuning preset: none, small, medium, or large.
config.postgresql string "" Raw postgresql.conf content appended after generated settings.
config.pgHbaEntries array [] Structured pg_hba.conf entries appended after generated defaults.
config.pgHba string "" Raw pg_hba.conf content appended after generated rules.

TLS

Parameter Type Default Description
tls.enabled boolean false Enable TLS for PostgreSQL and internal clients.
tls.existingSecret string "" Existing secret containing server TLS material.
tls.certFilename string tls.crt Certificate filename inside the TLS secret.
tls.keyFilename string tls.key Private key filename inside the TLS secret.
tls.caFilename string ca.crt CA certificate filename inside the TLS secret.
tls.sslMode string require libpq SSL mode used by internal probes, replication, and exporter.
tls.minProtocolVersion string TLSv1.2 Minimum accepted PostgreSQL TLS protocol version.
tls.volumePermissions.enabled boolean false Copy TLS material to an owned emptyDir and restrict the private key to 0600.

Initialization

Parameter Type Default Description
initdb.runDefaultScript boolean true Run the chart-generated app database, app user, and replication user script.
initdb.scripts object {} Additional scripts written into docker-entrypoint-initdb.d.
initdb.existingConfigMap string "" Existing ConfigMap mounted into docker-entrypoint-initdb.d.

Standalone

Parameter Type Default Description
standalone.resourcesPreset string small Optional resource preset for standalone mode.
standalone.persistence.enabled boolean true Enable a PVC for standalone mode.
standalone.persistence.storageClass string "" StorageClass for the standalone PVC.
standalone.persistence.accessModes array ["ReadWriteOnce"] Access modes for the standalone PVC.
standalone.persistence.size string 8Gi PVC size for standalone mode.
standalone.resources object {} Explicit resource requests and limits for standalone mode.

Replication - Primary

Parameter Type Default Description
replication.primary.resourcesPreset string small Optional resource preset for the primary pod.
replication.primary.persistence.enabled boolean true Enable PVCs for the primary StatefulSet.
replication.primary.persistence.storageClass string "" StorageClass for primary PVCs.
replication.primary.persistence.accessModes array ["ReadWriteOnce"] Access modes for primary PVCs.
replication.primary.persistence.size string 20Gi PVC size for the primary pod.
replication.primary.resources object {} Explicit resource requests and limits for the primary pod.
replication.primary.probes.requireWritable boolean true In replication mode, require primary readiness to confirm the pod is not in recovery mode.

Replication - Read Replicas

Parameter Type Default Description
replication.readReplicas.resourcesPreset string small Optional resource preset for replica pods.
replication.readReplicas.replicaCount integer 2 Number of asynchronous read replica pods.
replication.readReplicas.persistence.enabled boolean true Enable PVCs for replica StatefulSets.
replication.readReplicas.persistence.storageClass string "" StorageClass for replica PVCs.
replication.readReplicas.persistence.accessModes array ["ReadWriteOnce"] Access modes for replica PVCs.
replication.readReplicas.persistence.size string 20Gi PVC size for each replica.
replication.readReplicas.resources object {} Explicit resource requests and limits for replica pods.
replication.readReplicas.probes.requireRecoveryMode boolean true In replication mode, require replica readiness to confirm the pod is in recovery mode.

Replication - WAL, PDB, and Scheduling

Parameter Type Default Description
replication.wal.keepSize string 512MB WAL retention kept locally for replicas and recovery workflows.
replication.wal.maxSenders integer 10 Maximum WAL sender processes.
replication.wal.maxReplicationSlots integer 10 Maximum replication slots.
replication.wal.maxSlotWalKeepSize string "" Maximum WAL retained by replication slots before invalidation.
replication.wal.idleReplicationSlotTimeout string "" Timeout for inactive replication slots before invalidation.
replication.slots.enabled boolean false Use deterministic physical replication slots for read replicas.
replication.slots.namePrefix string replica Prefix used to build per-replica slot names.
replication.pdb.enabled boolean true Enable a PodDisruptionBudget by default in replication mode.
replication.pdb.minAvailable integer 1 Minimum available pods for the replication PDB.
replication.pdb.maxUnavailable string "" Maximum unavailable pods for the replication PDB.
replication.scheduling.enableDefaultPodAntiAffinity boolean true Enable opinionated anti-affinity defaults in replication mode.
replication.scheduling.enableDefaultTopologySpread boolean true Enable opinionated topology spread defaults in replication mode.
replication.scheduling.topologyKey string kubernetes.io/hostname Topology key used by the default replication spread rules.

Service

Parameter Type Default Description
service.type string ClusterIP Service type for PostgreSQL client access.
service.annotations object {} Annotations applied to the client Service.
service.primaryAnnotations object {} Annotations applied to the dedicated primary Service.
service.replicasAnnotations object {} Annotations applied to the dedicated replicas Service.
service.port integer 5432 PostgreSQL listener port.
service.metricsPort integer 9187 Metrics port exposed when metrics are enabled.
service.ipFamilyPolicy string omitted Service IP family policy: SingleStack, PreferDualStack, or RequireDualStack. Omit for cluster default.
service.ipFamilies array omitted Ordered list of IP families (IPv4, IPv6). Omit for cluster default.

Metrics

Parameter Type Default Description
metrics.enabled boolean false Enable the postgres_exporter sidecar.
metrics.resourcesPreset string small Optional resource preset for postgres_exporter.
metrics.service.annotations object {} Annotations applied to metrics Services.
metrics.image.repository string quay.io/prometheuscommunity/postgres-exporter Metrics sidecar image repository.
metrics.image.tag string "v0.18.0" Metrics sidecar image tag.
metrics.image.pullPolicy string IfNotPresent Image pull policy for postgres_exporter.
metrics.resources object {} Explicit resource requests and limits for postgres_exporter.
metrics.serviceMonitor.enabled boolean false Create a ServiceMonitor for Prometheus Operator.
metrics.serviceMonitor.interval string 30s Scrape interval for the ServiceMonitor.
metrics.serviceMonitor.labels object {} Extra labels applied to the ServiceMonitor.

Backup

Parameter Type Default Description
backup.enabled boolean false Enable the built-in backup CronJob.
backup.schedule string "0 3 * * *" Backup schedule in cron format.
backup.suspend boolean false Suspend backup execution.
backup.concurrencyPolicy string Forbid CronJob concurrency policy.
backup.successfulJobsHistoryLimit integer 3 Successful backup Jobs retained by the CronJob.
backup.failedJobsHistoryLimit integer 3 Failed backup Jobs retained by the CronJob.
backup.backoffLimit integer 1 Job backoff limit for backup execution.
backup.archivePrefix string postgresql Prefix used in generated backup archive names.
backup.resources object requests 100m/128Mi, limits 500m/512Mi Resources applied to backup dump and upload containers.
backup.images.uploader.repository string docker.io/helmforge/mc Uploader image repository for S3 copies.
backup.images.uploader.tag string "1.0.0" Uploader image tag.
backup.images.uploader.pullPolicy string IfNotPresent Pull policy for the uploader image.
backup.images.postgresql.repository string docker.io/library/postgres PostgreSQL client image used for backup jobs.
backup.images.postgresql.tag string "18.4-trixie" PostgreSQL client image tag used for backup jobs.
backup.images.postgresql.pullPolicy string IfNotPresent Pull policy for the PostgreSQL backup image.
backup.s3.endpoint string "" S3-compatible endpoint URL.
backup.s3.bucket string "" Target bucket name.
backup.s3.prefix string postgresql Optional key prefix inside the bucket.
backup.s3.createBucketIfNotExists boolean true Create the bucket automatically when it does not exist.
backup.s3.existingSecret string "" Existing secret containing backup access and secret keys.
backup.s3.existingSecretAccessKeyKey string access-key Secret key name for the S3 access key.
backup.s3.existingSecretSecretKeyKey string secret-key Secret key name for the S3 secret key.
backup.s3.accessKey string "" Inline S3 access key when not using an existing secret.
backup.s3.secretKey string "" Inline S3 secret key when not using an existing secret.
backup.database.pgDumpAllArgs string "--clean --if-exists" Extra arguments passed to pg_dumpall.

NetworkPolicy

Parameter Type Default Description
networkPolicy.enabled boolean false Create a NetworkPolicy for PostgreSQL pods.
networkPolicy.ingress.allowSameNamespace boolean true Allow ingress from the same namespace.
networkPolicy.ingress.extraFrom array [] Additional ingress from rules appended to the generated policy.
networkPolicy.egress.enabled boolean false Add explicit egress rules to the NetworkPolicy.
networkPolicy.egress.allowDNS boolean true Allow DNS egress on TCP/UDP 53.
networkPolicy.egress.allowSameNamespacePostgreSQL boolean true Allow PostgreSQL egress to same-namespace pods.
networkPolicy.egress.allowHTTPS boolean true Allow HTTPS egress for S3-compatible backup endpoints.
networkPolicy.egress.extraTo array [] Additional egress to rules appended to the policy.

Pod Disruption Budget

Parameter Type Default Description
pdb.enabled boolean false Enable a PodDisruptionBudget outside replication defaults.
pdb.minAvailable integer 1 Minimum available pods when the PDB is enabled.
pdb.maxUnavailable string "" Maximum unavailable pods when the PDB is enabled.

Service Account

Parameter Type Default Description
serviceAccount.create boolean false Create a dedicated ServiceAccount.
serviceAccount.name string "" Override the ServiceAccount name.
serviceAccount.annotations object {} Annotations applied to the ServiceAccount.
serviceAccount.automountServiceAccountToken boolean false Mount Kubernetes API credentials into workload and backup pods.

Probes

Parameter Type Default Description
livenessProbe.enabled boolean true Enable the liveness probe.
livenessProbe.initialDelaySeconds integer 60 Liveness probe initial delay.
livenessProbe.periodSeconds integer 20 Liveness probe period.
livenessProbe.timeoutSeconds integer 5 Liveness probe timeout.
livenessProbe.failureThreshold integer 6 Liveness probe failure threshold.
readinessProbe.enabled boolean true Enable the readiness probe.
readinessProbe.initialDelaySeconds integer 20 Readiness probe initial delay.
readinessProbe.periodSeconds integer 10 Readiness probe period.
readinessProbe.timeoutSeconds integer 5 Readiness probe timeout.
readinessProbe.failureThreshold integer 6 Readiness probe failure threshold.
startupProbe.enabled boolean true Enable the startup probe.
startupProbe.initialDelaySeconds integer 10 Startup probe initial delay.
startupProbe.periodSeconds integer 10 Startup probe period.
startupProbe.timeoutSeconds integer 5 Startup probe timeout.
startupProbe.failureThreshold integer 60 Startup probe failure threshold.

Scheduling and Security

Parameter Type Default Description
podSecurityContext.fsGroup integer 999 Filesystem group applied to PostgreSQL pods.
podSecurityContext.fsGroupChangePolicy string OnRootMismatch Filesystem group change policy.
podSecurityContext.seccompProfile.type string RuntimeDefault Seccomp profile applied at pod level.
securityContext.runAsUser integer 999 Runtime UID for PostgreSQL containers.
securityContext.runAsGroup integer 999 Runtime GID for PostgreSQL containers.
securityContext.runAsNonRoot boolean true Require containers to run as non-root.
securityContext.allowPrivilegeEscalation boolean false Disable privilege escalation.
securityContext.readOnlyRootFilesystem boolean false Keep the root filesystem writable for PostgreSQL runtime needs.
securityContext.capabilities.drop array ["ALL"] Linux capabilities dropped from PostgreSQL containers.
podLabels object {} Extra labels applied to PostgreSQL pods.
podAnnotations object {} Extra annotations applied to PostgreSQL pods.
annotations object {} Extra annotations applied to chart-managed resources that support them.
nodeSelector object {} Node selector applied to PostgreSQL pods.
tolerations array [] Tolerations applied to PostgreSQL pods.
affinity object {} Explicit affinity rules.
topologySpreadConstraints array [] Explicit topology spread constraints.
priorityClassName string "" PriorityClass applied to PostgreSQL pods.
terminationGracePeriodSeconds integer 120 Termination grace period for PostgreSQL pods.

Extra

Parameter Type Default Description
extraEnv array [] Extra environment variables injected into PostgreSQL containers.
extraVolumes array [] Extra volumes appended to PostgreSQL pods.
extraVolumeMounts array [] Extra volume mounts appended to PostgreSQL containers.

External Secrets Operator

Parameter Type Default Description
externalSecrets.enabled boolean false Render ExternalSecret resources.
externalSecrets.apiVersion string external-secrets.io/v1 ExternalSecret API version.
externalSecrets.refreshInterval string 1h Refresh interval used by rendered ExternalSecrets.
externalSecrets.secretStoreRef.name string "" Existing SecretStore or ClusterSecretStore name.
externalSecrets.auth.enabled boolean false Manage the PostgreSQL auth Secret through ESO.
externalSecrets.tls.enabled boolean false Manage the TLS Secret through ESO.
externalSecrets.backup.enabled boolean false Manage the backup S3 credential Secret through ESO.

Upgrade Notes

Upgrading from 0.x to 1.x

If upgrading across major versions, check the

changelog

for breaking changes in values structure. Always back up your data before upgrading.

  • When switching from standalone to replication, set auth.replicationPassword before upgrading
  • PVC resize requires the storage class to support volume expansion (allowVolumeExpansion: true)
  • Metrics sidecar changes may cause a pod restart during upgrade

Common Issues

Pod stuck in CrashLoopBackOff

Usually caused by incorrect auth.postgresPassword on upgrade. PostgreSQL does not re-initialize auth when the data directory already exists. Check logs with kubectl logs <pod> and verify the password matches the existing data.

Slow replication sync

For large databases, initial replica sync can take time. Increase readReplicas.resources and ensure the storage class provides adequate IOPS. Monitor replication lag with the Prometheus exporter (pg_replication_lag metric).

Using with connection poolers

For high-connection workloads, deploy PgBouncer alongside PostgreSQL. Point your application to PgBouncer and configure it to connect to the PostgreSQL service.

More Information