How To Setup NFS Storage for Kubernetes
For workloads that require massive storage (like media servers) or need to be accessible from any node in the cluster, local SSDs (local-path-provisioner) won't work. The data must live on the network.
A classic and highly effective homelab solution is to convert one of your worker nodes (the one with the largest HDDs) into an NFS Server and expose that storage directly to the Kubernetes cluster.
Step 1: Configure the NFS Server
Log into the node that houses your large hard drives (e.g., k8s-worker-02).
-
Install the NFS Kernel Server:
-
Create the Shared Directory:
-
Export the Directory to the Subnet: Edit
/etc/exportsand add a line explicitly allowing your local subnet (e.g.,192.168.1.0/24) to mount the drive:(Note:echo "/mnt/media 192.168.1.0/24(rw,sync,no_subtree_check,no_root_squash)" | sudo tee -a /etc/exportsno_root_squashallows containers running as root to write to the share as root. This is generally required for many Docker containers, though less secure in enterprise environments). -
Apply and Restart:
Step 1.5: Prepare the Cluster Nodes (The Client-Side)
Kubernetes does not have its own built-in NFS client. When a Pod requests an NFS volume, the underlying physical node it is scheduled on uses the host OS's native mount.nfs utility to attach the drive.
If this utility is missing from the node, the Pod will be permanently stuck in ContainerCreating with a cryptic fsconfig() failed: NFS: mount program didn't pass remote address error.
You must install the client package on every worker and control plane node in the cluster:
Step 2: Bind the NFS Share in Kubernetes
Unlike dynamic provisioners (like Longhorn or AWS EBS), a raw NFS share doesn't automatically carve out chunks of data for you. You must manually map the physical network share to a Kubernetes PersistentVolume (PV), and then claim it with a PersistentVolumeClaim (PVC).
Create a file named nfs-storage.yaml:
---
# 1. The PersistentVolume defines the PHYSICAL network location
apiVersion: v1
kind: PersistentVolume
metadata:
name: nfs-media-pv
spec:
capacity:
storage: 1000Gi # This is arbitrary for NFS, it just defines the upper bound
accessModes:
- ReadWriteMany # NFS supports multiple pods reading/writing at once!
persistentVolumeReclaimPolicy: Retain # If the PVC is deleted, DO NOT delete my media files!
nfs:
server: 192.168.1.53 # The static IP of k8s-worker-02
path: /mnt/media
---
# 2. The PersistentVolumeClaim is what your Pod actually requests
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nfs-media-pvc
namespace: media # Must match your application's namespace
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1000Gi
volumeName: nfs-media-pv # Explicitly bind to the PV we created above
Apply it to the cluster:
Step 3: Mount the PVC in your Pod
Finally, inside your application's Deployment manifest, mount the PVC exactly as you would a local disk:
volumes:
- name: media-storage
persistentVolumeClaim:
claimName: nfs-media-pvc
containers:
- name: jellyfin
image: linuxserver/jellyfin
volumeMounts:
- name: media-storage
mountPath: /data/media # This path inside the container maps to /mnt/media on the NFS server!
Any media files you drop into /mnt/media on k8s-worker-02 will instantly appear inside the Jellyfin container at /data/media, regardless of which physical node the Jellyfin pod is currently running on.
Step 4: Advanced - Dynamic Provisioning with Helm
Manually creating PVs and PVCs for every application gets tedious. Modern Kubernetes uses Dynamic Provisioning to automatically create PVs (and underlying folders on the NFS server) whenever an application asks for a PVC.
To set this up, we use Helm, the package manager for Kubernetes.
-
Install Helm on your workstation (e.g., Mac):
-
Add the NFS Provisioner repository:
-
Install the Provisioner:
Now, instead of manually creating a PersistentVolume, you only need to create a PersistentVolumeClaim, and the provisioner will automatically handle the rest!