search⌘K
search the log…
writing
$sysadmin$cybersecurity$devops$thoughts
homelabs
brasil homelabkubecraft homelab
site
about mecontact

This is the do-it version of the ephemeral vs persistent storage post. You run Postgres twice. The only thing that changes between the two runs is where it writes its data directory. You delete a pod both times and get opposite results.

Every step below shows the command and the output you should see, so you can tell “working” from “stuck” without guessing. The manifests are in the companion repo under manifests/.

Before you start

You need a running cluster and kubectl. k3s or k3d both work, since both ship local-path as the default StorageClass. Confirm it is there:

bash
kubectl get storageclass
text
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE AGE local-path (default) rancher.io/local-path Delete WaitForFirstConsumer 40d

Two columns there matter later. RECLAIMPOLICY: Delete means removing the claim wipes the data. VOLUMEBINDINGMODE: WaitForFirstConsumer means the volume is not created until a pod mounts it. Both show up as “gotchas” further down, except now you will know they are expected.

Part 1: ephemeral, watch the database vanish

Deploy Postgres on an emptyDir and wait for it to be ready.

bash
kubectl apply -f manifests/01-ephemeral-postgres.yaml kubectl rollout status deploy/pg-ephemeral
text
deployment.apps/pg-ephemeral created deployment "pg-ephemeral" successfully rolled out

If rollout status hangs for more than a few seconds, the image is probably still pulling. Check with kubectl get pods -l app=pg-ephemeral. A ContainerCreating or Pending state that clears on its own is normal on the first run.

Make a table and put two rows in it.

bash
kubectl exec deploy/pg-ephemeral -- psql -U postgres -c "create table notes (id serial, body text);" kubectl exec deploy/pg-ephemeral -- psql -U postgres -c "insert into notes (body) values ('first note'), ('second note');" kubectl exec deploy/pg-ephemeral -- psql -U postgres -c "select * from notes;"
text
CREATE TABLE INSERT 0 2 id | body ----+------------- 1 | first note 2 | second note (2 rows)

The data is real and it is there. Now break it.

Delete the pod. The Deployment replaces it. Read the table again.

bash
kubectl delete pod -l app=pg-ephemeral kubectl rollout status deploy/pg-ephemeral kubectl exec deploy/pg-ephemeral -- psql -U postgres -c "select * from notes;"
text
pod "pg-ephemeral-7d9c..." deleted deployment "pg-ephemeral" successfully rolled out ERROR: relation "notes" does not exist LINE 1: select * from notes; ^

The table is gone, not empty. Here is what just happened: the new pod got a brand new emptyDir, which is an empty directory. Postgres saw an empty data directory, assumed a fresh install, and ran initdb from scratch. Your table was never on that new disk. This is the failure the concept post opens with, reproduced in about thirty seconds.

Part 2: persistent, watch it survive

Create the claim first, then look at it before anything mounts it.

bash
kubectl apply -f manifests/02-pvc.yaml kubectl get pvc pg-data
text
persistentvolumeclaim/pg-data created NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE pg-data Pending local-path 3s

Pending with no volume looks broken. It is not. This is WaitForFirstConsumer from the storage class output earlier. The volume gets created when a pod actually needs it, not a second before.

Deploy Postgres on the claim. Now the PVC binds.

bash
kubectl apply -f manifests/03-persistent-postgres.yaml kubectl rollout status deploy/pg-persistent kubectl get pvc pg-data
text
deployment "pg-persistent" successfully rolled out NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE pg-data Bound pvc-8f3a1c22-... 1Gi RWO local-path 45s

Bound, with a real volume and 1Gi behind it.

Same table, same rows.

bash
kubectl exec deploy/pg-persistent -- psql -U postgres -c "create table notes (id serial, body text);" kubectl exec deploy/pg-persistent -- psql -U postgres -c "insert into notes (body) values ('first note'), ('second note');"

Delete the pod. Same command as Part 1. Read the table again.

bash
kubectl delete pod -l app=pg-persistent kubectl rollout status deploy/pg-persistent kubectl exec deploy/pg-persistent -- psql -U postgres -c "select * from notes;"
text
pod "pg-persistent-6b4f..." deleted deployment "pg-persistent" successfully rolled out id | body ----+------------- 1 | first note 2 | second note (2 rows)

Same deletion, opposite result. The new pod mounted the same claim, Postgres found its existing data directory, skipped initdb, and started on top of the data that was already there.

Where the data actually lives

The PVC is a real directory on the node’s disk. Find the path:

bash
kubectl get pv kubectl describe pv <volume-name> | grep -i path
text
Path: /var/lib/rancher/k3s/storage/pvc-8f3a1c22-.../

On k3s that directory is on the node’s real filesystem. On k3d the node is a container, so the same path is inside the k3d node container instead.

Gotchas and troubleshooting

These are the ones that actually cost me time. Each has a symptom so you can match it to what you are seeing.

You deleted the PVC and the data is gone. This is not a bug. local-path defaults to reclaimPolicy: Delete, so removing the claim wipes the directory on the node with no undo. Deleting the pod is safe. Deleting the claim is destructive. Keep them straight.

PVC stays Pending and never binds

If nothing is mounting it, that is expected (WaitForFirstConsumer). If a pod is scheduled and it still will not bind, check that the local-path provisioner is actually running:

bash
kubectl -n kube-system get pods | grep local-path kubectl describe pvc pg-data

The describe events at the bottom will tell you what it is waiting on.

Rollout hangs after an edit, new pod stuck

Symptom: you change the persistent Deployment, the rollout never finishes, and kubectl get pods shows the new pod stuck in ContainerCreating with a multi-attach error in its events. Cause: a ReadWriteOnce volume can attach to one pod at a time, and the default RollingUpdate tries to start the new pod before the old one lets go. Fix is already in the manifest:

yaml
spec: strategy: type: Recreate

Recreate tears the old pod down before bringing the new one up, so the volume is free when the replacement needs it.

Persistent Postgres crash-loops on first boot

Symptom: the pod goes CrashLoopBackOff, and kubectl logs shows initdb refusing because the data directory is not empty. Cause: some volumes hand you a directory that already contains a lost+found from a fresh filesystem. local-path gives you a clean directory so this lab is fine, but on other provisioners point PGDATA at a subdirectory:

yaml
env: - name: PGDATA value: /var/lib/postgresql/data/pgdata

Other things worth knowing

Cleanup

bash
kubectl delete -f manifests/01-ephemeral-postgres.yaml kubectl delete -f manifests/03-persistent-postgres.yaml kubectl delete -f manifests/02-pvc.yaml kubectl get pvc,pv

The last command should return nothing for these resources. Deleting the PVC releases the PV, and local-path removes the directory on the node.

kubernetesk3sstoragepostgreswalkthrough