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.
hostPathbind mounts andterminationMessagePathfile 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.