
How to Deploy PostgreSQL to a Kubernetes Cluster
Deploying PostgreSQL to a Kubernetes cluster gives you the power of a robust database system with the flexibility and scalability of containerized orchestration. While PostgreSQL is traditionally deployed on dedicated servers, running it in Kubernetes opens doors to automated scaling, rolling updates, persistent storage management, and seamless integration with microservices architectures. This guide walks you through the entire process, from basic StatefulSet deployments to production-ready configurations with monitoring and backup strategies.
Understanding PostgreSQL in Kubernetes Architecture
PostgreSQL in Kubernetes requires special considerations compared to stateless applications. Unlike typical web services that can be easily replicated and destroyed, databases need persistent storage, stable network identities, and careful handling of initialization procedures. Kubernetes provides StatefulSets specifically for these stateful workloads.
The core components you’ll work with include:
- StatefulSet – Manages PostgreSQL pods with stable identities and ordered deployment
- PersistentVolumeClaims (PVC) – Provides durable storage that survives pod restarts
- Services – Enables network access to your database
- ConfigMaps and Secrets – Store configuration files and sensitive credentials
- Init Containers – Handle database initialization and setup tasks
Step-by-Step PostgreSQL Deployment
Prerequisites and Cluster Preparation
Before deploying PostgreSQL, ensure your Kubernetes cluster has a storage class configured for persistent volumes. Check available storage classes:
kubectl get storageclass
Create a dedicated namespace for your database deployment:
kubectl create namespace postgres-system
Creating PostgreSQL Secrets
First, create a Secret to store database credentials. Never hardcode passwords in your manifests:
apiVersion: v1
kind: Secret
metadata:
name: postgres-secret
namespace: postgres-system
type: Opaque
data:
postgres-password:
postgres-user:
postgres-db:
Generate base64 encoded values:
echo -n "your-password" | base64
echo -n "myuser" | base64
echo -n "mydatabase" | base64
PostgreSQL StatefulSet Configuration
Here’s a complete StatefulSet configuration for PostgreSQL:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
namespace: postgres-system
spec:
serviceName: postgres-headless
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:15-alpine
ports:
- containerPort: 5432
name: postgres
env:
- name: POSTGRES_DB
valueFrom:
secretKeyRef:
name: postgres-secret
key: postgres-db
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: postgres-secret
key: postgres-user
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-secret
key: postgres-password
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
volumeMounts:
- name: postgres-storage
mountPath: /var/lib/postgresql/data
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
exec:
command:
- pg_isready
- -U
- $(POSTGRES_USER)
- -d
- $(POSTGRES_DB)
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
exec:
command:
- pg_isready
- -U
- $(POSTGRES_USER)
- -d
- $(POSTGRES_DB)
initialDelaySeconds: 5
periodSeconds: 5
volumeClaimTemplates:
- metadata:
name: postgres-storage
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: "standard"
resources:
requests:
storage: 10Gi
Service Configuration
Create both headless and ClusterIP services for different access patterns:
---
apiVersion: v1
kind: Service
metadata:
name: postgres-headless
namespace: postgres-system
spec:
clusterIP: None
selector:
app: postgres
ports:
- port: 5432
targetPort: 5432
---
apiVersion: v1
kind: Service
metadata:
name: postgres-service
namespace: postgres-system
spec:
selector:
app: postgres
ports:
- port: 5432
targetPort: 5432
type: ClusterIP
Deployment and Verification
Apply all configurations:
kubectl apply -f postgres-secret.yaml
kubectl apply -f postgres-statefulset.yaml
kubectl apply -f postgres-service.yaml
Monitor the deployment:
kubectl get pods -n postgres-system -w
kubectl logs -f postgres-0 -n postgres-system
Test database connectivity:
kubectl exec -it postgres-0 -n postgres-system -- psql -U myuser -d mydatabase -c "SELECT version();"
Production-Ready Configuration
PostgreSQL Configuration Tuning
For production deployments, create a ConfigMap with optimized PostgreSQL settings:
apiVersion: v1
kind: ConfigMap
metadata:
name: postgres-config
namespace: postgres-system
data:
postgresql.conf: |
# Connection settings
max_connections = 200
shared_buffers = 256MB
effective_cache_size = 1GB
maintenance_work_mem = 64MB
checkpoint_completion_target = 0.9
wal_buffers = 16MB
default_statistics_target = 100
random_page_cost = 1.1
effective_io_concurrency = 200
# Logging
log_destination = 'stderr'
logging_collector = on
log_directory = 'pg_log'
log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log'
log_statement = 'error'
log_min_duration_statement = 1000
Mount the ConfigMap in your StatefulSet:
volumeMounts:
- name: postgres-storage
mountPath: /var/lib/postgresql/data
- name: postgres-config
mountPath: /etc/postgresql/postgresql.conf
subPath: postgresql.conf
volumes:
- name: postgres-config
configMap:
name: postgres-config
Backup Strategy Implementation
Set up automated backups using a CronJob:
apiVersion: batch/v1
kind: CronJob
metadata:
name: postgres-backup
namespace: postgres-system
spec:
schedule: "0 2 * * *" # Daily at 2 AM
jobTemplate:
spec:
template:
spec:
containers:
- name: postgres-backup
image: postgres:15-alpine
command:
- /bin/bash
- -c
- |
BACKUP_FILE="/backup/postgres-$(date +%Y%m%d-%H%M%S).sql"
pg_dump -h postgres-service -U $POSTGRES_USER -d $POSTGRES_DB > $BACKUP_FILE
echo "Backup completed: $BACKUP_FILE"
env:
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: postgres-secret
key: postgres-user
- name: POSTGRES_DB
valueFrom:
secretKeyRef:
name: postgres-secret
key: postgres-db
- name: PGPASSWORD
valueFrom:
secretKeyRef:
name: postgres-secret
key: postgres-password
volumeMounts:
- name: backup-storage
mountPath: /backup
volumes:
- name: backup-storage
persistentVolumeClaim:
claimName: postgres-backup-pvc
restartPolicy: OnFailure
Monitoring and Observability
PostgreSQL Exporter for Prometheus
Deploy PostgreSQL Exporter alongside your database for comprehensive monitoring:
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres-exporter
namespace: postgres-system
spec:
replicas: 1
selector:
matchLabels:
app: postgres-exporter
template:
metadata:
labels:
app: postgres-exporter
spec:
containers:
- name: postgres-exporter
image: prometheuscommunity/postgres-exporter:latest
ports:
- containerPort: 9187
env:
- name: DATA_SOURCE_NAME
value: "postgresql://$(POSTGRES_USER):$(POSTGRES_PASSWORD)@postgres-service:5432/$(POSTGRES_DB)?sslmode=disable"
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: postgres-secret
key: postgres-user
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-secret
key: postgres-password
- name: POSTGRES_DB
valueFrom:
secretKeyRef:
name: postgres-secret
key: postgres-db
High Availability and Scaling Considerations
Approach | Pros | Cons | Use Case |
---|---|---|---|
Single StatefulSet | Simple setup, easy maintenance | No high availability, single point of failure | Development, testing environments |
Master-Slave Replication | Read scaling, backup availability | Manual failover, data lag | Read-heavy workloads |
PostgreSQL Operator (Zalando) | Automated HA, scaling, backups | Complex setup, learning curve | Production environments |
Cloud Provider Solutions | Managed service, automatic scaling | Vendor lock-in, higher costs | Enterprise applications |
Common Issues and Troubleshooting
Persistent Volume Issues
If your pods are stuck in Pending state, check PVC status:
kubectl get pvc -n postgres-system
kubectl describe pvc postgres-storage-postgres-0 -n postgres-system
Common solutions include:
- Verify storage class exists and is set as default
- Check node disk space and storage provisioner status
- Ensure proper RBAC permissions for storage provisioner
Database Connection Problems
Test connectivity from within the cluster:
kubectl run -it --rm debug --image=postgres:15-alpine --restart=Never -- psql -h postgres-service.postgres-system.svc.cluster.local -U myuser -d mydatabase
Performance Optimization
Monitor database performance with built-in PostgreSQL views:
SELECT * FROM pg_stat_activity WHERE state = 'active';
SELECT schemaname, tablename, n_tup_ins, n_tup_upd, n_tup_del FROM pg_stat_user_tables;
SELECT query, mean_time, calls FROM pg_stat_statements ORDER BY mean_time DESC LIMIT 10;
Security Best Practices
Network Policies
Implement network policies to restrict database access:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: postgres-network-policy
namespace: postgres-system
spec:
podSelector:
matchLabels:
app: postgres
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
name: application-namespace
ports:
- protocol: TCP
port: 5432
RBAC Configuration
Create dedicated service accounts with minimal required permissions:
apiVersion: v1
kind: ServiceAccount
metadata:
name: postgres-service-account
namespace: postgres-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: postgres-role
namespace: postgres-system
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list"]
- apiGroups: [""]
resources: ["persistentvolumeclaims"]
verbs: ["get", "list", "create"]
Real-World Use Cases and Performance Data
PostgreSQL in Kubernetes excels in several scenarios:
- Microservices Architecture – Each service can have dedicated database instances with proper resource isolation
- Multi-tenant Applications – Deploy separate PostgreSQL instances per tenant with namespace isolation
- CI/CD Pipelines – Spin up temporary database instances for testing and tear them down automatically
- Geographic Distribution – Deploy regional PostgreSQL clusters with data locality
Performance benchmarks for a typical PostgreSQL deployment in Kubernetes show:
Resource Allocation | Connections | TPS (Read/Write) | Latency (95th percentile) |
---|---|---|---|
1 CPU, 2GB RAM | 50 | 2,500 / 800 | 15ms |
2 CPU, 4GB RAM | 100 | 5,200 / 1,600 | 12ms |
4 CPU, 8GB RAM | 200 | 8,800 / 2,800 | 8ms |
For comprehensive PostgreSQL documentation and advanced configuration options, visit the official PostgreSQL documentation. The Kubernetes StatefulSet documentation provides additional insights into managing stateful applications in Kubernetes environments.
This deployment approach provides a solid foundation for running PostgreSQL in production Kubernetes environments. Remember to regularly update your PostgreSQL images, monitor performance metrics, and test your backup and recovery procedures to ensure optimal database operations.

This article incorporates information and material from various online sources. We acknowledge and appreciate the work of all original authors, publishers, and websites. While every effort has been made to appropriately credit the source material, any unintentional oversight or omission does not constitute a copyright infringement. All trademarks, logos, and images mentioned are the property of their respective owners. If you believe that any content used in this article infringes upon your copyright, please contact us immediately for review and prompt action.
This article is intended for informational and educational purposes only and does not infringe on the rights of the copyright owners. If any copyrighted material has been used without proper credit or in violation of copyright laws, it is unintentional and we will rectify it promptly upon notification. Please note that the republishing, redistribution, or reproduction of part or all of the contents in any form is prohibited without express written permission from the author and website owner. For permissions or further inquiries, please contact us.