Skip to main content
pgAgroal docs · Deploy on Kubernetes (Helm)

Tutorial

Deploy on Kubernetes (Helm)

Helm chart, health probes, scaling, and production deployment patterns.

The Helm chart is the primary way to deploy pgagroal to Kubernetes. It runs pgagroal as a standalone Deployment with a ClusterIP Service and ships production-ready defaults for security contexts, resource limits, probes, and pod disruption budgets. If you prefer plain manifests instead, the raw-manifest example below covers that path.

Install with Helm

helm install pgagroal helm/pgagroal/ \
  --set postgresql.host=your-postgres-service \
  --set credentials.username=app \
  --set credentials.password=secret \
  -n pgagroal --create-namespace

Replace your-postgres-service with the Kubernetes Service name or hostname of your PostgreSQL backend.

Minimal production values

Most deployments need only a few overrides. The chart ships with production-ready defaults for security contexts, resource limits, probes, and pod disruption budgets.

# values-production.yaml
replicaCount: 2

image:
  repository: elevarq/pgagroal
  tag: "0.2.0"

postgresql:
  host: "pg-primary.database.svc.cluster.local"
  port: 5432

pgagroal:
  maxConnections: 50
  logLevel: warn

credentials:
  existingSecret: "pgagroal-credentials"

service:
  type: ClusterIP
  port: 6432

Store credentials in a Kubernetes Secret, not in values files. The chart reads PG_USERNAME and PG_PASSWORD from the Secret named in credentials.existingSecret.

Health probes

The chart configures liveness and readiness probes using the same command as the container's built-in health check:

pgagroal-cli -c /etc/pgagroal/pgagroal.conf ping

This checks that the pgagroal daemon is running and responsive. It does not verify backend connectivity — a healthy pooler with an unreachable backend will still pass the probe. This is intentional: the pooler should stay running so it can recover when the backend returns.

ProbeDelayIntervalFailure threshold
Liveness5s10s3 (restart after 30s of failure)
Readiness3s5s2 (stop traffic after 10s of failure)

Security context

The chart enforces a hardened security posture by default. These settings are applied out of the box — you do not need to configure them.

  • Non-root — runs as UID/GID 1000
  • No privilege escalation allowPrivilegeEscalation: false
  • All capabilities dropped capabilities.drop: [ALL]
  • Read-only root filesystem — writable paths use emptyDir volumes
  • Seccomp RuntimeDefault profile

Scaling

The chart defaults to 2 replicas with a PodDisruptionBudget of minAvailable: 1. This ensures at least one pooler remains available during rolling updates and node drains.

Replica count

Each replica maintains its own connection pool to the backend. Two replicas with maxConnections: 50each means up to 100 total backend connections. Plan your replica count and pool size together so the total does not exceed PostgreSQL's max_connections.

Resource limits

The chart defaults to 100m CPU request / 1 CPU limit and 64Mi memory request / 256Mi limit. pgagroal is lightweight — these defaults are generous for most workloads. Increase CPU if you run more than 100 pooled connections per replica.

Common patterns

Sidecar vs dedicated deployment

The Helm chart deploys pgagroal as a standalone Deployment with its own Service. This is the recommended pattern — it lets multiple application Deployments share one pool and makes scaling and monitoring independent.

A sidecar pattern (pooler in each application pod) is possible but rarely useful. It defeats the purpose of connection pooling because each pod maintains a separate pool that cannot share connections.

Connecting applications

Point your application's database connection string at the pgagroal Service:

postgresql://app:secret@pgagroal.pgagroal.svc.cluster.local:6432/appdb

The Service name and namespace depend on your Helm release name and -n flag.

Apply raw manifests

As an alternative to Helm, you can deploy pgagroal as a standalone Kubernetes Deployment with a ClusterIP Service using plain manifests. This assumes PostgreSQL is already running in the cluster or reachable from it.

Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: pgagroal
  namespace: pgagroal
spec:
  replicas: 2
  selector:
    matchLabels:
      app: pgagroal
  template:
    metadata:
      labels:
        app: pgagroal
    spec:
      securityContext:
        runAsUser: 1000
        runAsGroup: 1000
        runAsNonRoot: true
        fsGroup: 1000
        seccompProfile:
          type: RuntimeDefault
      containers:
        - name: pgagroal
          image: elevarq/pgagroal:1.1.0
          ports:
            - containerPort: 6432
              name: pooler
          env:
            - name: PG_BACKEND_HOST
              value: "postgres.database.svc.cluster.local"
            - name: PG_BACKEND_PORT
              value: "5432"
            - name: MAX_CONNECTIONS
              value: "50"
            - name: PGAGROAL_LOG_LEVEL
              value: "warn"
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities:
              drop: ["ALL"]
          readinessProbe:
            exec:
              command:
                - pgagroal-cli
                - "-c"
                - /etc/pgagroal/pgagroal.conf
                - ping
            initialDelaySeconds: 3
            periodSeconds: 5
            failureThreshold: 2
          livenessProbe:
            exec:
              command:
                - pgagroal-cli
                - "-c"
                - /etc/pgagroal/pgagroal.conf
                - ping
            initialDelaySeconds: 5
            periodSeconds: 10
            failureThreshold: 3
          resources:
            requests:
              cpu: 100m
              memory: 64Mi
            limits:
              cpu: "1"
              memory: 256Mi

Service

apiVersion: v1
kind: Service
metadata:
  name: pgagroal
  namespace: pgagroal
spec:
  type: ClusterIP
  selector:
    app: pgagroal
  ports:
    - port: 6432
      targetPort: pooler
      protocol: TCP
      name: pooler

Apply and verify

# Create namespace and apply
kubectl create namespace pgagroal
kubectl apply -f deployment.yaml
kubectl apply -f service.yaml

# Wait for pods to be ready
kubectl -n pgagroal rollout status deployment/pgagroal

# Verify from inside the cluster
kubectl -n pgagroal run test --rm -it --image=postgres:17 -- \
  psql -h pgagroal.pgagroal.svc.cluster.local -p 6432 \
  -U app -d appdb -c 'SELECT 1'

Applications connect to pgagroal.pgagroal.svc.cluster.local:6432. Replace with your namespace if different.

Scaling and connection limits

Each replica maintains its own pool. Two replicas with MAX_CONNECTIONS=50 opens up to 100 total backend connections.

Make sure the total across all replicas does not exceed PostgreSQL's max_connections, leaving room for admin and monitoring connections.

# Scale to 3 replicas (3 x 50 = 150 backend connections)
kubectl -n pgagroal scale deployment/pgagroal --replicas=3

Common adjustments

ChangeHow
Backend addressChange PG_BACKEND_HOST to your PostgreSQL Service or RDS endpoint
Pool sizeChange MAX_CONNECTIONS and verify total across replicas
CredentialsAdd PG_USERNAME and PG_PASSWORD from a Secret via envFrom
Resource limitsIncrease CPU if running >100 connections per replica
Expose externallyChange Service type to LoadBalancer — but prefer ClusterIP with application-level access

See also: Configuration for environment variables and pool sizing, or Docker Compose for local development.