Storage

Volumes & Volume Mounts

● Intermediate ⏱ 12 min read

A container's filesystem is ephemeral — when the container restarts, everything written to disk is gone. Kubernetes Volumes solve this by attaching storage to a pod that outlives any individual container restart. Volumes also enable multiple containers in a pod to share data through a common directory. This guide covers the built-in volume types you'll use most often; PersistentVolumes (for durable, node-independent storage) are covered in the next guide.

Why Volumes?

Containers need external storage for two distinct reasons:

A Volume's lifetime is tied to the pod, not the container. If a container restarts due to a crash, the volume data is preserved. If the pod is deleted and rescheduled, ephemeral volumes like emptyDir start fresh — for persistent data that survives pod deletion, use PersistentVolumes.

Volume Anatomy

Every volume requires two entries: a declaration in spec.volumes giving the volume a name and type, and one or more volumeMounts inside container specs linking that name to a filesystem path.

volume structure
spec:
  volumes:                    # declare volumes at the pod level
  - name: shared-data         # any name you choose
    emptyDir: {}              # volume type
  - name: app-config
    configMap:
      name: my-config         # reference an existing ConfigMap

  containers:
  - name: app
    image: my-app:latest
    volumeMounts:             # mount inside this container
    - name: shared-data       # must match a volume name above
      mountPath: /data        # where it appears in the container
    - name: app-config
      mountPath: /etc/config
      readOnly: true          # optional: read-only mount

emptyDir

emptyDir starts as an empty directory when the pod is scheduled to a node. It lives for the lifetime of the pod. Multiple containers in the same pod can mount the same emptyDir volume and read/write to it concurrently.

emptydir-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: cache-pod
spec:
  volumes:
  - name: cache
    emptyDir: {}          # default: stored on node disk
  - name: ramdisk
    emptyDir:
      medium: Memory      # stored in RAM (tmpfs) — faster, uses node memory
      sizeLimit: 256Mi    # cap RAM usage

  containers:
  - name: app
    image: my-app:latest
    volumeMounts:
    - name: cache
      mountPath: /var/cache/app
    - name: ramdisk
      mountPath: /tmp/fast

Use medium: Memory for high-performance scratch space — temp files, shared memory IPC. The data counts against the container's memory limit. Without a sizeLimit, the ramdisk can grow until it exhausts node memory.

hostPath

hostPath mounts a file or directory from the node's filesystem into the pod. The node path must exist before the pod starts (unless type: DirectoryOrCreate is used).

volumes:
- name: docker-sock
  hostPath:
    path: /var/run/docker.sock
    type: Socket           # File, Directory, Socket, CharDevice, BlockDevice
                           # DirectoryOrCreate / FileOrCreate: create if missing
⚠️
hostPath is a security risk — avoid in production

A container that mounts hostPath: / or /etc can read and write the entire node filesystem. Mounting /var/run/docker.sock gives full Docker daemon access, which is equivalent to root on the node. Use hostPath only for system-level DaemonSets (log collectors, monitoring agents) that genuinely need node-level access, and restrict it with PodSecurity policies.

ConfigMap & Secret Volumes

ConfigMaps and Secrets can be projected into the filesystem as files, not just environment variables. This is useful for config files, TLS certificates, and credentials that apps expect to read from disk.

configmap-volume.yaml
apiVersion: v1
kind: Pod
metadata:
  name: configured-app
spec:
  volumes:
  - name: config
    configMap:
      name: nginx-config
      items:                    # optional: select specific keys
      - key: nginx.conf         # ConfigMap key
        path: nginx.conf        # filename in the volume
  - name: tls-certs
    secret:
      secretName: tls-secret
      defaultMode: 0400         # file permissions (octal)

  containers:
  - name: nginx
    image: nginx:1.27
    volumeMounts:
    - name: config
      mountPath: /etc/nginx/conf.d
    - name: tls-certs
      mountPath: /etc/nginx/tls
      readOnly: true

