Persist App Data with a PersistentVolume and Claim
Deployment `records-app` in namespace `records` appends a line to `/data/records.log` every five seconds, but `/data` is an `emptyDir`, so the records are lost whenever the Pod is replaced. This cluster has a single node; the data may live under `/srv/records` on that node.
Live lab access
This task description is public. Live practice requires full access unless this is the selected free weekly task. AI Lab Coach is available in single-task labs, including the free weekly task. See this week's free task.
Create an account to continue →
On a phone? Read the task and create your account here, then sign in on a desktop or laptop for the terminal lab. Nothing starts until you choose Start.
The task
Deployment records-app in namespace records appends a line to /data/records.log every five seconds, but /data is an emptyDir, so the records are lost whenever the Pod is replaced. This cluster has a single node; the data may live under /srv/records on that node.
Requirements
1. Create PersistentVolume records-pv with capacity 256Mi, access mode ReadWriteOnce, reclaim policy Retain, storage class manual and a hostPath of /srv/records.
2. Create PersistentVolumeClaim records-pvc in records that requests 128Mi with access mode ReadWriteOnce from storage class manual.
3. Ensure the claim is bound to records-pv.
4. Change records-app so the data volume uses records-pvc instead of emptyDir, keeping the mount at /data, and ensure the new Pod writes its records there.
The cluster also has a default StorageClass; this claim must use the volume you created.
Original Prepium guide · Written for Kubernetes v1.35 and checked against the official documentation on 6 October 2026. Use a disposable practice cluster. A single-node one is best, because the example volume is a directory on one node.
Make a claim bind to your volume, not to the default StorageClass
A PersistentVolumeClaim never names its disk directly by default; the control plane looks for a PersistentVolume whose class, access modes and size all satisfy the claim, then binds the two one-to-one. The trap on clusters with a default StorageClass is that a claim which says nothing about its class is quietly given the default class, gets a freshly provisioned volume, and ignores the volume you made by hand.
notes-pv (300Mi, RWO, class "") ◀── bound ── notes-claim (100Mi, RWO, class "")
▲ volume in Deployment notes-writer at /notes
1. See what the cluster would do by default
kubectl get storageclass
kubectl get pv
A class marked (default) carries the annotation storageclass.kubernetes.io/is-default-class: "true". If one exists, any new claim without a storageClassName key is assigned it, and can then bind only to volumes of that class.
2. Create a classless volume and a claim that asks for no class
Save this as ckad-pvc-notes.yaml. Both objects set storageClassName: "". On the claim, the explicit empty string means "a volume with no class", which also stops the default from being filled in. Leaving the key out would not mean the same thing.
apiVersion: v1
kind: PersistentVolume
metadata:
name: notes-pv
spec:
capacity:
storage: 300Mi
accessModes:
- ReadWriteOnce
storageClassName: "" # no class: only claims asking for "" can bind
persistentVolumeReclaimPolicy: Retain
hostPath:
path: /var/tmp/prepium-notes
type: DirectoryOrCreate
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: notes-claim
namespace: prepium-tutorial
spec:
storageClassName: "" # explicit empty string, so the default class is NOT applied
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 100Mi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: notes-writer
namespace: prepium-tutorial
spec:
replicas: 1
selector:
matchLabels:
app: notes-writer
template:
metadata:
labels:
app: notes-writer
spec:
containers:
- name: writer
image: busybox:1.36
command: ["sh", "-c", "while true; do date >> /notes/stamps.txt; sleep 10; done"]
volumeMounts:
- name: notes
mountPath: /notes
volumes:
- name: notes
persistentVolumeClaim:
claimName: notes-claim
kubectl create namespace prepium-tutorial
kubectl apply -f ckad-pvc-notes.yaml
kubectl get pv notes-pv
kubectl -n prepium-tutorial get pvc notes-claim
Both should report Bound, and the PV's CLAIM column names prepium-tutorial/notes-claim. The claim's CAPACITY shows 300Mi, not 100Mi: a claim gets at least what it asked for, and a statically created volume is bound whole.
3. Know the matching rules
- Class must be identical, including both being empty.
- Access modes requested by the claim must be offered by the volume.
ReadWriteOncemeans one node may mount it read-write, which still lets several Pods on that node share it. - Size: the volume's capacity must be at least the request.
When nothing matches, the claim stays Pending indefinitely and binds as soon as a matching volume appears. To force one particular volume, set spec.volumeName on the claim; the other rules still apply.
4. Prove the data outlives the Pod
kubectl -n prepium-tutorial rollout status deployment/notes-writer
kubectl -n prepium-tutorial exec deploy/notes-writer -- tail -n 2 /notes/stamps.txt
kubectl -n prepium-tutorial rollout restart deployment/notes-writer
kubectl -n prepium-tutorial rollout status deployment/notes-writer
kubectl -n prepium-tutorial exec deploy/notes-writer -- wc -l /notes/stamps.txt
The new Pod finds the earlier lines; with an emptyDir the count would restart from zero.
hostPath caveats
hostPath is a directory on whichever node runs the Pod, so it is only trustworthy on a single-node practice cluster; on several nodes a rescheduled Pod can land beside a different, empty directory. DirectoryOrCreate creates a missing directory with mode 0755 owned like the kubelet, so a non-root container may not be able to write to it. Host mounts also carry real security risk, which is why production uses a CSI driver or a local volume instead.
Common mistakes
- Omitting
storageClassNameon the claim in a cluster with a default class: it binds to a new dynamic volume and your PV staysAvailable. - Requesting more than the volume holds, or
ReadWriteManyfrom a volume that only offersReadWriteOnce: the claim staysPending. - Creating the claim in a different namespace from the Deployment. A Pod can only use claims in its own namespace.
- Deleting the PV first during cleanup. Removal is postponed while a claim is still bound to it, so the delete appears to hang until the claim is gone.
| Example check | Evidence |
|---|---|
| Claim bound to the intended volume | spec.volumeName on the claim names your PV; both show Bound |
| Matching attributes | Same storageClassName; compatible access modes; capacity at least the request |
| Workload uses the claim | volumes[].persistentVolumeClaim.claimName in the Pod template |
| Data persists | Line count keeps growing across a rollout restart |
Cleanup: kubectl delete namespace prepium-tutorial, then kubectl delete pv notes-pv (PVs are cluster-scoped and Retain keeps it around). The files stay in /var/tmp/prepium-notes on the node until you remove them. References: Persistent Volumes, hostPath volumes, Storage Classes. Next, try binding to a named PV or StorageClass binding modes.
Practice scope
This additional curriculum lab is available in single-task practice with the existing 12-minute limit. The three CKAD mocks keep their original 17-task sets. Hints, an explained solution and requirement-by-requirement feedback are available in the lab.
Browse CKAD practice tasks →