k3sm licensed apache-2.0 (DCO)

Storage that survives the pod

Claim a local-path PersistentVolume, see the node affinity it carries, and watch the data outlive the Pod that wrote it.


Budget 20 minutes, including building a small binary. You bind a PVC to a local-path PersistentVolume on your Mac’s APFS filesystem, write into it from a Pod, delete that Pod, and watch a fresh one rebind to the same data.

1. One Thing to Settle First#

The workload has to be a compiled binary, not a shell script. k3sm Pods run at real host paths with no chroot and no mount namespace, so a volume declared at an absolute path such as /var/lib/demo is materialized under the Pod’s data directory and made to resolve at the declared path by a DYLD_INSERT_LIBRARIES path-rebase shim. That shim loads into ordinary native workloads, your Go or C binary, but macOS strips the variable from SIP platform binaries, which is every /bin/sh and /usr/bin/*. A shell script that reads a mounted file at its absolute path will not see it.

So build something small first. Anything that writes to a directory it is told about will do:

GOOS=darwin GOARCH=arm64 go build -o /opt/demo/bin/counter ./cmd/counter

Point command[0] at that absolute path in the manifests below.

There is a second setup detail to handle before the PVC fails on you. An unprivileged k3sm server run without --pod-root roots the runtime under your home directory, and the sandbox generator denies /Users unconditionally, so a PVC in that setup is refused with a protected-path error. Pass --pod-root to relocate the on-disk root off /Users. --work-dir alone only moves control-plane state and does not change the pods root. See limitations.

2. Claim a Volume#

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: data
  namespace: default
spec:
  accessModes: ["ReadWriteOnce"]
  storageClassName: local-path
  resources:
    requests:
      storage: 1Gi
k3sm kubectl apply -f pvc.yaml
k3sm kubectl get pvc data

The claim stays Pending, and that is correct. The local-path StorageClass binds WaitForFirstConsumer, so nothing is provisioned until the scheduler has chosen a node for a Pod that uses the claim.

3. Mount It#

apiVersion: v1
kind: Pod
metadata:
  name: writer
  namespace: default
spec:
  nodeSelector:
    kubernetes.io/os: darwin
  tolerations:
    - key: k3sm.io/provider
      operator: Exists
      effect: NoSchedule
  restartPolicy: Never
  containers:
    - name: writer
      image: native
      command:
        - /opt/demo/bin/counter
        - --data-dir=/var/lib/demo
      volumeMounts:
        - name: data
          mountPath: /var/lib/demo
      resources:
        requests:
          memory: 32Mi
        limits:
          memory: 128Mi
  volumes:
    - name: data
      persistentVolumeClaim:
        claimName: data
k3sm kubectl apply -f writer.yaml
k3sm kubectl get pvc,pv

The claim is now Bound. The provisioner created a directory on this Mac’s APFS filesystem and a PersistentVolume whose node affinity names this node.

4. See the Pin#

k3sm kubectl get pv -o jsonpath='{.items[0].spec.nodeAffinity}'

Because the data lives on one machine’s disk, every future Pod bound to this claim can only be scheduled back to that machine. In a multi-node cluster your stateful Pods are pinned to wherever their data is; plan placement around it.

The pinning is a consequence of where the volume was provisioned, applied by the scheduler through the PV’s node affinity. Never write nodeName yourself. A hand-pinned Pod bypasses the scheduler, and therefore bypasses the volume-node checks that keep a Pod and its data on the same Mac.

5. Survive the Pod#

k3sm kubectl delete pod writer
k3sm kubectl get pvc data           # still Bound, same VOLUME
k3sm kubectl apply -f writer.yaml   # a new Pod, the same volume

The claim keeps its volume, and the new Pod reads what the old one wrote. Deleting the claim does not delete the data either. The local-path class reclaims with Retain, so the directory and the PV stay behind and are removed by hand.

6. What Is Not Supported#

  • Volume resize, snapshots, and generic ephemeral volumes are planned and not implemented yet.
  • hostPath bind mounts and terminationMessagePath file mounts are a documented ceiling of the native substrate, because there are no Linux bind mounts to implement them with.
  • Networked and distributed storage classes are out of scope for the local-path model.

ConfigMaps, Secrets, emptyDir, and projected volumes all work, and all carry the same compiled-binary requirement as above.

Next#

  • statefulset covers per-replica claims and stable identity.
  • storage is the reference page.
  • back up and restore covers the control-plane datastore, which is a different thing from your PV data.