If you are not familiar with the oc command, refer to OpenShift - Getting Started with the oc command.
Config Map
Since the name of the Postgres database and the username are not sensitive data, these values can be stored in a Config Map. Check out my article FreeKB - OpenShift - Create Config Map. Let's say you have the following in a YAML file.
apiVersion: v1
kind: ConfigMap
metadata:
name: postgres
labels:
app: postgres
data:
POSTGRESQL_DATABASE: my-postgres-db
POSTGRESQL_USER: admin
The oc apply command with the -f or --filename option can be used to create the config map using the template YAML file.
~]$ oc apply --filename configmap.yml
configmap/postgres created
The oc get configmaps command can be used to list the config maps that have been created.
~]$ oc get configmaps
NAME DATA AGE
postgres 2 11m
And you can validate that the config maps contains the postgres database name and username.
~]$ oc get configmap postgres --output jsonpath="{.data}"
{"POSTGRESQL_DATABASE":"my-postgres-db","POSTGRESQL_USER":"admin"}
Secret
Since the Postgres password is sensitive data, this should be created in a secret. Check out my article FreeKB - OpenShift - Create Opaque Generic Secrets (key value pairs). Let's say you want your admin password to be itsasecret. The base64 command can be used to get the base64 of itsasecret.
~]$ echo itsasecret | base64
aXRzYXNlY3JldAo=
Let's say you have the following in a YAML file. Notice the value is aXRzYXNlY3JldAo=, which is the base64 of itsasecret.
apiVersion: v1
kind: Secret
metadata:
name: postgres-secret
labels:
app: postgres
data:
POSTGRESQL_PASSWORD: aXRzYXNlY3JldAo=
The oc apply command with the -f or --filename option can be used to create the secret using the template YAML file.
~]$ oc create --filename secret.yml
secret/postgres-secret created
The oc get secrets command can be used to list the secrets that have been created.
~]# oc get secrets
NAME TYPE DATA AGE
postgres-secret Opaque 1 30s
And you can validate that the secret contains the base64 password.
]$ oc get secret postgres-secret --output jsonpath="{.data}"
{"POSTGRESQL_PASSWORD":"aXRzYXNlY3JldAo="}
Service
By default, postgres listens for connections on port 5432. Check out my article FreeKB - OpenShift - Create ClusterIP Service. Let's say you have the following in a YAML file.
apiVersion: v1
kind: Service
metadata:
name: postgres
labels:
app: postgres
spec:
selector:
app: postgres
ports:
- protocol: TCP
port: 5432
targetPort: 5432
type: ClusterIP
The oc apply command with the -f or --filename option can be used to create the service using the template YAML file.
~]$ oc create --filename service.yml
service/postgres created
The oc get services command can be used to list the services that have been created.
]$ oc get services
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
postgres ClusterIP 10.11.12.13 <none> 5432/TCP 24m
Persistent Volume and Persistent Volume Claim
First and foremost, a Persistent Volume will be needed for storage for the Postgres database. Check out my article FreeKB - OpenShift - Create Persistent Volume Claim (pvc) for details on how to create a Persistent Volume (pv) and Persistent Volume Claim (pvc). The oc get storageclass command can be used to list the available storage classes.
storageClassName must be an exact match of one of the storage classes returned by the oc get storageclass command.
~]$ oc get storageclass
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
file-storage (default) csi.trident.netapp.io Delete Immediate true 377d
thin kubernetes.io/vsphere-volume Delete Immediate false 381d
accessMode can contain one or more of the following:
- ReadWriteOnce (RWO) - The volume may only be mounted on a single node
- ReadWriteOncePod (RWOP) - The volume may only be mounted on a single pod
- ReadOnlyMany (ROX) - The volume may be mounted on different nodes
- ReadWriteMany (RWX) - The volume can be mounted on different nodes
In this example, the file-storage storage class is used.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-pvc
namespace: my-project
spec:
accessModes:
- ReadOnlyOnce
- ReadWriteOnce
resources:
requests:
storage: 5Gi
storageClassName: file-storage
The oc apply command with the -f or --filename option can be used to create the persistent volume claim using the template YAML file.
~]$ oc apply -f create_pvc.yml
persistentvolumeclaim/postgres-pvc created
The oc get persistentvolumeclaims (or oc get pvc) command will return the list of persistent volume claims. The important thing is to ensure the Persistent Volume Claim is bound to a Persistent Volume.
~]$ oc get pvc --output wide
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE VOLUMEMODE
postges-pvc Bound pvc-2db07c57-e282-48e7-bfb1-4cbd7245c25e 1Gi RWM file-storage 3m29s Filesystem
And the oc get persistentvolumes (or oc get pv) command should list the Persistent Volume.
~]$ oc get pv pvc-2db07c57-e282-48e7-bfb1-4cbd7245c25e
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
pvc-2db07c57-e282-48e7-bfb1-4cbd7245c25e 5Gi RWM Delete Bound my-project/postgres-pvc file-storage 19m
Stateful Set
Now that we have the config map, secret, and persistent volume / persistent volume claim, we can create the stateful set.
The oc get images command can be used to list the postgres images that have been imported into your OpenShift cluster.
]$ oc get images | grep -i postgres
sha256:0b939c13john.doe43a85b813ec8c170adcb7479507ed4e592d982296f745c62e registry.redhat.io/rhel8/postgresql-15@sha256:0b939c13john.doe43a85b813ec8c170adcb7479507ed4e592d982296f745c62e
sha256:12b1d5a86864d21d6594384edfe5cccc94205dbf689fc12345678901237060a5 registry.redhat.io/rhel8/postgresql-13@sha256:12b1d5a86864d21d6594384edfe5cccc94205dbf689fc12345678901237060a5
sha256:1d1d42ba374084d59308f44aafdab883145b06ca109aa7c2f362a578a6abf318 registry.redhat.io/rhel9/postgresql-13@sha256:1d1d42ba374084d59308f44aafdab883145b06ca109aa7c2f362a578a6abf318
sha256:72a202313710713359429b9bbb172c4363efe32d9a1490d25f5e313041022c7a registry.redhat.io/rhel8/postgresql-13@sha256:72a202313710713359429b9bbb172c4363efe32d9a1490d25f5e313041022c7a
sha256:a1c75f08c929a4992816b8b91f9372852aefe46e183d3cf0464dd5dbe3d836a1 registry.redhat.io/rhel8/postgresql-10@sha256:a1c75f08c929a4992816b8b91f9372852aefe46e183d3cf0464dd5dbe3d836a1
sha256:a21e2a6d92d4ec02421219d6a382f379a12d3ee7a8abe3e74fc01ba0cf56a232 registry.redhat.io/rhscl/postgresql-10-rhel7@sha256:a21e2a6d92d4ec02421219d6a382f379a12d3ee7a8abe3e74fc01ba0cf56a232
sha256:bbe3dd60fefb35dbaf2ee8f45d0b0b903fc5ceab9f58fb522ebd3d9aee4f535f registry.redhat.io/rhel8/postgresql-12@sha256:bbe3dd60fefb35dbaf2ee8f45d0b0b903fc5ceab9f58fb522ebd3d9aee4f535f
sha256:c6fde1a8653a597c18b0326bc71ce4a614273be74b9aef3ced83a1b11472687a registry.redhat.io/rhscl/postgresql-13-rhel7@sha256:c6fde1a8653a597c18b0326bc71ce4a614273be74b9aef3ced83a1b11472687a
sha256:d05fd5870e03045baeb1113c913b290e56616a307a559c067c6a57cfcb60e1a2 registry.redhat.io/rhel9/postgresql-15@sha256:d05fd5870e03045baeb1113c913b290e56616a307a559c067c6a57cfcb60e1a2
sha256:d99bb8c63bbdbbfb1bd1a1f4e1a5f114dc5a0d972cdd9d27ca9e69c16cb98089 registry.redhat.io/rhscl/postgresql-12-rhel7@sha256:d99bb8c63bbdbbfb1bd1a1f4e1a5f114dc5a0d972cdd9d27ca9e69c16cb98089
Let's say you have the following in a YAML file. You may need to update image to reference one of the postgres images in your cluster
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
labels:
app: postgres
spec:
replicas: 1
serviceName: postgres
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- env:
- name: POSTGRESQL_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-secret
key: POSTGRESQL_PASSWORD
envFrom:
- configMapRef:
name: postgres
name: postgres
image: registry.redhat.io/rhel9/postgresql-15
imagePullPolicy: "IfNotPresent"
ports:
- containerPort: 5432
volumeMounts:
- mountPath: /var/lib/postgresql/data
name: postgres-vm
resources:
limits:
memory: "2Gi"
cpu: "1"
requests:
memory: "1Gi"
cpu: "0.5"
volumes:
- name: postgres-vm
persistentVolumeClaim:
claimName: postgres
The oc apply command with the -f or --filename option can be used to create the stateful set using the template YAML file.
~]$ oc apply -f statefulset.yml
statefulset/postgres created
The oc get statefulsets command will return the list of stateful sets. Since the stateful set YAML had replicas: 1 this is why 1 pod was created by the stateful set. In this example, since ready is 1/1 this means there should be a running postgres pod.
]$ oc get statefulsets
NAME READY AGE
postgres 1/1 27m
And the oc get pods command can be used to ensure the postgres pod is running.
$ oc get pods
NAME READY STATUS RESTARTS AGE
postgres-0 1/1 Running 0 29m
And the pod logs should return something like this.
]$ oc logs pod/postgres-0
The files belonging to this database system will be owned by user "postgres".
This user must also own the server process.
The database cluster will be initialized with locale "en_US.utf8".
The default database encoding has accordingly been set to "UTF8".
The default text search configuration will be set to "english".
Data page checksums are disabled.
fixing permissions on existing directory /var/lib/pgsql/data/userdata ... ok
creating subdirectories ... ok
selecting dynamic shared memory implementation ... posix
selecting default max_connections ... 100
selecting default shared_buffers ... 128MB
selecting default time zone ... UTC
creating configuration files ... ok
running bootstrap script ... ok
performing post-bootstrap initialization ... ok
syncing data to disk ... ok
initdb: warning: enabling "trust" authentication for local connections
initdb: hint: You can change this by editing pg_hba.conf or using the option -A, or --auth-local and --auth-host, the next time you run initdb.
Success. You can now start the database server using:
pg_ctl -D /var/lib/pgsql/data/userdata -l logfile start
waiting for server to start....2025-10-09 01:15:43.003 UTC [35] LOG: redirecting log output to logging collector process
2025-10-09 01:15:43.003 UTC [35] HINT: Future log output will appear in directory "log".
done
server started
/var/run/postgresql:5432 - accepting connections
=> sourcing /usr/share/container-scripts/postgresql/start/set_passwords.sh ...
ALTER ROLE
waiting for server to shut down.... done
server stopped
Starting server...
2025-10-09 01:15:43.244 UTC [1] LOG: redirecting log output to logging collector process
2025-10-09 01:15:43.244 UTC [1] HINT: Future log output will appear in directory "log".
Did you find this article helpful?
If so, consider buying me a coffee over at 