CloudNativePG is a Kubernetes operator that manages the full lifecycle of PostgreSQL clusters — primary election, replication, failover, backups, and connection pooling — entirely inside your cluster. It's a battle-tested way to run production Postgres on Kubernetes, but the operator model introduces a layer of abstraction that makes database health monitoring more complex, not simpler.
In this tutorial you'll set up external monitoring for your CloudNativePG clusters using Vigilmon — free tier, no credit card required.
Why CloudNativePG needs external monitoring
CloudNativePG ships with a Prometheus metrics endpoint and integrates with the standard Kubernetes health probe system. So why add external monitoring?
Because the Kubernetes control plane and CloudNativePG operator can report everything as healthy while your database is unreachable from applications:
- Primary failover in progress — a failover takes 10–30 seconds; during this window all writes fail, but the operator reports the cluster as
Healthyonce the new primary is elected - Connection pooler (PgBouncer) failure — the CloudNativePG-managed PgBouncer pod crashes; the database is fine but no application can connect
- LoadBalancer or Service routing broken — the
ClusterIPorLoadBalancerservice for the cluster stops routing traffic after a node replacement - Replica lag exceeds threshold — a replica falls hours behind the primary; read traffic routed there returns stale data
- Backup job failure — WAL archiving or scheduled base backups silently fail; you discover this during a recovery
For each of these, CloudNativePG may show Cluster: Healthy while your application is either down or silently degraded. External monitoring catches what the operator can't see.
What you'll need
- A Kubernetes cluster with CloudNativePG installed (v1.20+ recommended)
- A running
Clusterresource with at least one instance - A free Vigilmon account (sign up takes 30 seconds)
Step 1: Deploy a CloudNativePG cluster
Here's a production-ready CloudNativePG cluster manifest with connection pooling and monitoring enabled:
# postgres-cluster.yaml
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: pg-production
namespace: databases
spec:
instances: 3
primaryUpdateStrategy: unsupervised
postgresql:
parameters:
max_connections: "200"
shared_buffers: "256MB"
log_min_duration_statement: "1000"
bootstrap:
initdb:
database: appdb
owner: appuser
secret:
name: pg-production-credentials
storage:
size: 50Gi
storageClass: fast-ssd
monitoring:
enablePodMonitor: true
backup:
retentionPolicy: "30d"
barmanObjectStore:
destinationPath: "s3://your-bucket/postgres-backups"
s3Credentials:
accessKeyId:
name: s3-creds
key: ACCESS_KEY_ID
secretAccessKey:
name: s3-creds
key: SECRET_ACCESS_KEY
---
# connection pooler
apiVersion: postgresql.cnpg.io/v1
kind: Pooler
metadata:
name: pg-production-rw
namespace: databases
spec:
cluster:
name: pg-production
instances: 2
type: rw
pgbouncer:
poolMode: transaction
parameters:
max_client_conn: "1000"
default_pool_size: "25"
Apply and verify:
kubectl apply -f postgres-cluster.yaml
# Watch cluster come up
kubectl get clusters -n databases -w
# Check instance status
kubectl get pods -n databases -l cnpg.io/cluster=pg-production
# Get the primary pod name
kubectl get pods -n databases -l cnpg.io/cluster=pg-production \
-l role=primary -o jsonpath='{.items[0].metadata.name}'
Step 2: Deploy a health-check sidecar for external probing
CloudNativePG doesn't expose an HTTP endpoint by default — Postgres speaks TCP. The cleanest way to enable Vigilmon HTTP monitoring is to deploy a lightweight health-check proxy alongside the cluster.
# pg-health-proxy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: pg-health-proxy
namespace: databases
spec:
replicas: 1
selector:
matchLabels:
app: pg-health-proxy
template:
metadata:
labels:
app: pg-health-proxy
spec:
containers:
- name: proxy
image: wrouesnel/postgres_exporter:latest
env:
- name: DATA_SOURCE_NAME
valueFrom:
secretKeyRef:
name: pg-production-credentials
key: uri
ports:
- containerPort: 9187
name: metrics
livenessProbe:
httpGet:
path: /metrics
port: 9187
initialDelaySeconds: 10
periodSeconds: 30
---
apiVersion: v1
kind: Service
metadata:
name: pg-health-proxy
namespace: databases
spec:
type: LoadBalancer
selector:
app: pg-health-proxy
ports:
- port: 9187
targetPort: 9187
protocol: TCP
Alternatively, deploy a minimal Go health server that actually queries the database:
// pg-health/main.go
package main
import (
"database/sql"
"encoding/json"
"log"
"net/http"
"os"
"time"
_ "github.com/lib/pq"
)
var db *sql.DB
func healthHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var version string
err := db.QueryRowContext(ctx, "SELECT version()").Scan(&version)
status := "ok"
httpStatus := http.StatusOK
if err != nil {
status = "error"
httpStatus = http.StatusServiceUnavailable
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(httpStatus)
json.NewEncoder(w).Encode(map[string]interface{}{
"status": status,
"checked": time.Now().UTC(),
"pg_version": version,
})
}
func main() {
var err error
db, err = sql.Open("postgres", os.Getenv("PG_DSN"))
if err != nil {
log.Fatal(err)
}
db.SetMaxOpenConns(2)
db.SetConnMaxLifetime(30 * time.Second)
http.HandleFunc("/health", healthHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
Deploy it and expose it externally so Vigilmon can reach it from outside the cluster.
Step 3: Set up HTTP monitoring in Vigilmon
With your health endpoint accessible, add it to Vigilmon:
- Log in to vigilmon.online and go to Monitors → New Monitor
- Choose HTTP / HTTPS as the monitor type
- Set the URL to your health proxy:
https://pg-health.example.com/health - Set the check interval to 1 minute
- Under Expected response, configure:
- Status code:
200 - Response body contains:
"status":"ok"
- Status code:
- Save the monitor
What this catches
This monitor fires if:
- The health proxy pod is down (connection pooler or network failure)
- The query to PostgreSQL fails (primary crashed, failover in progress)
- The Kubernetes service stops routing traffic
Step 4: Monitor the TCP layer
In addition to HTTP, monitor the raw TCP port of the CloudNativePG service. This catches load balancer or service routing failures independently of the health proxy:
# Get the external IP of the CloudNativePG read-write service
kubectl get svc -n databases pg-production-rw \
-o jsonpath='{.status.loadBalancer.ingress[0].ip}'
In Vigilmon:
- Go to Monitors → New Monitor
- Choose TCP Port
- Enter the external IP and port
5432 - Save the monitor
If the TCP monitor fires but the HTTP monitor is healthy, you likely have a service routing issue rather than a database failure.
Step 5: Monitor backup health with a heartbeat
CloudNativePG backup failures are silent. Add a heartbeat monitor that your backup job pings on success:
# backup-checker.yaml — a CronJob that verifies recent backups exist
apiVersion: batch/v1
kind: CronJob
metadata:
name: backup-health-checker
namespace: databases
spec:
schedule: "0 * * * *" # hourly
jobTemplate:
spec:
template:
spec:
containers:
- name: checker
image: bitnami/kubectl:latest
command:
- /bin/sh
- -c
- |
LATEST=$(kubectl get backup -n databases \
-l cnpg.io/cluster=pg-production \
--sort-by=.metadata.creationTimestamp \
-o jsonpath='{.items[-1].status.phase}')
if [ "$LATEST" = "completed" ]; then
curl -fsS "https://vigilmon.online/api/v1/heartbeat/YOUR_HEARTBEAT_ID"
else
echo "Latest backup not completed: $LATEST"
exit 1
fi
restartPolicy: Never
Set up the heartbeat in Vigilmon:
- Go to Monitors → New Monitor
- Choose Heartbeat
- Set interval to 2 hours (allows for backup job delay)
- Copy the heartbeat URL into your CronJob manifest
- Save the monitor
If backups stop completing, you'll get an alert within 2 hours instead of discovering it during a disaster recovery.
Step 6: Configure alert channels and runbook integration
When Vigilmon detects a PostgreSQL cluster issue, you need immediate context. Configure a webhook to enrich alerts with cluster state:
#!/bin/bash
# on-alert.sh — called by your webhook handler
CLUSTER="pg-production"
NS="databases"
echo "=== CloudNativePG Cluster Status ==="
kubectl get cluster $CLUSTER -n $NS -o yaml | grep -A 20 "status:"
echo "=== Pods ==="
kubectl get pods -n $NS -l cnpg.io/cluster=$CLUSTER
echo "=== Recent Events ==="
kubectl get events -n $NS --field-selector involvedObject.name=$CLUSTER \
--sort-by='.lastTimestamp' | tail -20
echo "=== Replication Lag ==="
kubectl exec -n $NS \
$(kubectl get pods -n $NS -l cnpg.io/cluster=$CLUSTER,role=primary \
-o jsonpath='{.items[0].metadata.name}') \
-- psql -U postgres -c \
"SELECT client_addr, state, sent_lsn, replay_lsn,
(sent_lsn - replay_lsn) AS replay_lag
FROM pg_stat_replication;"
To receive alerts:
- In Vigilmon, go to Alert Channels → Add Channel
- Choose Email for on-call and Webhook for your automated runbook handler
- Assign both channels to all PostgreSQL monitors
Step 7: Create a database status page
Add a status page that gives your application teams visibility into database health:
- In Vigilmon, go to Status Pages → New Status Page
- Name it "Database Infrastructure"
- Add all your CloudNativePG monitors: HTTP health, TCP port, backup heartbeat
- Group by cluster name if you run multiple clusters
- Publish the URL and share with your backend engineering team
Monitoring reference
| Monitor | Type | What it catches |
|---------|------|-----------------|
| https://pg-health.example.com/health | HTTP | Primary crashes, failover windows, query failures |
| External IP:5432 | TCP | Service routing failures, load balancer issues |
| Backup CronJob | Heartbeat | Silent backup failures, WAL archiving errors |
| PgBouncer endpoint | HTTP | Connection pooler failures |
Useful CloudNativePG debugging commands when alerts fire:
# Check overall cluster health
kubectl get cluster pg-production -n databases
# Check which pod is primary
kubectl get pods -n databases -l cnpg.io/cluster=pg-production,role=primary
# View operator logs
kubectl logs -n cnpg-system \
$(kubectl get pods -n cnpg-system -l app.kubernetes.io/name=cloudnative-pg \
-o jsonpath='{.items[0].metadata.name}') -f
# View recent backups
kubectl get backup -n databases -l cnpg.io/cluster=pg-production \
--sort-by=.metadata.creationTimestamp
# Trigger a manual failover
kubectl cnpg promote pg-production -n databases
# Check replication status from primary
kubectl exec -n databases \
$(kubectl get pods -n databases -l cnpg.io/cluster=pg-production,role=primary \
-o jsonpath='{.items[0].metadata.name}') \
-- psql -U postgres -c "SELECT * FROM pg_stat_replication;"
What's next
- SSL certificate monitoring — if you expose PgBouncer via TLS, Vigilmon will alert you before the certificate expires
- Replica monitoring — add separate monitors for read replicas to detect replication lag before it affects your read-only workloads
- Multi-cluster coverage — if you run CloudNativePG clusters in multiple namespaces or environments, create a monitor group for each and aggregate them on a single status page
Get started free at vigilmon.online — no credit card, monitors start running in under a minute.