Persist Data with a PVC
Store messages in SQLite on a PersistentVolumeClaim, verify restart survival, and understand stateful scaling tradeoffs.
Pods are replaceable. A PersistentVolumeClaim (PVC) asks Kubernetes for storage whose lifecycle is separate from a particular Pod. The persistent snapshot mounts one claim at /data, where FastAPI stores messages.db.
Download this chapter's files
Use your browser to download either ivia-chapter-10.tar.gz or ivia-chapter-10.zip. The archive is self-contained; you do not need this tutorial repository.
Open a terminal after the browser download:
cd ~/Downloads
tar -xzf ivia-chapter-10.tar.gz
cd ivia-chapter-10
All remaining relative paths in this chapter start from that extracted directory.
The challenge
Challenge: create a message, delete the backend Pod, and prove the replacement Pod reads the same message from persistent storage.
Prerequisites
- A running Minikube cluster with the ingress addon available.
- Helm, kubectl, and the Minikube node label.
- Test host resolution or willingness to use
curl --resolve. - About 1 GiB of provisionable Minikube storage.
Learning goals
- Create a chart-managed PVC
- Mount the PVC into a StatefulSet
- Verify data survival across Pod replacement
- Explain why this SQLite design deliberately uses one replica
Build persistent images
minikube addons enable ingress
kubectl label node minikube ivia.ch/nodetype=app --overwrite
minikube image build -t simple-backend:persistent persistent-app/backend
minikube image build -t simple-frontend:persistent persistent-app/frontend
Inspect the important values:
grep -n -E 'type:|replicaCount:|pvcs:|mountPath:|storage:|storageClassName:' \
persistent-app/values.yaml
The backend is a one-replica StatefulSet. Its unnamed claim becomes simple-app-backend-0, requests 1Gi, uses ReadWriteOnce, and mounts at /data.
Deploy and wait for storage
helm upgrade --install simple-app \
oci://harbor.ivia.ch/ivia-generic-helm-chart/ivia-generic-helm-chart \
--version 4.8.0 -n tutorial --atomic --timeout 5m \
-f persistent-app/values.yaml
kubectl get statefulset,pods,pvc -n tutorial
kubectl wait --for=condition=Ready pod \
-l app.kubernetes.io/component=backend \
-n tutorial --timeout=180s
Write, replace, and read
MINIKUBE_IP=$(minikube ip)
curl --fail --resolve api.simple-app.test:80:$MINIKUBE_IP \
-H 'Origin: http://simple-app.test' \
-H 'Content-Type: application/json' \
-d '{"text":"I survived a Pod restart!"}' \
http://api.simple-app.test/api/messages
curl --fail --resolve api.simple-app.test:80:$MINIKUBE_IP \
http://api.simple-app.test/api/messages
Delete only the Pod, not the claim:
BACKEND_POD=$(kubectl get pod -n tutorial \
-l app.kubernetes.io/component=backend \
-o jsonpath='{.items[0].metadata.name}')
kubectl delete pod "$BACKEND_POD" -n tutorial
kubectl wait --for=condition=Ready pod \
-l app.kubernetes.io/component=backend \
-n tutorial --timeout=180s
curl --fail --resolve api.simple-app.test:80:$MINIKUBE_IP \
http://api.simple-app.test/api/messages
Stateful tradeoffs
This KISS design is educational, not horizontally scalable. A ReadWriteOnce claim is normally mounted read-write by one node, and SQLite expects one shared local file. Two backend replicas could contend for the file or be unable to mount it as intended. For multiple replicas, use a networked database such as PostgreSQL, add migrations and backups, and keep API Pods stateless.
A StatefulSet gives stable Pod identity and ordered replacement; the PVC gives durable storage. Neither automatically provides backups, replication, encryption, or disaster recovery.
Expected validation
The PVC shows Bound; the backend Pod is 1/1 Running; both list requests contain I survived a Pod restart!; and the replacement Pod has a newer age than the PVC. The browser message board should show the same entry.
Troubleshooting and common pitfalls
- PVC stays
Pending: runkubectl get storageclassandkubectl describe pvc simple-app-backend-0 -n tutorial; Minikube normally providesstandard. - Permission denied at
/data: keep the image's/dataownership and non-root user setup intact; inspect backend logs. - Deleting the namespace or PVC: that can delete stored data. The challenge deletes only the Pod.
- Scaling to two replicas: do not scale this SQLite stage; migrate durable state to a multi-client database first.
- Health endpoint returns 503: it tests SQLite access; inspect mount events, filesystem permission, and logs.
- Assuming PVC equals backup: take an independent backup before upgrades or destructive tests.
Your message outlived its process. You have now experienced the key boundary between replaceable compute and durable state.