The first time storage bit me, I had Postgres running in a pod, I deleted the pod to restart it, and every table was gone when it came back. Not empty. Gone. Nothing warned me it was going to happen.
That is the whole reason this distinction exists. This post is the concept: what ephemeral and persistent storage are, how you ask for each, when to use which, and why the difference matters. The hands-on version, where you reproduce that failure on purpose and watch it not happen the second time, is the separate walkthrough linked at the bottom.
What it is
Two categories. The whole topic is knowing which one you are standing on.
Ephemeral storage lives and dies with the workload. Two kinds:
- The container’s own writable layer. Invisible, default, wiped when the container restarts.
emptyDir. A volume attached to the pod. It survives a container crash inside the pod, but it is deleted the moment the pod is removed.
Persistent storage outlives the pod. Three objects working together:
- A
PersistentVolumeClaim(PVC) is your request. “I want 1Gi.” - A
PersistentVolume(PV) is the actual disk that request binds to. - A
StorageClassprovisions that disk automatically, so you are not hand-creating volumes.
| Storage | Survives container restart | Survives pod delete |
|---|---|---|
| Writable layer | No | No |
emptyDir | Yes | No |
| PVC | Yes | Yes |
How to use it
Ephemeral storage you get by default. If you want a named scratch volume, that is an emptyDir on the pod:
The volume starts empty and is scoped to the pod. Nothing to size, nothing to provision. When the pod goes, it goes.
Persistent storage is two steps. First you claim it:
Three lines in there are doing the work. accessModes: [ReadWriteOnce] means the volume can be mounted read and write by one node at a time. Most block storage is like this, and it is the setting that later dictates how you can roll a pod that uses it. storageClassName: local-path picks which provisioner makes the disk. local-path is what k3s ships with, and it carves the volume out as a directory on the node. requests.storage: 1Gi is how much you are asking for.
Then you mount that claim into the pod like any other volume:
Notice the pod references the claim by name. It never names a disk. The claim binds to a PV the StorageClass created, and the pod just asks for the claim. That indirection is the point: your workload does not need to know what storage is underneath it.
When to reach for which
The test I use is simple. If losing the contents would matter tomorrow, it needs a PVC. If I could throw it away and rebuild it without caring, ephemeral is fine.
Ephemeral (emptyDir or the writable layer) is for cache, temp files, a scratch area, or handing files between two containers in the same pod. Things you can regenerate.
Persistent (a PVC) is for databases, user uploads, anything with state you expect to still be there after a restart. If it holds the only copy of something, it goes on a PVC.
Why it matters
Kubernetes assumes your workload can die and come back anywhere. To make that safe, it treats the container filesystem as disposable. A web server that holds nothing in itself does not care. A database does.
Here is the difference made concrete, which is the whole shape of the walkthrough. Run Postgres on an emptyDir, create a table, delete the pod. The replacement pod comes up with a fresh empty directory, Postgres re-initializes from scratch, and the table is gone. Run the same Postgres on a PVC, create the same table, delete the pod. The replacement mounts the same claim, Postgres finds its existing data, and the table is still there. Same deletion, opposite result, and the only thing that changed was where the data was written.
The cost of not knowing is the story at the top. You do not find out your storage was ephemeral until something you needed is already gone.
Things that trip people up
emptyDirfeels durable because it survives a container crash. It does not survive a pod delete, a node drain, or a reschedule. It is not the safety net people assume.- Once you have a PVC, deleting the pod keeps your data. Deleting the claim destroys it. Those are two very different deletes, and the second one has no undo on
local-path. local-pathkeeps the data on one node’s disk and pins the pod back to that node. It survives the pod, not the node. There are no copies, so a dead disk is a dead database.
The hands-on version: Walkthrough: proving ephemeral vs persistent storage with Postgres. Every command, the output you should see at each step, and a troubleshooting section for the parts that look broken but are not.