Challenge, Easy,  on  Kubernetes

Kubernetes - emptyDir Volumes: Sharing, Lifecycle, and Memory-Backed Storage

An emptyDir volume defined in a pod is created when the pod is assigned to a node. All containers in the pod can mount it and share the same files. The volume's lifecycle matches the pod's: deleting the pod removes the volume and its contents.

The default medium for emptyDir is the node's disk - specifically, the filesystem under the kubelet's working directory (usually /var/lib/kubelet, configurable via --root-dir). The first three tasks use this default. Tasks 4, 5, and 6 switch to medium: Memory and explore its behavior.

On the node, each emptyDir volume lives at /var/lib/kubelet/pods/<pod-uid>/volumes/kubernetes.io~empty-dir/<volume-name>/. Task 6 uses this path to recover a pod stuck in a restart loop after the OOM.


Task 1 - Share Data Between Containers

Two containers in the same pod can exchange data through a shared emptyDir. One container writes, the other reads. No network, no external storage.

Steps:

  • Create a pod named shared with two containers writer and reader, both using image busybox
  • Define one emptyDir volume named shared-data at the pod level
  • Mount the volume at /shared in both containers
  • writer runs echo 'hello from writer' > /shared/data.txt && sleep 3600
  • reader runs sleep 3 && cat /shared/data.txt && sleep 3600
kubectl logs shared -c reader
Hint: shared volume between two containers

Define one volume at the pod level, then list it under volumeMounts in both containers with the same mountPath.

spec:
  volumes:
  - name: shared-data
    emptyDir: {}
  containers:
  - name: writer
    ...
    volumeMounts:
    - name: shared-data
      mountPath: /shared
  - name: reader
    ...
    volumeMounts:
    - name: shared-data
      mountPath: /shared

Task 2 - Volume Survives Container Crash

emptyDir is tied to the pod, not to the container. When a container crashes and the kubelet restarts it, the volume is still there with the same data. When the pod is deleted, the volume is gone.

To trigger a controlled container restart from the CLI, the pod uses a livenessProbe that checks for the absence of /tmp/die. Creating the file makes the probe fail and the kubelet restarts the container. Because /tmp lives in the container's writable layer, it is wiped on restart and the probe passes again. The mounted emptyDir at /data keeps its contents through the restart.

Steps:

  • Create a pod named crash-test with one container named app, using image busybox, running command sleep 3600
  • Define one emptyDir volume named data at the pod level
  • Mount the volume at /data in the container
  • Add a livenessProbe that runs test ! -f /tmp/die every 1 second (the probe fails when the file exists)
  • Write a non-empty file to /data/message.txt inside the container using kubectl exec
  • Create /tmp/die inside the container to make the probe fail
  • Verify restartCount is at least 1 and /data/message.txt still exists
kubectl get pod crash-test
kubectl exec crash-test -- cat /data/message.txt

The file persists across the container restart. The pod's restartCount increments while the contents of /data remain unchanged.

After creating /tmp/die, the restart can take a few seconds. The kubelet has to stop the old container before starting a new one, and the default wait for that is 30 seconds. To speed it up, set .spec.terminationGracePeriodSeconds: 5 on the pod.

To see the other side - delete crash-test and create a new pod with the same spec. The /data directory will be empty. A new pod always gets a fresh emptyDir.

Hint: kubectl exec to write into a running container

kubectl exec runs a command inside an existing container. Use it to write directly into the mounted volume without modifying the pod spec.

kubectl exec crash-test -- sh -c 'echo hello > /data/message.txt'
Hint: livenessProbe shape

The probe runs test ! -f /tmp/die. The command exits 0 when the file is absent (probe passes) and 1 when the file is present (probe fails). After enough consecutive failures the kubelet restarts the container.

livenessProbe:
  exec:
    command: ["sh", "-c", "test ! -f /tmp/die"]
  periodSeconds: 1
  failureThreshold: 1
Hint: trigger the restart

Create /tmp/die inside the running container. Within the probe's next period, the probe fails and the kubelet restarts the container.

kubectl exec crash-test -- touch /tmp/die
kubectl get pod crash-test -w

Watch RESTARTS go from 0 to 1, then Ctrl+C.

Hint: check restartCount

kubectl get with jsonpath extracts specific fields from the pod object. This returns the same restartCount value visible in kubectl describe, but as a single value suitable for scripting.

kubectl get pod crash-test -o jsonpath='{.status.containerStatuses[*].restartCount}'

Task 3 - Init Container Populates the Volume

An init container runs to completion before the main container starts. Mounting a shared emptyDir between them is a common pattern for pulling configs, rendering templates, or running setup scripts before the app runs.

