PostgreSQL on Kubernetes with Longhorn Storage
This guide explains how to deploy PostgreSQL on Kubernetes using StatefulSets with Longhorn storage for persistent data. We’ll set up a production-ready PostgreSQL instance with proper resource management, health checks, and encrypted secrets using FluxCD GitOps workflow.
Why Use StatefulSets for PostgreSQL?
Benefits of this setup:
- Persistent data: Survives pod restarts and rescheduling
- Longhorn storage: Distributed storage with replication and backup capabilities
- Resource management: Proper CPU and memory limits
- Health monitoring: Liveness and readiness probes
- Encrypted secrets: Secure credential management with SOPS
Prerequisites
- Kubernetes cluster with Longhorn storage installed
- FluxCD installed and configured in your cluster
- Worker nodes with
nfs-common
package installed - Git repository configured with FluxCD
- SOPS configured for secret encryption (see FluxCD SOPS guide)
Step 1: Prepare Worker Nodes
-
Install NFS utilities on worker nodes
Connect to each worker node and install the required package:
Terminal window sudo apt install -y nfs-common -
Verify Longhorn storage class
Terminal window kubectl get storageclass longhorn
Step 2: Create Namespace
Create namespace manifest
Create namespace.yaml
:
apiVersion: v1kind: Namespacemetadata: name: postgres labels: name: postgres
Step 3: Create PostgreSQL Secret
-
Create secret manifest
Create
secret.yaml
with your PostgreSQL credentials:apiVersion: v1kind: Secretmetadata:name: postgres-secretnamespace: postgreslabels:app: postgrestype: OpaquestringData:POSTGRES_DB: main_dbPOSTGRES_USER: thomasPOSTGRES_PASSWORD: YourSecurePassword123! -
Encrypt the secret with SOPS
Before committing to Git, encrypt the secret:
Terminal window sops --encrypt --in-place secret.yaml -
Verify encryption The secret values should now be encrypted:
apiVersion: v1kind: Secretmetadata:name: postgres-secretnamespace: postgreslabels:app: postgrestype: OpaquestringData:POSTGRES_DB: ENC[AES256_GCM,data:...POSTGRES_USER: ENC[AES256_GCM,data:...POSTGRES_PASSWORD: ENC[AES256_GCM,data:...
Step 4: Create PostgreSQL StatefulSet
-
Create StatefulSet manifest
Create
deployment.yaml
:apiVersion: apps/v1kind: StatefulSetmetadata:name: postgresnamespace: postgresspec:replicas: 1serviceName: postgrespersistentVolumeClaimRetentionPolicy:whenDeleted: RetainwhenScaled: Retainselector:matchLabels:app: postgrestemplate:metadata:labels:app: postgresspec:containers:- name: postgresimage: "postgres:14.8"imagePullPolicy: IfNotPresentports:- containerPort: 5432env:- name: PGDATAvalue: /var/lib/postgresql/data/pgdataenvFrom:- secretRef:name: postgres-secretresources:requests:memory: "256Mi"cpu: "250m"limits:memory: "1Gi"cpu: "1000m"livenessProbe:exec:command:- /bin/sh- -c- exec pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB" -h 127.0.0.1 -p 5432initialDelaySeconds: 30periodSeconds: 10timeoutSeconds: 5failureThreshold: 6readinessProbe:exec:command:- /bin/sh- -c- exec pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB" -h 127.0.0.1 -p 5432initialDelaySeconds: 5periodSeconds: 10timeoutSeconds: 5failureThreshold: 6volumeMounts:- mountPath: /var/lib/postgresql/dataname: postgresdatavolumeClaimTemplates:- metadata:name: postgresdatalabels:app: postgresspec:storageClassName: longhornaccessModes:- ReadWriteOnceresources:requests:storage: 20Gi -
Understanding the configuration Key configuration details:
- PGDATA: Custom data directory to avoid permission issues
- Resources: Memory and CPU limits for production use
- Probes: Health checks ensure database is ready and responsive
- Volume: 20GB persistent storage with Longhorn
- Retention policy: Keeps data when StatefulSet is deleted
Step 5: Create PostgreSQL Service
-
Create service manifest
Create
service.yaml
:apiVersion: v1kind: Servicemetadata:name: postgresnamespace: postgreslabels:app: postgresspec:type: NodePortports:- protocol: TCPname: postgresport: 5432targetPort: 5432nodePort: 30002selector:app: postgres -
Service configuration options Choose the appropriate service type for your needs:
- NodePort: External access via node IP and port 30002
- ClusterIP: Internal cluster access only (change
type: ClusterIP
and removenodePort
) - LoadBalancer: External load balancer (if supported by your cluster)
Step 6: Create Kustomization
-
Create kustomization manifest
Create
kustomization.yaml
to deploy all resources together:apiVersion: kustomize.config.k8s.io/v1beta1kind: Kustomizationresources:- namespace.yaml- secret.yaml- deployment.yaml- service.yaml -
Add to infrastructure kustomization
PostgreSQL is managed as part of my infrastructure setup. Make sure your postgress kustomization is being picked up by flux:
apiVersion: kustomize.config.k8s.io/v1beta1kind: Kustomizationresources:- ./traefik- ./longhorn- ./postgres -
Repository structure
infrastructure/├── kustomization.yaml # Main infrastructure kustomization├── traefik/├── longhorn/└── postgres/├── namespace.yaml├── secret.yaml├── deployment.yaml├── service.yaml└── kustomization.yaml
Step 7: Deploy via GitOps
-
Commit PostgreSQL files to Git
Terminal window git add infrastructure/postgres/git commit -m "Add PostgreSQL deployment to infrastructure"git push origin main -
Wait for FluxCD to reconcile FluxCD will automatically detect the changes and deploy PostgreSQL as part of your infrastructure:
Terminal window # Watch infrastructure reconciliationflux get kustomizations -w# Check infrastructure kustomization statusflux get kustomization infrastructure -
Verify deployment
Terminal window # Check StatefulSet statuskubectl get statefulset -n postgreskubectl get pods -n postgres# Verify persistent volumekubectl get pvc -n postgreskubectl get pv# Check servicekubectl get service -n postgres -
Monitor logs
Terminal window kubectl logs -n postgres postgres-0
Step 8: Test PostgreSQL Connection
-
Connect from within the cluster
Terminal window kubectl exec -it -n postgres postgres-0 -- psql -U thomas -d main_db -
Test basic operations
-- Create a test tableCREATE TABLE test_table (id SERIAL PRIMARY KEY, name VARCHAR(50));-- Insert test dataINSERT INTO test_table (name) VALUES ('Hello PostgreSQL');-- Query the dataSELECT * FROM test_table;-- Exit\q -
Connect externally (if using NodePort)
Terminal window psql -h <node-ip> -p 30002 -U thomas -d main_db