PGO Hands-On Demo & Operations Playbook
From “Operator Installed” to Production-Ready PostgreSQL on Kubernetes
Audience: Platform engineers, SREs, DBAs presenting a live PGO capability demo. Primary source: https://access.crunchydata.com/documentation/postgres-operator/latest — official guidance is prioritized wherever community sources (Reddit, Stack Overflow, GitHub issues) conflict; such sources are explicitly labeled “Community note.” Assumption: PGO operator pod is already
Runningin namespacepostgres-operator; CRDs are registered. Demo namespace used throughout:demo-db. Cluster name used:shopdb. DB:shopdb. App user:shopuser.
How to Use This Document
Each phase is a self-contained lab: Objective → Background → Architecture → Commands/YAML → Expected Output → Validation → Internals → Common Mistakes → Troubleshooting → Best Practices → Cleanup. Run phases in order for a first full pass; each can also be re-run independently once shopdb exists.
Create the demo namespace once, up front:
1
kubectl create namespace demo-db
Phase 1 — Verify Operator Installation
Objective: Prove PGO is healthy before touching any cluster — this is the “first slide” of the demo.
Background: PGO is a single controller Deployment watching PostgresCluster/PGUpgrade/PGAdmin CRDs cluster-wide (or namespace-scoped).
Commands:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Operator deployment health
kubectl -n postgres-operator get deployment pgo
kubectl -n postgres-operator get pods -l postgres-operator.crunchydata.com/control-plane=postgres-operator
# CRDs registered
kubectl get crd | grep postgres-operator.crunchydata.com
# RBAC
kubectl get clusterrole postgres-operator
kubectl get clusterrolebinding postgres-operator
kubectl -n postgres-operator get serviceaccount pgo
# Namespace
kubectl get ns postgres-operator
# Operator logs (reconciliation activity)
kubectl -n postgres-operator logs deploy/pgo --tail=50
# Events
kubectl -n postgres-operator get events --sort-by=.lastTimestamp
# Version verification (image tag = operator version)
kubectl -n postgres-operator get deployment pgo -o jsonpath='{.spec.template.spec.containers[0].image}'
Expected Output: pgo deployment 1/1 ready; three CRDs (postgresclusters, pgupgrades, pgadmins) listed; no Warning events.
Note: PGO does not use admission webhooks by default in the kustomize install path — if kubectl get validatingwebhookconfigurations | grep postgres returns nothing, that is expected, not an error.
Validation: All commands above return non-empty, healthy results with no CrashLoopBackOff/Error states.
Common Mistakes: Assuming a missing webhook means installation failed (it’s optional); checking the wrong namespace.
Cleanup: None — this phase is read-only.
Phase 2 — Deploy the First PostgreSQL Cluster
Objective: Go from zero to a running, production-shaped 3-node HA cluster, explaining every field live.
Step 2a — Minimal single-instance cluster (fastest first win):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
apiVersion: postgres-operator.crunchydata.com/v1beta1
kind: PostgresCluster
metadata:
name: shopdb
namespace: demo-db
spec:
postgresVersion: 16
instances:
- name: instance1
replicas: 1
dataVolumeClaimSpec:
accessModes: ["ReadWriteOnce"]
storageClassName: rook-ceph-block
resources:
requests:
storage: 5Gi
backups:
pgbackrest:
repos:
- name: repo1
volume:
volumeClaimSpec:
accessModes: ["ReadWriteOnce"]
storageClassName: rook-ceph-block
resources:
requests:
storage: 5Gi
1
2
kubectl apply -f shopdb-basic.yaml
kubectl -n demo-db get pods -w
Expected Output: shopdb-instance1-xxxx-0 reaches 2/2 Running (database + pgBackRest containers) within ~60-90s; a shopdb-repo-host-0 pod also appears.
Step 2b — Evolve to production-grade HA (edit in place, kubectl apply again):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
apiVersion: postgres-operator.crunchydata.com/v1beta1
kind: PostgresCluster
metadata:
name: shopdb
namespace: demo-db
labels:
environment: demo
spec:
postgresVersion: 16
instances:
- name: instance1
replicas: 3
resources:
requests: { cpu: "1", memory: "2Gi" }
limits: { cpu: "2", memory: "2Gi" }
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- topologyKey: kubernetes.io/hostname
labelSelector:
matchLabels:
postgres-operator.crunchydata.com/cluster: shopdb
postgres-operator.crunchydata.com/instance-set: instance1
dataVolumeClaimSpec:
accessModes: ["ReadWriteOnce"]
storageClassName: rook-ceph-block
resources:
requests: { storage: 10Gi }
users:
- name: shopuser
databases: ["shopdb"]
proxy:
pgBouncer:
replicas: 2
monitoring:
pgmonitor:
exporter: {}
backups:
pgbackrest:
repos:
- name: repo1
schedules:
full: "0 1 * * 0"
volume:
volumeClaimSpec:
accessModes: ["ReadWriteOnce"]
storageClassName: rook-ceph-block
resources:
requests: { storage: 10Gi }
1
2
kubectl apply -f shopdb-ha.yaml
kubectl -n demo-db get pods -l postgres-operator.crunchydata.com/cluster=shopdb -w
Expected Output: Scales to 3 instance pods + 2 pgBouncer pods + 1 repo-host pod, rolled out one at a time.
What’s happening internally: PGO diffs the new spec against generated StatefulSets/Services, adds two new instance pods via Patroni’s bootstrap-from-primary path (pg_basebackup-equivalent), and updates the pgBouncer Deployment replica count.
Common Mistakes: Forgetting storageClassName (leaves PVC unbound — see Phase 16); setting CPU limits equal to requests and expecting no throttling under load spikes.
Cleanup (end of demo only): kubectl -n demo-db delete postgrescluster shopdb
Phase 3 — Explore Generated Resources
Objective: Show the supervisor everything PGO created from that one CR — this is usually the most impressive part of a live demo.
1
2
3
4
kubectl -n demo-db get all -l postgres-operator.crunchydata.com/cluster=shopdb
kubectl -n demo-db get statefulset,pod,svc,endpoints,configmap,secret,pvc,job,cronjob -l postgres-operator.crunchydata.com/cluster=shopdb
kubectl -n demo-db get secrets -o custom-columns=NAME:.metadata.name | grep shopdb
kubectl -n demo-db describe postgrescluster shopdb
| Resource type | Name pattern | Why it exists |
|---|---|---|
| Pods | shopdb-instance1-xxxx-0/1/2 |
Patroni-managed Postgres instances |
| Pod | shopdb-repo-host-0 |
pgBackRest repository host (local PVC repo) |
| Pods | shopdb-pgbouncer-xxxx |
Connection pooling layer |
| Service | shopdb-ha |
Routes to current Patroni leader |
| Service | shopdb-replicas |
Load-balances across ready replicas |
| Service | shopdb-pgbouncer |
Pooled connection entrypoint |
| Service (headless) | shopdb-pods |
Direct pod DNS for Patroni/replication |
| Secret | shopdb-pguser-shopuser |
App connection credentials + URI |
| Secret | shopdb-cluster-cert |
TLS server cert (auto-generated) |
| ConfigMap | (Patroni DCS keys, via Endpoints/Leases) | Distributed leader-election state |
| Job | shopdb-repo1-stanza-create |
One-time pgBackRest stanza initialization |
| CronJob | shopdb-repo1-full (if scheduled) |
Automated backup trigger |
| PVC | shopdb-instance1-xxxx-pgdata |
PGDATA storage per instance |
| PVC | shopdb-repo-host-0-repo1 |
Local backup repository storage |
Validation: Decode a secret to prove real credentials exist:
1
kubectl -n demo-db get secret shopdb-pguser-shopuser -o jsonpath='{.data.password}' | base64 -d
Cleanup: None — read-only exploration.
Phase 4 — Database Operations & Client Connectivity
Objective: Demonstrate DBA tasks and connect via psql, pgAdmin, and DBeaver — this is the section your supervisor will care most about seeing live.
4.1 Common DBA tasks via psql
1
2
kubectl -n demo-db exec -it shopdb-instance1-xxxx-0 -c database -- \
psql -h 127.0.0.1 -U shopuser -d shopdb
Note: The -h 127.0.0.1 flag forces a TCP connection instead of the Unix domain socket. PGO’s generated pg_hba.conf does not include a local socket entry for application users, so omitting -h produces: no pg_hba.conf entry for host "[local]".
1
2
3
4
5
6
CREATE TABLE products (id serial PRIMARY KEY, name text, price numeric);
INSERT INTO products (name, price) VALUES ('Demo Widget', 9.99);
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
GRANT SELECT ON products TO shopuser;
\copy products TO '/tmp/products.csv' CSV HEADER; -- export
\copy products FROM '/tmp/products.csv' CSV HEADER; -- import
4.2 Connecting externally: port-forward (fastest for a live demo)
1
2
kubectl -n demo-db port-forward svc/shopdb-ha 5432:5432
psql "host=localhost port=5432 dbname=shopdb user=shopuser sslmode=require"
4.3 NodePort / LoadBalancer / Ingress
1
2
3
# NodePort (works on bare-metal/on-prem, like this Dhaka-based cluster)
kubectl -n demo-db expose svc shopdb-ha --type=NodePort --name=shopdb-ha-nodeport \
--port=5432 --target-port=5432 --selector=postgres-operator.crunchydata.com/patroni=shopdb-ha
Community note: Ingress controllers designed for HTTP (e.g. NGINX Ingress) do not natively proxy raw Postgres TCP traffic — an EKS user on Reddit hit exactly this and had to switch to TCP-mode/HAProxy Ingress or a LoadBalancer Service instead [web:65]. This is a very common first-time mistake worth calling out live.
4.4 pgAdmin (PGO-managed, browser-based)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apiVersion: postgres-operator.crunchydata.com/v1beta1
kind: PGAdmin
metadata:
name: shopdb-pgadmin
namespace: demo-db
spec:
dataVolumeClaimSpec:
accessModes: ["ReadWriteOnce"]
resources:
requests: { storage: 1Gi }
serverGroups:
- name: demo
postgresClusterSelector:
matchLabels:
postgres-operator.crunchydata.com/cluster: shopdb
1
2
3
kubectl apply -f shopdb-pgadmin.yaml
kubectl -n demo-db port-forward svc/shopdb-pgadmin 5050:5050
# Browse http://localhost:5050 — login with the pguser Secret credentials
[image:1]
Community note: Several users report the pgAdmin pod comes up but the login page 404s/won’t authenticate — GitHub issue #3672 traces this to a missing userInterface.pgAdmin block on older PGO versions vs. the newer standalone PGAdmin CRD shown above; using the standalone CRD (current official approach) avoids this [web:67].
4.5 DBeaver / DataGrip
Use the same port-forwarded localhost:5432, database shopdb, user shopuser, and the password from the shopdb-pguser-shopuser Secret. Enable SSL mode “require” in the connection’s SSL tab since PGO enforces TLS by default.
Troubleshooting connectivity:
| Symptom | Likely cause | Fix |
|---|---|---|
password authentication failed |
Stale cached password after rotation | Re-fetch Secret value |
SSL connection required |
Client SSL mode set to disable |
Set sslmode=require or higher |
| Connection refused via Ingress | HTTP-only Ingress can’t proxy TCP [web:65] | Use NodePort/LoadBalancer/TCP-mode ingress |
| Timeout via NodePort | Firewall/security group blocking node port | Open port range in cloud security group |
Cleanup: kubectl -n demo-db delete pgadmin shopdb-pgadmin; kubectl -n demo-db delete svc shopdb-ha-nodeport
Phase 5 — PostgreSQL Configuration
Objective: Show live parameter tuning and explain reload vs. restart behavior.
1
2
3
4
5
6
7
8
9
10
11
12
spec:
patroni:
dynamicConfiguration:
postgresql:
parameters:
shared_buffers: "512MB" # requires restart
work_mem: "16MB" # reload only
maintenance_work_mem: "256MB" # reload only
max_connections: "200" # requires restart
wal_level: "replica" # requires restart
log_min_duration_statement: "500" # reload only
autovacuum_max_workers: "4" # requires restart
1
2
kubectl apply -f shopdb-ha.yaml
kubectl -n demo-db exec -it shopdb-instance1-xxxx-0 -- patronictl show-config
Internals: Patroni classifies each parameter as reload-safe or restart-required internally; for restart-required params, PGO/Patroni performs a rolling restart, one pod at a time, always preserving quorum (never restarts a majority simultaneously).
Validation: SHOW shared_buffers; in psql after rollout completes.
Common Mistakes: Expecting shared_buffers changes to apply without any pod restart — always check patronictl show-config “pending restart” flag.
Phase 6 — Patroni Demonstrations
Objective: This is the core HA story — spend real demo time here.
1
2
3
kubectl -n demo-db exec -it shopdb-instance1-xxxx-0 -- patronictl list
kubectl -n demo-db exec -it shopdb-instance1-xxxx-0 -- curl -s localhost:8008/health
kubectl -n demo-db exec -it shopdb-instance1-xxxx-0 -- curl -s localhost:8008/cluster | jq
Manual switchover (planned, safe):
1
2
kubectl -n demo-db exec -it shopdb-instance1-xxxx-0 -- \
patronictl switchover --master shopdb-instance1-xxxx-0 --candidate shopdb-instance1-yyyy-0 --force
Manual failover (forces promotion even if leader reachable — riskier):
1
2
kubectl -n demo-db exec -it shopdb-instance1-xxxx-0 -- \
patronictl failover --candidate shopdb-instance1-yyyy-0
| Switchover | Failover | |
|---|---|---|
| Trigger | Manual, planned | Manual (forced) or automatic (leader lost) |
| Downtime | Seconds, coordinated | Can be longer, uncoordinated |
| Risk | Low — old leader demoted cleanly | Higher — possible split-brain if misused on a reachable leader |
| When to use | Maintenance, node drains | True primary failure only |
Synchronous vs asynchronous:
1
2
3
patroni:
dynamicConfiguration:
synchronous_mode: true # zero data loss, higher write latency
Automatic failover demo: delete the current leader pod and watch Patroni elect a new one live:
1
2
kubectl -n demo-db delete pod shopdb-instance1-xxxx-0
watch kubectl -n demo-db exec -it shopdb-instance1-yyyy-0 -- patronictl list
Cleanup: None needed; cluster self-heals.
Phase 7 — Scaling
1
2
3
4
5
6
7
8
# Add/remove replicas
kubectl -n demo-db patch postgrescluster shopdb --type='json' \
-p='[{"op":"replace","path":"/spec/instances/0/replicas","value":4}]'
# Vertical scaling (edit resources in YAML, reapply)
# Storage resize (requires allowVolumeExpansion: true on StorageClass)
kubectl -n demo-db patch postgrescluster shopdb --type='merge' \
-p='{"spec":{"instances":[{"name":"instance1","dataVolumeClaimSpec":{"resources":{"requests":{"storage":"20Gi"}}}}]}}'
Measure downtime during scaling: time a continuous psql -c "select 1" loop against shopdb-ha service while scaling — for replica-only changes, expect zero write downtime.
Phase 8 — Backup Demonstrations
1
2
3
4
5
6
7
8
9
10
# Manual full backup
kubectl -n demo-db annotate postgrescluster shopdb \
postgres-operator.crunchydata.com/pgbackrest-backup="$(date +%s)" --overwrite
# View CronJobs created by schedules
kubectl -n demo-db get cronjob
kubectl -n demo-db logs job/<backup-job-name>
# Verify repository contents
kubectl -n demo-db exec -it shopdb-repo-host-0 -- pgbackrest info --stanza=db
For S3/MinIO repos, add a second repo (repo2) as shown in the Production Guide’s Section 8 — demo both a local PVC repo and an S3 repo side-by-side to show redundancy live.
Phase 9 — Restore Demonstrations
Restore to a brand-new cluster (safe, non-destructive — ideal for live demo):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
apiVersion: postgres-operator.crunchydata.com/v1beta1
kind: PostgresCluster
metadata:
name: shopdb-restore-demo
namespace: demo-db
spec:
postgresVersion: 16
instances:
- name: instance1
dataVolumeClaimSpec:
accessModes: ["ReadWriteOnce"]
storageClassName: rook-ceph-block
resources: { requests: { storage: 10Gi } }
backups:
pgbackrest:
repos:
- name: repo1
volume:
volumeClaimSpec:
accessModes: ["ReadWriteOnce"]
storageClassName: rook-ceph-block
resources: { requests: { storage: 10Gi } }
dataSource:
postgresCluster:
clusterName: shopdb
repoName: repo1
options: ["--type=immediate"]
For PITR, replace options with ["--type=time", "--target=<timestamp>"]. Verify with SELECT * FROM products; in the restored cluster to prove data fidelity.
Cleanup: kubectl -n demo-db delete postgrescluster shopdb-restore-demo
Phase 10 — Disaster Recovery Simulations
| Simulated failure | Command | Expected PGO/Patroni behavior | Expected RTO |
|---|---|---|---|
| Delete primary pod | kubectl delete pod shopdb-instance1-xxxx-0 |
Automatic failover, new leader elected | ~10-30s |
| Delete replica pod | kubectl delete pod shopdb-instance1-yyyy-0 |
Pod recreated, resyncs from primary | ~30-60s |
| Delete PVC (destructive!) | kubectl delete pvc <pvc-name> |
Pod stuck; requires manual PVC recreation or replica reinit | Minutes |
| Node failure (drain) | kubectl drain <node> --ignore-daemonsets |
Pod reschedules if PVC topology allows; failover if primary | Minutes |
| Corrupted database | Simulate via bad write, then restore | Full restore-to-new-cluster from Phase 9 | Depends on data size |
RPO/RTO discussion for the demo: streaming replication gives near-zero RPO/fast RTO; PITR-only recovery has RPO bounded by WAL archive push frequency and RTO scaling with data volume — present both live for contrast.
Phase 11 — Kubernetes Failure Scenarios
Demonstrate PGO’s resilience to generic K8s issues: pod eviction (kubectl drain), resource pressure (kubectl top pod before/after a stress test), image pull failure (temporarily typo an image tag and observe ImagePullBackOff, then revert), and PVC Multi-Attach errors after a forced node loss (see Troubleshooting catalog, Phase 16, for the full resolution runbook).
Phase 12 — Monitoring
1
2
3
4
spec:
monitoring:
pgmonitor:
exporter: {}
1
2
kubectl -n demo-db port-forward svc/shopdb-instance1-xxxx 9187:9187
curl localhost:9187/metrics | grep pg_up
Deploy Prometheus + Grafana (via your existing cluster monitoring stack) and import Crunchy’s official pgMonitor dashboards. Key panels to show live: replication lag, connections, backup age, checkpoint frequency, WAL generation rate.
Phase 13 — Application Integration
Django
1
2
3
4
5
6
7
8
9
10
11
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'shopdb',
'USER': 'shopuser',
'PASSWORD': os.environ['DB_PASSWORD'],
'HOST': 'shopdb-pgbouncer',
'PORT': '5432',
'OPTIONS': {'sslmode': 'require'},
}
}
1
2
3
4
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef: { name: shopdb-pguser-shopuser, key: password }
Run python manage.py migrate against the pgBouncer endpoint, then perform CRUD via the app while triggering a switchover (Phase 6) to demonstrate the app reconnecting transparently.
Laravel (.env)
1
2
3
4
5
6
DB_CONNECTION=pgsql
DB_HOST=shopdb-pgbouncer
DB_PORT=5432
DB_DATABASE=shopdb
DB_USERNAME=shopuser
DB_PASSWORD=${DB_PASSWORD}
Run php artisan migrate the same way. The same Secret-injection pattern applies to Spring Boot (application.yml datasource), Node/Express (pg client config), and Go (database/sql + lib/pq/pgx).
Failover behavior demo: run a simple write-loop script against the app while triggering patronictl switchover; observe brief connection errors (expected — clients must reconnect) then successful resumption, illustrating that PGO gives HA at the database layer, but applications still need connection retry logic for true zero-downtime.
Phase 14 — Security
- TLS: Default operator-managed certs, or supply
customTLSSecret; demo verifyingsslmode=requireis enforced by testingsslmode=disablefails. - Password rotation:
kubectl -n demo-db delete secret shopdb-pguser-shopuser— PGO regenerates on next reconcile. - RBAC: Show the
postgres-operatorClusterRole scope; contrast with a namespace-scoped install. - NetworkPolicy: Add a policy restricting ingress to
shopdb-ha/shopdb-pgbouncerfrom only the app namespace. - Least privilege: Use
readonlyuser(from the Production Guide) for reporting connections instead of the app’s write user.
Phase 15 — Day-2 Operations
Cover PostgreSQL major version upgrade via PGUpgrade CRD, PGO operator upgrade (reapply newer config/default), pod/cluster restarts via annotation, storage class changes, adding a second backup repo, and certificate/config rotation — all detailed with commands in the companion Production Guide (Sections 13 and 15).
Phase 16 — Troubleshooting Workshop
| Issue | Symptoms | Root Cause | Diagnostic | Resolution | Prevention |
|---|---|---|---|---|---|
| Cluster won’t start | No pods appear | CRDs not ready when CR applied (race condition) | kubectl get crd \| grep postgres-operator |
Wait, reapply | Add delay/wait step in scripts |
| Pods stuck Pending | describe pod shows unbound PVC |
Wrong/missing StorageClass | kubectl get pvc, kubectl get sc |
Fix storageClassName |
Validate SC exists pre-deploy |
| CrashLoopBackOff | Repeated restarts | Bad config param, OOMKill | kubectl logs --previous |
Revert config, raise memory limits | Test config in staging first |
| Failed backups | Job Error/stuck |
PVC full, S3 auth failure | pgbackrest info, job logs |
Fix creds/free space | Alert on backup age |
| Restore failures | Restore job never completes | Wrong clusterName/repoName, version mismatch |
Job logs | Correct dataSource fields |
Validate before demo |
| Replication lag | patronictl list shows high lag |
Slow replica I/O, long queries | patronictl list, pg_stat_replication |
patronictl reinit |
Match replica sizing to primary |
| Leader election problems | No promotion after failure | All replicas exceed maximum_lag_on_failover |
patronictl history |
Manual failover once safe |
Tune lag threshold appropriately |
| PVC full | Write errors, alerts | Undersized volume, WAL bloat | df -h in pod, kubectl exec |
Expand PVC | Monitor disk usage proactively |
| WAL growth unbounded | Disk fills despite normal traffic | Community-reported: stale replication slot for an offline standby retains WAL indefinitely [web:69] | pg_replication_slots |
Drop stale slot | Alert on slot lag, remove decommissioned standbys’ slots |
| High CPU | Sustained load | Missing indexes, bad query plans | pg_stat_activity |
Kill/optimize query | Query review, pg_stat_statements |
| High memory | OOM risk | work_mem too high × many connections |
kubectl top pod |
Lower work_mem, add pgBouncer |
Pool connections |
| Slow queries | App latency | Missing indexes, lock contention | EXPLAIN ANALYZE, pg_stat_activity |
Add index, tune query | Regular query audits |
| Authentication failures | password authentication failed |
Stale cached credential | App logs | Refetch Secret | Rolling restart on rotation |
| TLS issues | Handshake failure | CA mismatch across primary/standby | openssl s_client |
Ensure common CA | Standardize cert issuance |
| pgBackRest failures | stanza-create job fails |
Repo storage/permission issue | Job logs | Fix PVC/IAM perms | Pre-validate repo config |
| Patroni failures | DCS unreachable | RBAC/network issue to Endpoints/Leases API | patronictl list errors |
Fix RBAC | Test failover in staging |
| S3 auth errors | could not connect to S3 bucket |
Wrong keys/region/endpoint | Job/pod logs | Fix Secret/IAM policy | Rotate & test creds regularly |
| MinIO connectivity | Same as S3 but self-hosted | Wrong URI style (path vs host) |
Job logs | Set repo1-s3-uri-style: path |
Document MinIO-specific config |
| Ingress can’t proxy Postgres | Connection refused via HTTP Ingress | HTTP-only Ingress used for TCP traffic (Community note, Reddit) [web:65] | Ingress logs | Switch to NodePort/LB or TCP-mode Ingress (HAProxy) | Use LB/NodePort for DB traffic by default |
| pgAdmin login fails | 404/auth failure on pgAdmin UI | Using deprecated userInterface.pgAdmin field (Community note, GitHub #3672) [web:67] |
pgAdmin pod logs | Migrate to standalone PGAdmin CRD |
Use current CRD API from the start |
| Custom image deploy errors | Pod fails to start with custom image | Missing required binaries/labels expected by PGO in custom builds (Community note, Stack Overflow) [web:66] | Pod logs, describe pod |
Rebuild image from Crunchy’s base UBI image | Always base custom images on official Crunchy images |
Phase 17 — Demo Scenarios Checklist (Live Presentation Script)
- ☐ Show operator health (Phase 1)
- ☐ Deploy
shopdbHA cluster live (Phase 2) - ☐ Walk through generated resources (Phase 3)
- ☐ Connect via
psql(Phase 4.1) - ☐ Connect via pgAdmin in browser (Phase 4.4)
- ☐ Connect via DBeaver (Phase 4.5)
- ☐ Deploy sample Django app, run migration, verify CRUD (Phase 13)
- ☐ Kill primary pod → show automatic failover (Phase 6/10)
- ☐ Perform planned manual switchover (Phase 6)
- ☐ Trigger manual backup, show in
pgbackrest info(Phase 8) - ☐ Restore to new cluster, verify data (Phase 9)
- ☐ Scale replicas live, measure zero write downtime (Phase 7)
- ☐ Change a PostgreSQL parameter, show rolling restart (Phase 5)
- ☐ Show Grafana dashboard with live metrics (Phase 12)
- ☐ Rotate app user’s password, show app picks up new Secret (Phase 14)
- ☐ (Optional, if time) PostgreSQL minor version upgrade (Phase 15)
- ☐ Clone
shopdbinto a “staging” copy from backups (Phase 9)
Reference Links
- Official docs: https://access.crunchydata.com/documentation/postgres-operator/latest
- pgAdmin CRD blog: https://www.crunchydata.com/blog/cpk-5-5-a-new-pgadmin-experience [web:61]
- Patroni docs: https://patroni.readthedocs.io
- pgBackRest docs: https://pgbackrest.org/user-guide.html
- Community pgAdmin issue: https://github.com/CrunchyData/postgres-operator/issues/3672 [web:67]
- Community Ingress/TCP discussion: https://www.reddit.com/r/kubernetes/comments/1jd6e3f/ [web:65]
- Open community issues catalog: https://github.com/crunchydata/postgres-operator/issues [web:69]