Lab 3: Postgres + Adminer - Config, Secrets, Volumes, Probes
Pairs with Phase 3. You'll run Postgres with a persistent disk and Adminer (a web UI for databases) in front of it, wiring config through ConfigMap and Secret, then prove the data survives a pod kill.
Everything lives in its own namespace so it's easy to clean up:
kubectl create namespace lab3
kubectl config set-context --current --namespace=lab3(That second command makes lab3 the default namespace for this session, so you don't need -n lab3 on every command. Switch back later with ... --namespace=default.)
Step 1: Secret and ConfigMap
Save as lab-3/config.yaml:
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
type: Opaque
stringData:
POSTGRES_PASSWORD: s3cr3t-lab-password
---
apiVersion: v1
kind: ConfigMap
metadata:
name: db-config
data:
POSTGRES_DB: appdb
POSTGRES_USER: app
PGDATA: /var/lib/postgresql/data/pgdatastringData lets you write the plain value; k8s stores it base64-encoded. Prove to yourself it's encoding, not encryption:
kubectl apply -f lab-3/config.yaml
kubectl get secret db-credentials -o jsonpath='{.data.POSTGRES_PASSWORD}' | base64 -d; echoExpected: your password, printed right back. Anyone with read access to Secrets reads the values.
Step 2: Postgres with a PVC
Save as lab-3/postgres.yaml:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-data
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres
spec:
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:16-alpine
envFrom:
- configMapRef:
name: db-config
- secretRef:
name: db-credentials
ports:
- containerPort: 5432
readinessProbe:
exec:
command: ["pg_isready", "-U", "app", "-d", "appdb"]
initialDelaySeconds: 5
periodSeconds: 5
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
memory: 512Mi
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumes:
- name: data
persistentVolumeClaim:
claimName: postgres-data
---
apiVersion: v1
kind: Service
metadata:
name: postgres
spec:
selector:
app: postgres
ports:
- port: 5432What to notice before applying:
- The PVC requests a disk; kind's built-in
standardStorageClass provisions it automatically. envFrompulls all keys from the ConfigMap and Secret as env vars - exactly your composeenvironment:block, split by sensitivity.- The readiness probe runs
pg_isreadyinside the container; the Service withholds traffic until it passes. - Requests/limits: scheduler reserves 100m CPU and 128Mi; the container is killed if it exceeds 512Mi memory.
Deployment vs StatefulSet
A real database in k8s belongs in a StatefulSet (stable identity, one PVC per replica). With replicas: 1 a Deployment behaves the same and keeps this lab focused. Re-read the StatefulSet section of Phase 3 - and remember the honest take: prod databases usually belong in a managed service.
kubectl apply -f lab-3/postgres.yaml
kubectl get pvc
kubectl get pods -wExpected: PVC status Bound, and the pod goes Running 0/1 then Running 1/1 - that transition is the readiness probe passing.
Step 3: Adminer, gated by a readiness probe
Save as lab-3/adminer.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: adminer
spec:
replicas: 1
selector:
matchLabels:
app: adminer
template:
metadata:
labels:
app: adminer
spec:
containers:
- name: adminer
image: adminer
env:
- name: ADMINER_DEFAULT_SERVER
value: postgres
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /
port: 8080
initialDelaySeconds: 3
periodSeconds: 5
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
memory: 256Mi
---
apiVersion: v1
kind: Service
metadata:
name: adminer
spec:
selector:
app: adminer
ports:
- port: 8080ADMINER_DEFAULT_SERVER: postgres - that's cluster DNS again: Adminer reaches the database at hostname postgres, exactly like a compose service name.
kubectl apply -f lab-3/adminer.yaml
kubectl port-forward svc/adminer 8080:8080Open http://localhost:8080 and log in: server postgres, username app, password s3cr3t-lab-password, database appdb. You're in your k8s-hosted database. Ctrl-C the port-forward when done.
Step 4: Prove the data survives
Create data, kill the database pod, check the data is still there:
kubectl exec deploy/postgres -- psql -U app -d appdb \
-c "CREATE TABLE survivors (note text); INSERT INTO survivors VALUES ('still here');"
kubectl delete pod -l app=postgres
kubectl get pods -w # wait for the new postgres pod to reach 1/1, then Ctrl-C
kubectl exec deploy/postgres -- psql -U app -d appdb -c "SELECT * FROM survivors;"Expected:
note
-----------
still here
(1 row)The pod died, a new one took its place, and the PVC reattached. Compare: without the PVC, that table would be gone.
Verify it worked
- [ ] You decoded the Secret and understand why base64 is not security
- [ ] You watched
0/1become1/1and can say what the readiness probe was doing - [ ] You logged into Adminer using the
postgresservice name as the host - [ ] Your table survived a pod kill
Keep this namespace running - Lab 6 converts these manifests to a Kustomize base. Switch your default namespace back:
kubectl config set-context --current --namespace=defaultNext: Lab 4: Ingress