The image reads the password from a file (POSTGRES_PASSWORD_FILE), so Database Connection's Secret mounts as for the API, and PostgreSQL 18 1,289 's claim mounts at /var/lib/postgresql (Persisting PostgreSQL):
apiVersion: v1
kind: Service
metadata: { name: postgres, labels: { app: booknest, tier: db } }
spec:
clusterIP: None
selector: { app: booknest, tier: db }
ports: [{ name: postgres, port: 5432 }]
---
apiVersion: apps/v1
kind: StatefulSet
metadata: { name: postgres, labels: { app: booknest, tier: db } }
spec:
serviceName: postgres
replicas: 1
selector: { matchLabels: { app: booknest, tier: db } }
template:
metadata: { labels: { app: booknest, tier: db } }
spec:
containers:
- name: postgres
image: localhost:33500/postgres:18
env:
- { name: POSTGRES_DB, value: booknest }
- name: POSTGRES_USER
valueFrom: { secretKeyRef: { name: db-credentials, key: username } }
- { name: POSTGRES_PASSWORD_FILE, value: /run/secrets/db/password }
ports: [{ name: postgres, containerPort: 5432 }]
readinessProbe:
exec: { command: [sh, -c, 'pg_isready -U "$POSTGRES_USER" -d booknest'] }
volumeMounts:
- { name: data, mountPath: /var/lib/postgresql }
- { name: db-credentials, mountPath: /run/secrets/db, readOnly: true }
volumes: [{ name: db-credentials, secret: { secretName: db-credentials } }]
volumeClaimTemplates:
- metadata: { name: data }
spec:
storageClassName: booknest-data
accessModes: [ReadWriteOnce]
resources: { requests: { storage: 1Gi } }An empty database is where Deploying the API's warning bites: three API replicas starting together would all run CREATE TABLE and seed at once. A Job (Batch and Node-Level Workloads) runs BookNest's own db.init() exactly once instead:
apiVersion: batch/v1
kind: Job
metadata: { name: db-init, labels: { app: booknest } }
spec:
backoffLimit: 4
template:
spec:
restartPolicy: OnFailure
containers:
- name: init
image: localhost:33500/booknest-api:1.4
command: [node, -e, "const db = require('./db'); db.init().then(() => db.close())"]
envFrom: [{ configMapRef: { name: api-config } }]
env:
- name: PGUSER
valueFrom: { secretKeyRef: { name: db-credentials, key: username } }
- { name: PGPASSWORD_FILE, value: /run/secrets/db/password }
volumeMounts: [{ name: db-credentials, mountPath: /run/secrets/db, readOnly: true }]
volumes: [{ name: db-credentials, secret: { secretName: db-credentials } }]Stop the API, generate a password, start PostgreSQL, point api-config at postgres, run the Job, restart the API, then delete postgres-0 to see the claim outlive its Pod:
kubectl scale deployment api --replicas=0 >/dev/null
kubectl create secret generic db-credentials --from-literal=username=booknest \
--from-literal=password="$(openssl rand -hex 16)" --dry-run=client -o yaml | kubectl replace -f -
kubectl apply -f k8s/postgres.yaml && kubectl rollout status sts/postgres | tail -1
sed -i 's/PGHOST: db,/PGHOST: postgres,/' k8s/api-config.yaml
kubectl apply -f k8s/api-config.yaml
kubectl apply -f k8s/db-init-job.yaml && kubectl wait --for=condition=Complete job/db-init
kubectl scale deployment api --replicas=3 >/dev/null && kubectl rollout status deploy/api | tail -1
kubectl get pvc -o custom-columns=NAME:.metadata.name,STATUS:.status.phase,\
CLASS:.spec.storageClassName,SIZE:.status.capacity.storage
kubectl delete pod postgres-0 && kubectl wait --for=condition=Ready pod/postgres-0 >/dev/null
kubectl exec postgres-0 -- psql -U booknest -d booknest -tAc 'SELECT count(*) FROM books'
kubectl delete pod,service db >/dev/null
git add k8s && git commit -qm "Run PostgreSQL as a StatefulSet and create the schema with a Job"Output
secret/db-credentials replaced service/postgres created statefulset.apps/postgres created partitioned roll out complete: 1 new pods have been updated... configmap/api-config configured job.batch/db-init created job.batch/db-init condition met deployment "api" successfully rolled out NAME STATUS CLASS SIZE data-postgres-0 Bound booknest-data 1Gi pod "postgres-0" deleted from booknest namespace 6
The Pod came back as postgres-0 on the same claim, with all six books, and the throwaway db is gone.