When the ConfigMap or Secret is updated, Kubernetes eventually reflects the change in the mounted files — typically within the kubelet's sync period (default 60s). Environment variables from ConfigMaps/Secrets do not update automatically; volume mounts do.

Projected Volumes

A projected volume combines multiple sources into a single mount directory — a ConfigMap, a Secret, a ServiceAccount token, and the pod's downward API data, all accessible under one path:

volumes:
- name: all-in-one
  projected:
    sources:
    - configMap:
        name: app-config
    - secret:
        name: app-secret
    - serviceAccountToken:
        path: token
        expirationSeconds: 3600   # auto-rotated token
    - downwardAPI:
        items:
        - path: pod-name
          fieldRef:
            fieldPath: metadata.name

Multi-Container Sharing

The most common use of emptyDir is sharing a directory between a main container and a sidecar. The sidecar pattern below has a log-shipper reading from a shared log directory:

Sidecar log-shipping with shared emptyDir
pod boundary (emptyDir lives here)
main container
app
writes to /var/log/app/ ↓
emptyDir
logs
/var/log/app/
shared
sidecar
log-shipper
reads /var/log/app/ ↑
ships logs to Elasticsearch / Loki →
Both containers mount the same emptyDir name — changes by one are immediately visible to the other
The emptyDir volume is a shared directory within the pod. The app writes logs; the sidecar ships them. No network call, no external coordination.
sidecar-log-shipping.yaml
spec:
  volumes:
  - name: logs
    emptyDir: {}

  containers:
  - name: app
    image: my-app:latest
    volumeMounts:
    - name: logs
      mountPath: /var/log/app

  - name: log-shipper
    image: fluent/fluent-bit:latest
    volumeMounts:
    - name: logs
      mountPath: /var/log/app   # same path, same volume name
    env:
    - name: OUTPUT_HOST
      value: elasticsearch.logging:9200

subPath Mounts

By default, mounting a volume replaces the entire target directory. subPath lets you mount a single key from a ConfigMap (or a subdirectory) into an existing directory without overwriting the rest of its contents:

volumeMounts:
- name: config
  mountPath: /etc/nginx/nginx.conf    # mount a single file
  subPath: nginx.conf                  # key from the ConfigMap

# Without subPath: /etc/nginx/ would be replaced entirely by the ConfigMap
# With subPath: only nginx.conf is added/replaced; other files in /etc/nginx/ are untouched
⚠️
subPath volumes don't auto-update

Files mounted with subPath do not reflect ConfigMap/Secret changes automatically. The whole-directory mount (without subPath) will update within the sync period. If you need live config updates, avoid subPath and use the full directory mount instead.

Init Container Pattern

Init containers run to completion before the main container starts. A common pattern uses an emptyDir to pass prepared data from an init container to the main container — downloading assets, rendering config templates, seeding a database schema:

init-container-volume.yaml
spec:
  volumes:
  - name: config
    emptyDir: {}

  initContainers:
  - name: init-config
    image: alpine:3.20
    command: ["/bin/sh", "-c"]
    args:
    - |
      wget -O /config/settings.json https://config-service/settings
    volumeMounts:
    - name: config
      mountPath: /config

  containers:
  - name: app
    image: my-app:latest
    volumeMounts:
    - name: config
      mountPath: /app/config    # reads settings.json written by init container

kubectl Commands

# Describe a pod and see its volume definitions and mounts
kubectl describe pod my-pod

# Verify a volume is mounted inside the container
kubectl exec my-pod -- ls /data
kubectl exec my-pod -- cat /etc/config/nginx.conf

# Check what's in an emptyDir (from inside the pod)
kubectl exec my-pod -c app -- df -h /var/cache

# Verify a ConfigMap volume is up to date
kubectl exec my-pod -- cat /etc/config/settings.json

# Debug mount issues — look for "Unhealthy" events
kubectl describe pod my-pod | grep -A 20 Events

# List volumes on a running pod (jsonpath)
kubectl get pod my-pod \
  -o jsonpath='{.spec.volumes[*].name}'