Lab Objective
In this lab, you will deploy a PostgreSQL database in OpenShift using the oc CLI tool. You will learn how to:
- Create a new OpenShift project.
- Store database credentials in a Kubernetes Secret.
- Create persistent storage for database data.
- Deploy PostgreSQL using a Deployment.
- Expose PostgreSQL internally using a Service.
- Connect to the database from another pod inside the same project.
- Create a table, insert data, and query data.
- Troubleshoot common database deployment issues.
- Clean up all lab resources.
Important Concept
A database should normally not be exposed to the public internet using an OpenShift Route.
In this lab:
- PostgreSQL runs inside the OpenShift cluster.
- Other pods connect to PostgreSQL using the internal Service name.
- The database Service is called
postgresql. - Applications inside the same project can connect to it using:
postgresql:5432
Code language: CSS (css)
Lab Architecture
Student Terminal
|
| oc CLI
v
OpenShift Project: lab8-database-cli
|
+-- Secret: postgresql-secret
|
+-- PVC: postgresql-data
|
+-- Deployment: postgresql
| |
| +-- Pod: PostgreSQL database
|
+-- Service: postgresql
|
+-- Pod: pg-client
|
+-- connects to postgresql:5432
Prerequisites
Before starting this lab, students should have:
- Access to an OpenShift cluster.
- The
ocCLI installed. - Permission to create a project.
- Permission to create pods, deployments, services, secrets, and PVCs.
- A working internet connection from the cluster to pull the container image.
This lab works best on:
- OpenShift Local / CRC
- OpenShift Developer Sandbox
- OpenShift Container Platform 4.x
- OKD 4.x
Database Used in This Lab
| Item | Value |
|---|---|
| Database | PostgreSQL |
| Version | PostgreSQL 15 |
| Database image | quay.io/sclorg/postgresql-15-c9s:latest |
| Database service name | postgresql |
| Database port | 5432 |
| Database user | student |
| Database password | studentpass123 |
| Database name | studentdb |
Step 1: Verify oc CLI
Run:
oc version
Expected output:
Client Version: ...
Server Version: ...
Kubernetes Version: ...
If you only see the client version and no server version, you are not logged in yet.
Step 2: Log in to OpenShift
For OpenShift Local / CRC, first get credentials:
crc console --credentials
Code language: JavaScript (javascript)
Then log in using the command shown by CRC, for example:
oc login https://api.crc.testing:6443 -u developer -p developer
Code language: JavaScript (javascript)
For OpenShift Developer Sandbox, copy the login command from the web console and paste it into your terminal. It usually looks like this:
oc login --token=<your-token> --server=<your-api-server>
Code language: HTML, XML (xml)
Verify login:
oc whoami
Expected output:
developer
or your OpenShift username.
Step 3: Create a New Project
Create a new project for this lab:
oc new-project lab8-database-cli
Code language: JavaScript (javascript)
Expected output:
Now using project "lab8-database-cli" on server ...
Code language: JavaScript (javascript)
Verify the current project:
oc project
Expected output:
Using project "lab8-database-cli" on server ...
Code language: JavaScript (javascript)
Step 4: Create a Secret for Database Credentials
Create a Secret to store the database username, password, and database name.
oc create secret generic postgresql-secret \
--from-literal=POSTGRESQL_USER=student \
--from-literal=POSTGRESQL_PASSWORD=studentpass123 \
--from-literal=POSTGRESQL_DATABASE=studentdb
Code language: JavaScript (javascript)
Expected output:
secret/postgresql-secret created
Verify the Secret exists:
oc get secret postgresql-secret
Code language: JavaScript (javascript)
Expected output:
NAME TYPE DATA AGE
postgresql-secret Opaque 3 ...
Do not print secret values in a real production environment. This is a training lab, so we are using simple demo credentials.
Step 5: Create Persistent Storage for PostgreSQL
Create a PersistentVolumeClaim for database storage:
oc apply -f - <<'EOF'
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgresql-data
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
EOF
Code language: JavaScript (javascript)
Expected output:
persistentvolumeclaim/postgresql-data created
Check the PVC:
oc get pvc postgresql-data
Code language: JavaScript (javascript)
Expected output when storage is available:
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
postgresql-data Bound ... 1Gi RWO ... ...
If the status is Pending, your cluster may not have a default StorageClass or you may not have storage quota. Continue to the troubleshooting section and use the ephemeral fallback.
Step 6: Deploy PostgreSQL
Create the PostgreSQL Deployment:
oc apply -f - <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgresql
labels:
app: postgresql
spec:
replicas: 1
selector:
matchLabels:
app: postgresql
template:
metadata:
labels:
app: postgresql
spec:
containers:
- name: postgresql
image: quay.io/sclorg/postgresql-15-c9s:latest
imagePullPolicy: IfNotPresent
ports:
- name: postgresql
containerPort: 5432
envFrom:
- secretRef:
name: postgresql-secret
volumeMounts:
- name: postgresql-data
mountPath: /var/lib/pgsql/data
volumes:
- name: postgresql-data
persistentVolumeClaim:
claimName: postgresql-data
EOF
Code language: JavaScript (javascript)
Expected output:
deployment.apps/postgresql created
Wait for the deployment to become ready:
oc rollout status deployment/postgresql --timeout=180s
Expected output:
deployment "postgresql" successfully rolled out
Code language: JavaScript (javascript)
Verify the pod:
oc get pods -l app=postgresql
Code language: JavaScript (javascript)
Expected output:
NAME READY STATUS RESTARTS AGE
postgresql-xxxxxxxxxx-xxxxx 1/1 Running 0 ...
Step 7: Create an Internal Service for PostgreSQL
Create a Service so other pods can connect to PostgreSQL using a stable DNS name.
oc expose deployment/postgresql \
--port=5432 \
--target-port=5432 \
--name=postgresql
Expected output:
service/postgresql exposed
Verify the Service:
oc get svc postgresql
Code language: JavaScript (javascript)
Expected output:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
postgresql ClusterIP ... <none> 5432/TCP ...
Code language: HTML, XML (xml)
Important: This Service is internal to the cluster. That is correct for a database.
Step 8: Check Database Logs
View PostgreSQL logs:
oc logs deployment/postgresql --tail=50
Expected output should include PostgreSQL startup messages.
Example output:
PostgreSQL init process complete; ready for start up.
database system is ready to accept connections
Exact log text can vary, but the important idea is that PostgreSQL should be ready to accept connections.
Step 9: Create a Temporary PostgreSQL Client Pod
Create a temporary client pod that includes the psql command-line tool.
oc run pg-client \
--image=quay.io/sclorg/postgresql-15-c9s:latest \
--restart=Never \
-- sleep 3600
Expected output:
pod/pg-client created
Wait until the client pod is ready:
oc wait --for=condition=Ready pod/pg-client --timeout=120s
Expected output:
pod/pg-client condition met
Verify both pods:
oc get pods
Code language: JavaScript (javascript)
Expected output:
NAME READY STATUS RESTARTS AGE
pg-client 1/1 Running 0 ...
postgresql-xxxxxxxxxx-xxxxx 1/1 Running 0 ...
Step 10: Test Database Connectivity
From the client pod, connect to PostgreSQL through the Service name.
oc exec pg-client -- sh -lc \
'PGPASSWORD=studentpass123 psql -h postgresql -U student -d studentdb -c "SELECT version();"'
Code language: JavaScript (javascript)
Expected output:
version
----------------------------------------------------------------------------------------------------
PostgreSQL 15 ...
(1 row)
This proves:
- The PostgreSQL pod is running.
- The Service name
postgresqlresolves inside the project. - The username and password work.
- The database
studentdbexists.
Step 11: Create a Table
Create a table named students.
oc exec pg-client -- sh -lc \
'PGPASSWORD=studentpass123 psql -h postgresql -U student -d studentdb -c "CREATE TABLE IF NOT EXISTS students (id SERIAL PRIMARY KEY, name TEXT NOT NULL, city TEXT NOT NULL);"'
Code language: JavaScript (javascript)
Expected output:
CREATE TABLE
Step 12: Insert Sample Data
Insert three records:
oc exec pg-client -- sh -lc \
'PGPASSWORD=studentpass123 psql -h postgresql -U student -d studentdb -c "INSERT INTO students (name, city) VALUES ('"'"'Rajesh'"'"', '"'"'Tokyo'"'"'), ('"'"'Asha'"'"', '"'"'Bengaluru'"'"'), ('"'"'Miguel'"'"', '"'"'Madrid'"'"');"'
Code language: JavaScript (javascript)
Expected output:
INSERT 0 3
Step 13: Query the Data
Query the table:
oc exec pg-client -- sh -lc \
'PGPASSWORD=studentpass123 psql -h postgresql -U student -d studentdb -c "SELECT * FROM students;"'
Code language: JavaScript (javascript)
Expected output:
id | name | city
----+--------+-----------
1 | Rajesh | Tokyo
2 | Asha | Bengaluru
3 | Miguel | Madrid
(3 rows)
Step 14: Open an Interactive SQL Session
You can also open an interactive psql session:
oc exec -it pg-client -- sh
Inside the client pod, run:
export PGPASSWORD=studentpass123
psql -h postgresql -U student -d studentdb
Code language: JavaScript (javascript)
Inside psql, try:
\dt
SELECT * FROM students;
\q
Exit the pod shell:
exit
Code language: PHP (php)
Step 15: Inspect the Running Resources
Show all major resources in the project:
oc get all
Code language: JavaScript (javascript)
Expected resources include:
pod/pg-client
pod/postgresql-...
service/postgresql
deployment.apps/postgresql
replicaset.apps/postgresql-...
Show only database-related resources:
oc get deployment,svc,pvc,secret,pod -l app=postgresql
Code language: JavaScript (javascript)
Note: The Secret and PVC may not show with this label because we did not label them. You can list them directly:
oc get secret postgresql-secret
oc get pvc postgresql-data
Code language: JavaScript (javascript)
Describe the PostgreSQL Deployment:
oc describe deployment postgresql
Describe the PostgreSQL Service:
oc describe service postgresql
Step 16: Optional Local Port Forwarding
If your local machine has the psql client installed, you can forward PostgreSQL to your local machine.
Start port forwarding:
oc port-forward svc/postgresql 5432:5432
Expected output:
Forwarding from 127.0.0.1:5432 -> 5432
Forwarding from [::1]:5432 -> 5432
Code language: CSS (css)
Keep this terminal open.
In another terminal, connect locally:
PGPASSWORD=studentpass123 psql -h 127.0.0.1 -U student -d studentdb
Exit psql:
\q
Stop port forwarding with:
Ctrl+C
Step 17: Why We Do Not Create a Route for PostgreSQL
Do not run this for the database:
oc expose service/postgresql
A Route is normally used for HTTP/HTTPS web applications. PostgreSQL is a database service and should usually stay private inside the cluster.
Correct database access pattern:
Application Pod -> Service postgresql -> PostgreSQL Pod
Incorrect beginner pattern:
Internet -> Route -> PostgreSQL
Step 18: Do Not Scale This Database Deployment
Do not scale this PostgreSQL Deployment to multiple replicas:
oc scale deployment/postgresql --replicas=2
A single PostgreSQL database using one ReadWriteOnce volume should not be scaled like a stateless web application. For real high availability, use a database Operator or managed database service.
For this lab, keep:
oc scale deployment/postgresql --replicas=1
Expected output:
deployment.apps/postgresql scaled
Troubleshooting
Problem 1: PVC Stays Pending
Check the PVC:
oc get pvc postgresql-data
Code language: JavaScript (javascript)
If it shows:
STATUS: Pending
Code language: HTTP (http)
Possible reasons:
- No default StorageClass.
- No available persistent volumes.
- Storage quota exceeded.
Check storage classes:
oc get storageclass
Code language: JavaScript (javascript)
If your training cluster does not support persistent storage, use the ephemeral fallback below.
Problem 2: Pod Shows ImagePullBackOff
Check pod status:
oc get pods
Code language: JavaScript (javascript)
Describe the pod:
oc describe pod -l app=postgresql
Possible reasons:
- Cluster cannot pull from the image registry.
- Network egress is blocked.
- Registry is temporarily unavailable.
Problem 3: Pod Shows CrashLoopBackOff
Check logs:
oc logs deployment/postgresql --tail=100
Common causes:
- Secret name is wrong.
- Required environment variables are missing.
- Storage mount has permission problems.
Problem 4: Client Cannot Resolve Hostname
If this command fails:
oc exec pg-client -- sh -lc \
'PGPASSWORD=studentpass123 psql -h postgresql -U student -d studentdb -c "SELECT version();"'
Code language: JavaScript (javascript)
Check that the Service exists:
oc get svc postgresql
Code language: JavaScript (javascript)
Check that both pods are in the same project:
oc project
oc get pods
Code language: JavaScript (javascript)
Ephemeral Fallback: Use This Only If PVC Is Not Available
If your cluster does not provide persistent storage, delete the existing deployment and PVC:
oc delete deployment postgresql --ignore-not-found=true
oc delete pvc postgresql-data --ignore-not-found=true
Code language: JavaScript (javascript)
Create an ephemeral PostgreSQL deployment using emptyDir:
oc apply -f - <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgresql
labels:
app: postgresql
spec:
replicas: 1
selector:
matchLabels:
app: postgresql
template:
metadata:
labels:
app: postgresql
spec:
containers:
- name: postgresql
image: quay.io/sclorg/postgresql-15-c9s:latest
imagePullPolicy: IfNotPresent
ports:
- name: postgresql
containerPort: 5432
envFrom:
- secretRef:
name: postgresql-secret
volumeMounts:
- name: postgresql-data
mountPath: /var/lib/pgsql/data
volumes:
- name: postgresql-data
emptyDir: {}
EOF
Code language: JavaScript (javascript)
Wait for rollout:
oc rollout status deployment/postgresql --timeout=180s
If the Service already exists, keep it. If not, create it:
oc get svc postgresql || oc expose deployment/postgresql --port=5432 --target-port=5432 --name=postgresql
Code language: JavaScript (javascript)
Warning: With emptyDir, database data is lost when the pod is deleted or recreated. Use this only for temporary training labs.
Cleanup Option 1: Delete Only Lab Resources
Delete the client pod:
oc delete pod pg-client --ignore-not-found=true
Code language: JavaScript (javascript)
Delete the PostgreSQL Service:
oc delete service postgresql --ignore-not-found=true
Code language: JavaScript (javascript)
Delete the PostgreSQL Deployment:
oc delete deployment postgresql --ignore-not-found=true
Code language: JavaScript (javascript)
Delete the PVC:
oc delete pvc postgresql-data --ignore-not-found=true
Code language: JavaScript (javascript)
Delete the Secret:
oc delete secret postgresql-secret --ignore-not-found=true
Code language: JavaScript (javascript)
Verify cleanup:
oc get all
oc get pvc
oc get secret postgresql-secret
Code language: JavaScript (javascript)
Cleanup Option 2: Delete the Whole Project
If this project was created only for the lab, delete the whole project:
oc delete project lab8-database-cli
Code language: JavaScript (javascript)
Expected output:
project.project.openshift.io "lab8-database-cli" deleted
Code language: CSS (css)
Lab Review Questions
- Why did we store database credentials in a Secret instead of directly writing them in the Deployment?
- Why did we create a Service for PostgreSQL?
- Why did we not create an OpenShift Route for PostgreSQL?
- What is the difference between persistent storage and
emptyDir? - Why should this PostgreSQL Deployment not be scaled to multiple replicas?
- How would an application in the same project connect to this database?
Key Takeaways
- Databases in OpenShift should normally be exposed internally with Services, not public Routes.
- Secrets are used to store sensitive configuration such as database usernames and passwords.
- PersistentVolumeClaims are used to request durable storage.
- Pods should connect to databases using Service DNS names.
- PostgreSQL listens on port
5432. - Stateful databases require different scaling and availability patterns than stateless web applications.
I’m a DevOps/SRE/DevSecOps/Cloud Expert passionate about sharing knowledge and experiences. I have worked at Cotocus. I share tech blog at DevOps School, travel stories at Holiday Landmark, stock market tips at Stocks Mantra, health and fitness guidance at My Medic Plus, product reviews at TrueReviewNow , and SEO strategies at Wizbrand.
Do you want to learn Quantum Computing?
Please find my social handles as below;
Rajesh Kumar Personal Website
Rajesh Kumar at YOUTUBE
Rajesh Kumar at INSTAGRAM
Rajesh Kumar at X
Rajesh Kumar at FACEBOOK
Rajesh Kumar at LINKEDIN
Rajesh Kumar at WIZBRAND
Find Trusted Cardiac Hospitals
Compare heart hospitals by city and services — all in one place.
Explore Hospitals
The tutorial explains database management using the
ocCLI clearly, but it would be even more useful if it included considerations for running databases in production OpenShift environments. Areas such as storage provisioning, backup and restore planning, secret rotation, and handling database upgrades are essential for maintaining reliability. Additionally, incorporating guidance on performance monitoring and capacity planning would help teams identify potential issues before they affect application availability.