Skip to content

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:

bash
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:

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/pgdata

stringData lets you write the plain value; k8s stores it base64-encoded. Prove to yourself it's encoding, not encryption:

bash
kubectl apply -f lab-3/config.yaml
kubectl get secret db-credentials -o jsonpath='{.data.POSTGRES_PASSWORD}' | base64 -d; echo

Expected: 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:

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: 5432

What to notice before applying:

  • The PVC requests a disk; kind's built-in standard StorageClass provisions it automatically.
  • envFrom pulls all keys from the ConfigMap and Secret as env vars - exactly your compose environment: block, split by sensitivity.
  • The readiness probe runs pg_isready inside 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.

bash
kubectl apply -f lab-3/postgres.yaml
kubectl get pvc
kubectl get pods -w

Expected: 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:

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: 8080

ADMINER_DEFAULT_SERVER: postgres - that's cluster DNS again: Adminer reaches the database at hostname postgres, exactly like a compose service name.

bash
kubectl apply -f lab-3/adminer.yaml
kubectl port-forward svc/adminer 8080:8080

Open 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:

bash
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/1 become 1/1 and can say what the readiness probe was doing
  • [ ] You logged into Adminer using the postgres service 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:

bash
kubectl config set-context --current --namespace=default

Next: Lab 4: Ingress

A VineLab lab. Released under the MIT License.