Steps:

  • Create a pod named init-demo with an init container named setup and a main container named app, both using image busybox
  • Define one emptyDir volume named config at the pod level and mount it at /config in both containers
  • setup runs echo 'config loaded by init container' > /config/app.conf
  • app runs cat /config/app.conf && sleep 3600
kubectl logs init-demo -c app
Hint: init container with shared volume

The init container and the main container mount the same emptyDir. The init container writes the file and exits, then the main container starts and reads it.

spec:
  initContainers:
  - name: setup
    ...
    volumeMounts:
    - name: config
      mountPath: /config
  containers:
  - name: app
    ...
    volumeMounts:
    - name: config
      mountPath: /config

Task 4 - Memory-Backed emptyDir

Setting medium: Memory on an emptyDir makes it a tmpfs mount. Reads and writes go to RAM instead of disk, which is faster than a regular emptyDir. The storage counts against the container's memory limit. Pick medium: Memory only when the data is small and latency matters. Filling the volume past the memory limit will OOM-kill the container.

Steps:

  • Create a pod named mem-pod with one container named app, using image busybox, running command sleep 3600
  • Define one emptyDir volume named mem at the pod level with medium: Memory
  • Mount the volume at /mem in the container
  • Verify the mount is tmpfs
kubectl exec mem-pod -- mount | grep /mem
Hint: medium: Memory
volumes:
- name: mem
  emptyDir:
    medium: Memory

Task 5 - OOM-kill from a Memory-Backed emptyDir

The tmpfs storage for a medium: Memory emptyDir counts against the container's memory limit. Writing past that limit triggers the kernel OOM-killer, which terminates the container's main process. The kubelet then restarts the container, recording an OOMKilled event on the pod.

The OOMKilled reason is visible in kubectl describe pod (in the Events section and in the container state). The current pod status may show another reason like StartError or CrashLoopBackOff because the tmpfs file remains in the volume after the first OOM, and the restarted container often dies again for related reasons.

Steps:

  • Create a pod named oom-test with one container named app, image busybox, running command sleep 3600
  • Set resources.limits.memory: 50Mi on the container
  • Add an emptyDir volume named mem with medium: Memory, mounted at /mem (no sizeLimit)
  • Use kubectl exec to write 100Mi into /mem/big with dd if=/dev/zero of=/mem/big bs=1M count=100
  • Verify the container was OOM-killed and restarted
kubectl get pod oom-test
kubectl describe pod oom-test

The pod shows RESTARTS greater than 0. The describe output contains OOMKilled in either the container's Last State section or in the Events list. The currently visible status in kubectl get pod may be Error, StartError, or CrashLoopBackOff.

Setting sizeLimit on the emptyDir to a value smaller than the container's memory limit changes the failure mode. The write fails with No space left on device before the container exceeds its memory limit. The container stays Running and no OOM-kill occurs.

Hint: container memory limit

resources.limits.memory sets the upper bound on the container's memory cgroup. The kernel OOM-killer fires when the cgroup exceeds this value.

resources:
  limits:
    memory: 50Mi
Hint: fill the tmpfs with dd

dd reads zeros from /dev/zero and writes them to the target file. With bs=1M count=100, it writes 100Mi.

kubectl exec oom-test -- dd if=/dev/zero of=/mem/big bs=1M count=100

The command may fail with an error or the pod may restart mid-write. Either outcome is fine. kubectl describe pod oom-test shows the OOM-kill in the container's Last State section or in the Events list.


Task 6 - Restore the OOM-Crashed Pod

After the OOM, the container keeps restarting because /mem/big is still in the tmpfs volume across container restarts. Each new container hits the memory limit again. The fix is to remove the file from the node side so the next restart can succeed.

Steps:

  • Find the pod's UID
  • On cplane-01, navigate to the kubelet's emptyDir directory for this pod (see the intro for the path layout)
  • Locate and delete the big file from the mem volume
  • Wait for the pod to return to Running

The pod may take up to 5 minutes to return to Running. The kubelet's restart backoff doubles after each crash (10s, 20s, 40s, 80s, 160s, 300s) and caps at 5 minutes. If you delete the file right after a fresh crash, the wait can be just a few seconds. If you delete it after many restarts, the wait is closer to the 5-minute cap.

Hint: get the pod UID
kubectl get pod oom-test -o jsonpath='{.metadata.uid}'
Hint: open a root shell on the node

The kubelet's pod directory is owned by root with mode 700. Become root before walking into it.

sudo -i
Hint: walk into the volume on the node
cd /var/lib/kubelet/pods/<pod-uid>/volumes/kubernetes.io~empty-dir/
ls
cd mem
ls
Hint: remove the file
rm big
exit