Dynamic storage provisioner: openebs.io/local
Lab platform: Self-managed Kubernetes v1.35.10 on Ubuntu Linux (kubeadm or other manual installation)
OpenEBS release referenced: OpenEBS 4.6.2 (HostPath engine v4.6.0), current as checked on November 12, 2026
Lab target: Three StatefulSet replicas, each with its own dynamically provisioned 1Gi Local PV backed by a directory on a worker node
Difficulty: Beginner to intermediate
Scope: A lightweight OpenEBS Local PV HostPath-only installation; no EKS, AKS, GKE, Ceph, LVM, ZFS, or Mayastor required.
Important: All commands are examples to run against your cluster, not commands already executed. Verify your
kubectlcontext and node status before applying anything. A 1Gi PVC represents a requested size; without filesystem quotas it does not impose a 1Gi disk-use limit. The backing storage is node-local, not replicated or highly available.
Table of contents
- Learning objectives
- Architecture and terminology
- OpenEBS vs Rancher vs Kubernetes no-provisioner
- Prerequisites and safety checks
- Install Helm if needed
- Install OpenEBS HostPath-only using Helm
- Check the default OpenEBS StorageClass
- Create a custom HostPath StorageClass
- Create a StatefulSet with automatic PVCs
- Apply resources and verify dynamic provisioning
- Find the real data directory on the worker node
- Test persistence after Pod recreation
- Test storage isolation across replicas
- Optional: Headless Service for stable DNS
- Advanced configuration
- Troubleshooting common errors
- Storage lifecycle, retention, and cleanup
- Operational considerations and limitations
- Frequently asked questions
- Command cheat sheet
- Official references
1. Learning objectives
At the end of this lab, you will be able to:
- Install the OpenEBS Local PV HostPath dynamic provisioner on a self-managed Ubuntu cluster.
- Understand
openebs.io/localandWaitForFirstConsumer. - Create only a StorageClass and StatefulSet as application manifests. OpenEBS and the StatefulSet will create PVs and PVCs automatically.
- Provision three independent 1Gi volumes for three StatefulSet Pods.
- Locate and inspect the directories on the underlying worker node.
- Confirm that files survive Pod deletion and recreation on the same node.
- Troubleshoot
PendingPVCs, taints, provisioning problems, and permission failures. - Clean up safely without accidentally deleting other storage workloads.
2. Architecture and terminology
2.1 What is OpenEBS Local PV HostPath?
OpenEBS is a Kubernetes storage project. The Local PV HostPath engine dynamically provisions persistent volumes from directories on a Kubernetes node’s filesystem. The local disk may be the OS disk or another disk already mounted on the node.
Provisioning flow:
StorageClass: openebs-hostpath-lab
provisioner: openebs.io/local
volumeBindingMode: WaitForFirstConsumer
|
v
StatefulSet (3 Pods)
|
+----------------+----------------+
| | |
v v v
Pod ...-0 Pod ...-1 Pod ...-2
| | |
PVC ...-0 PVC ...-1 PVC ...-2
| | |
v v v
OpenEBS provisions three local PVs dynamically
| | |
v v v
/var/openebs/local/<PV-0> .../<PV-1> .../<PV-2>
(on the node where the corresponding Pod runs)
There is no shared network volume in this model. The three backing directories may be on the same worker or on different workers, depending on scheduling.
2.2 Key terms
| Term | Meaning |
|---|---|
| StorageClass (SC) | Defines how storage should be provisioned. |
| PersistentVolumeClaim (PVC) | Application request for storage. A StatefulSet’s volumeClaimTemplates generates these automatically. |
| PersistentVolume (PV) | Cluster storage object bound to a claim; OpenEBS creates it dynamically. |
| HostPath base path | Directory on the host, e.g. /var/openebs/local, under which PV directories are created. |
| Local PV | Kubernetes PV referring to a filesystem path on a specific node, with node affinity. |
| OpenEBS provisioner | Controller that creates/deletes PV objects and manages backing directories. |
WaitForFirstConsumer | Allows the scheduler to choose the Pod’s node before local volume creation. |
ReadWriteOnce (RWO) | Supported access mode for OpenEBS Local PV HostPath; not a shared RWX network filesystem. |
2.3 Critical distinction: HostPath-backed does not mean spec.hostPath
OpenEBS Local PV HostPath normally creates a PV with spec.local.path, not spec.hostPath.path. Its data still lives in a host filesystem directory. This is intentional: the local PV API includes node affinity, making scheduling more reliable.
To inspect a provisioned PV, query .spec.local.path:
kubectl get pv <YOUR_PV_NAME> -o jsonpath='{.spec.local.path}{"\n"}'
Code language: HTML, XML (xml)
Do not use kubectl get pv ... -o jsonpath='{.spec.hostPath.path}' for this OpenEBS engine; it will usually be empty.
3. OpenEBS vs Rancher vs Kubernetes no-provisioner
| StorageClass provisioner | Dynamic PV creation? | Host-local directory backing? | Separate controller installation? |
|---|---|---|---|
kubernetes.io/no-provisioner | No | Yes, with manually created local PVs | No |
rancher.io/local-path | Yes | Yes | Yes: Rancher Local Path Provisioner |
openebs.io/local | Yes | Yes | Yes: OpenEBS Local PV HostPath |
Why your old StorageClass stayed Pending:
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
Code language: HTTP (http)
This supports static provisioning only. A matching PV must already exist. OpenEBS changes that: the controller creates PVs for qualifying PVC requests.
Merely changing a StorageClass’s
provisionertoopenebs.io/localwithout installing the OpenEBS controller will not work.
4. Prerequisites and safety checks
This tutorial assumes:
- A functioning Kubernetes API server, scheduler, controller manager, networking, kubelet, and container runtime.
kubectlconfigured to administer the cluster.- At least one Ready and schedulable worker node; control-plane-only nodes may have a
NoScheduletaint. - Disk space available on the node filesystem containing
/var/openebs/local(at least several GiB for a small lab). - Helm 3 installed (if not, see the next section).
- Cluster-wide rights to install CRDs, deployments, RBAC, and StorageClasses.
- Outbound access to Helm repositories and container registries from the appropriate client/nodes, unless using a preconfigured internal mirror.
4.1 Confirm cluster access
kubectl config current-context
kubectl version
kubectl cluster-info
kubectl get nodes -o wide
kubectl get sc
Code language: JavaScript (javascript)
Review current-context carefully before installing.
4.2 Identify node taints
kubectl get nodes -o custom-columns=NAME:.metadata.name,READY:.status.conditions[-1].type,TAINTS:.spec.taints
kubectl describe nodes | grep -A 3 'Taints:'
Code language: JavaScript (javascript)
Your earlier output showed two nodes, with one node rejected because of an untolerated taint. That can be normal for a control-plane node. The remaining worker must be Ready and have adequate resources. You do not need to remove the control-plane taint for this lab.
4.3 Check free disk space on the worker
SSH or access the worker node (not just the administrative host):
df -h /var
lsblk -f
Code language: JavaScript (javascript)
If /var/openebs/local does not exist yet, OpenEBS can create it during provisioning. For a dedicated disk, mount that disk at a suitable BasePath before provisioning.
4.4 Protect existing storage
kubectl get pv
kubectl get pvc -A
kubectl get sc
helm list -A
Code language: JavaScript (javascript)
Do not delete your existing manual StorageClass, PVs, PVCs, or Rancher provisioner. This tutorial uses new resource names to coexist with them.
5. Install Helm if needed
Check whether Helm is available:
helm version
If Helm is already installed, skip this step.
For Ubuntu, follow the official Helm installation instructions at https://helm.sh/docs/intro/install/. One documented installation method is:
curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
helm version
Code language: JavaScript (javascript)
Security note: Review the script or use your organization’s approved package-management process before executing remote scripts.
6. Install OpenEBS HostPath-only using Helm
The unified OpenEBS Helm chart installs several engines by default. A small Ubuntu lab needs only Local PV HostPath, so disable Mayastor, LVM, and ZFS. The commands below also disable optional Loki/Alloy observability components to conserve resources.
6.1 Add the official OpenEBS chart repository
helm repo add openebs https://openebs.github.io/openebs
helm repo update
helm search repo openebs/openebs --versions | head
Code language: JavaScript (javascript)
6.2 Inspect the chart before installation
As of this tutorial, the checked OpenEBS chart release is 4.6.2. Verify it is present in the repository:
helm show chart openebs/openebs --version 4.6.2
helm show values openebs/openebs --version 4.6.2 > openebs-chart-defaults.yaml
Review openebs-chart-defaults.yaml if your environment uses custom policies or registry mirrors. Pinning the chart version makes the lab more reproducible. If your chart repository exposes a different version, inspect it before adjusting the commands.
6.3 Install only Local PV HostPath
helm upgrade --install openebs openebs/openebs \
--namespace openebs \
--create-namespace \
--version 4.6.2 \
--set engines.replicated.mayastor.enabled=false \
--set engines.local.lvm.enabled=false \
--set engines.local.zfs.enabled=false \
--set engines.local.rawfile.enabled=false \
--set loki.enabled=false \
--set alloy.enabled=false
Code language: JavaScript (javascript)
Why disable the other components? You are testing only local directory provisioning; you do not need distributed replicas, LVM volumes, ZFS pools, or a complete OpenEBS observability stack.
6.4 Verify the Helm release and controller
helm status openebs -n openebs
helm list -n openebs
kubectl get pods -n openebs -o wide
kubectl get deployments -n openebs
kubectl get storageclass
Code language: JavaScript (javascript)
Look for a deployment with a name similar to openebs-localpv-provisioner in the openebs namespace. When present, verify its rollout:
kubectl rollout status deployment/openebs-localpv-provisioner \
-n openebs --timeout=180s
The provisioner Pod should become Running and Ready. If not, review troubleshooting.
7. Check the default OpenEBS StorageClass
OpenEBS ordinarily creates a default-named HostPath StorageClass:
kubectl get sc openebs-hostpath
kubectl describe sc openebs-hostpath
Code language: JavaScript (javascript)
Expected key properties:
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE
openebs-hostpath openebs.io/local Delete WaitForFirstConsumer
The usual base directory is /var/openebs/local.
Fastest possible test: Set storageClassName: openebs-hostpath in your StatefulSet and skip creating your own StorageClass. However, the next step creates a custom StorageClass, so you can learn exactly how its annotations work.
8. Create a custom HostPath StorageClass
Save this file as sc.yaml:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: openebs-hostpath-lab
annotations:
openebs.io/cas-type: local
cas.openebs.io/config: |
- name: StorageType
value: "hostpath"
- name: BasePath
value: "/var/openebs/local"
provisioner: openebs.io/local
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
Code language: JavaScript (javascript)
Create and inspect it:
kubectl apply -f sc.yaml
kubectl get sc openebs-hostpath-lab
kubectl describe sc openebs-hostpath-lab
Code language: JavaScript (javascript)
Understanding the fields
| Field | Purpose |
|---|---|
metadata.name | Name referenced in the PVC / StatefulSet. |
openebs.io/cas-type: local | Tells OpenEBS this is a local storage class. |
cas.openebs.io/config | OpenEBS-specific configuration, written as an embedded YAML list. |
StorageType: hostpath | A directory underneath a node’s filesystem path is the backing storage. |
BasePath | Parent directory for dynamically created PV directories. |
provisioner: openebs.io/local | Asks the installed OpenEBS controller to provision volumes. |
reclaimPolicy: Delete | When a PVC and PV are removed normally, OpenEBS deletes the backing directory/data. |
WaitForFirstConsumer | Defers provision until the scheduler picks a node for a consuming Pod. |
Do not change volumeBindingMode to Immediate. OpenEBS HostPath requires WaitForFirstConsumer because provisioning depends on the chosen node.
9. Create a StatefulSet with automatic PVCs
Save this file as sfs.yaml:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: openebs-local-test
spec:
serviceName: openebs-local-service
replicas: 3
selector:
matchLabels:
app: openebs-local-test
template:
metadata:
labels:
app: openebs-local-test
spec:
containers:
- name: test-container
image: busybox:1.36
command:
- /bin/sh
- -c
- sleep 100000
volumeMounts:
- name: local-vol
mountPath: /usr/test-pod
volumeClaimTemplates:
- metadata:
name: local-vol
labels:
app: openebs-local-test
spec:
accessModes:
- ReadWriteOnce
storageClassName: openebs-hostpath-lab
resources:
requests:
storage: 1Gi
Code language: JavaScript (javascript)
What it creates
Kubernetes creates the PVCs from volumeClaimTemplates, and OpenEBS creates matching PVs on demand:
| StatefulSet Pod | Generated PVC | Provisioned PV |
|---|---|---|
openebs-local-test-0 | local-vol-openebs-local-test-0 | New PV created by OpenEBS |
openebs-local-test-1 | local-vol-openebs-local-test-1 | New PV created by OpenEBS |
openebs-local-test-2 | local-vol-openebs-local-test-2 | New PV created by OpenEBS |
Each claims 1Gi, for 3Gi requested in total, not necessarily 3Gi actually consumed. Each has its own directory, and no handwritten PV or PVC manifest is required.
Why only Pod -0 may appear at first
StatefulSets default to OrderedReady. Kubernetes waits for openebs-local-test-0 to become Ready before proceeding to -1 and -2. That behavior is expected.
About the serviceName
openebs-local-service refers to the recommended headless Service for stable network identity. The storage lab can demonstrate volume creation without this Service, but create it if you need stable per-Pod DNS; see section 14.
10. Apply resources and verify dynamic provisioning
10.1 Apply both manifests
kubectl apply -f sc.yaml
kubectl apply -f sfs.yaml
Code language: CSS (css)
10.2 Watch rollout and PVC/PV binding
kubectl get pods -l app=openebs-local-test -w
Code language: JavaScript (javascript)
In another terminal:
kubectl get pvc -l app=openebs-local-test
kubectl get pv
kubectl get sc
Code language: JavaScript (javascript)
Once everything is ready, expect something like:
NAME READY STATUS RESTARTS
openebs-local-test-0 1/1 Running 0
openebs-local-test-1 1/1 Running 0
openebs-local-test-2 1/1 Running 0
NAME STATUS CAPACITY ACCESS MODES STORAGECLASS
local-vol-openebs-local-test-0 Bound 1Gi RWO openebs-hostpath-lab
local-vol-openebs-local-test-1 Bound 1Gi RWO openebs-hostpath-lab
local-vol-openebs-local-test-2 Bound 1Gi RWO openebs-hostpath-lab
The PV names are automatically assigned, usually beginning with pvc-.
10.3 Inspect one PVC
kubectl describe pvc local-vol-openebs-local-test-0
Expected attributes include:
Status: BoundStorageClass: openebs-hostpath-labCapacity: 1GiAccess Modes: RWO- A corresponding volume name (
pvc-...)
10.4 Check which nodes run your Pods
kubectl get pods -l app=openebs-local-test -o wide
Code language: JavaScript (javascript)
On your two-node cluster, there may be only one schedulable worker. In that case all three Pods can be placed on the same worker if sufficient resources are available.
11. Find the real data directory on the worker node
This is the most important verification that your dynamically provisioned volume is host-local.
11.1 Read the PV name bound to the first PVC
PV0=$(kubectl get pvc local-vol-openebs-local-test-0 \
-o jsonpath='{.spec.volumeName}')
echo "$PV0"
Code language: PHP (php)
11.2 Inspect its local path and node affinity
kubectl get pv "$PV0" -o yaml
kubectl get pv "$PV0" -o jsonpath='{.spec.local.path}{"\n"}'
kubectl get pv "$PV0" -o jsonpath='{.spec.nodeAffinity.required.nodeSelectorTerms[0].matchExpressions[0].values[0]}{"\n"}'
Code language: PHP (php)
Expected conceptual PV fragment:
spec:
storageClassName: openebs-hostpath-lab
capacity:
storage: 1Gi
local:
path: /var/openebs/local/pvc-example-id
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- your-worker-node
Code language: JavaScript (javascript)
spec.local.path is the filesystem directory. nodeAffinity pins that PV to the node with the data.
11.3 Verify on the correct worker
Find which node runs the Pod:
kubectl get pod openebs-local-test-0 -o wide
Code language: JavaScript (javascript)
Connect to that node (SSH, SSM, console, etc.) and run:
sudo ls -lah /var/openebs/local/
df -h /var/openebs/local
Code language: JavaScript (javascript)
Do not manually delete or move the PV directory while its volume is in use. The PV path may not exist on the control-plane node or on other workers.
12. Test persistence after Pod recreation
12.1 Write a file inside the mounted PVC
kubectl exec openebs-local-test-0 -- sh -c \
'echo "Hello from OpenEBS HostPath" > /usr/test-pod/hello.txt'
Code language: JavaScript (javascript)
12.2 Read the file
kubectl exec openebs-local-test-0 -- cat /usr/test-pod/hello.txt
Expected:
Hello from OpenEBS HostPath
Code language: JavaScript (javascript)
12.3 Delete only the Pod (not the PVC)
kubectl delete pod openebs-local-test-0
Code language: JavaScript (javascript)
The StatefulSet controller recreates it. Check readiness:
kubectl rollout status statefulset/openebs-local-test --timeout=180s
kubectl get pods -l app=openebs-local-test -o wide
Code language: JavaScript (javascript)
12.4 Read the original file after restart
kubectl exec openebs-local-test-0 -- cat /usr/test-pod/hello.txt
Expected:
Hello from OpenEBS HostPath
Code language: JavaScript (javascript)
What this proves: the file survives Pod replacement because the PVC remains bound to the existing PV. Kubernetes schedules the replacement to the node containing the local volume. It does not prove recovery from node/disk loss.
13. Test storage isolation across replicas
Each Pod receives its own PVC; writing to one volume must not write to the others.
13.1 Write a unique value in each Pod
for i in 0 1 2; do
kubectl exec "openebs-local-test-$i" -- sh -c \
"echo data-from-pod-$i > /usr/test-pod/identity.txt"
done
Code language: JavaScript (javascript)
13.2 Read from each Pod
for i in 0 1 2; do
echo "Pod $i:"
kubectl exec "openebs-local-test-$i" -- \
cat /usr/test-pod/identity.txt
done
Code language: PHP (php)
Expected:
Pod 0:
data-from-pod-0
Pod 1:
data-from-pod-1
Pod 2:
data-from-pod-2
Code language: JavaScript (javascript)
This demonstrates one claim and one separate storage directory per replica.
14. Optional: Headless Service for stable DNS
The lab’s storage creation does not depend on a Service. However, a StatefulSet normally uses a headless Service to provide stable Pod network identities.
If you want full StatefulSet DNS behavior, create service.yaml:
apiVersion: v1
kind: Service
metadata:
name: openebs-local-service
spec:
clusterIP: None
selector:
app: openebs-local-test
ports:
- name: http
port: 80
targetPort: 80
Apply:
kubectl apply -f service.yaml
kubectl get svc openebs-local-service
Code language: JavaScript (javascript)
This sample Service’s port is for DNS/identity demonstration only. The BusyBox container does not listen on port 80; no HTTP request will succeed until you run a server. If your only aim is testing storage and you insist on just two manifest types, skip this optional Service.
15. Advanced configuration
15.1 Change the host storage directory
For a dedicated mounted data disk, change the BasePath in a new StorageClass:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: openebs-hostpath-data-disk
annotations:
openebs.io/cas-type: local
cas.openebs.io/config: |
- name: StorageType
value: "hostpath"
- name: BasePath
value: "/mnt/openebs-data"
provisioner: openebs.io/local
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
Code language: JavaScript (javascript)
Make sure /mnt/openebs-data is a real mounted filesystem on any node eligible for these workloads. Provisioning to an unmounted directory can unexpectedly fill the OS disk.
15.2 Restrict provisioning to specified nodes
Use allowedTopologies in the StorageClass to permit only particular worker hostnames:
allowedTopologies:
- matchLabelExpressions:
- key: kubernetes.io/hostname
values:
- YOUR-WORKER-NODE-NAME
Insert this at the StorageClass top level alongside volumeBindingMode; replace the hostname. This influences both provisioning and scheduling. Avoid hard-coding Pod nodeName when using WaitForFirstConsumer, because bypassing the scheduler can leave the claim Pending.
15.3 Set directory permissions
OpenEBS HostPath’s default newly created directory mode is usually 0777. For tighter access, add a FilePermissions entry to the cas.openebs.io/config annotation:
- name: FilePermissions
data:
mode: "0770"
For example, a full custom StorageClass with permissions:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: openebs-hostpath-restricted
annotations:
openebs.io/cas-type: local
cas.openebs.io/config: |
- name: StorageType
value: "hostpath"
- name: BasePath
value: "/var/openebs/restricted"
- name: FilePermissions
data:
mode: "0770"
provisioner: openebs.io/local
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
Code language: JavaScript (javascript)
Confirm your application UID/GID can access the volume before tightening permissions. These settings affect new directories, not existing ones.
15.4 Set Retain instead of Delete
Use:
reclaimPolicy: Retain
Code language: HTTP (http)
With Retain, Kubernetes does not automatically remove the volume’s data when its PVC is deleted. This protects against automatic cleanup but also requires manual recovery and management of the retained volume. Retain is not a backup strategy.
15.5 Optional quotas (advanced, not needed for the lab)
By default, storage: 1Gi is not a filesystem quota. OpenEBS 4.6 documents optional XFSQuota and EXT4Quota, but these require correct filesystem features and project quota mount configuration. Do not copy quota settings into the beginner lab without configuring the node filesystem first.
Official guides:
- https://openebs.io/docs/main/user-guides/local-storage-user-guide/local-pv-hostpath/configuration/hostpath-storageclass-parameters
- https://openebs.io/docs/main/user-guides/local-storage-user-guide/local-pv-hostpath/advanced-operations/ext4-quota/enable-ext4-quota
15.6 Install from a consolidated Helm values file (optional)
For repeatable configuration, create openebs-values.yaml:
engines:
local:
lvm:
enabled: false
zfs:
enabled: false
rawfile:
enabled: false
replicated:
mayastor:
enabled: false
loki:
enabled: false
alloy:
enabled: false
Code language: JavaScript (javascript)
Then use this instead of the --set form in step 6:
helm upgrade --install openebs openebs/openebs \
-n openebs --create-namespace \
--version 4.6.2 \
-f openebs-values.yaml
Validate chart keys with helm show values before applying future chart releases.
16. Troubleshooting common errors
16.1 PVC stuck in Pending: WaitForFirstConsumer
Check:
kubectl describe pvc local-vol-openebs-local-test-0
kubectl describe pod openebs-local-test-0
kubectl get pods -n openebs
Code language: JavaScript (javascript)
A claim can show WaitForFirstConsumer briefly. It is not automatically a failure. Look for the scheduler’s error event and ensure a Pod consumes the PVC.
If the Pod shows FailedScheduling, continue below.
16.2 0/2 nodes are available ... untolerated taint(s)
Your earlier cluster logs had this pattern. Check:
kubectl get nodes -o wide
kubectl get nodes -o custom-columns=NAME:.metadata.name,TAINTS:.spec.taints
kubectl describe pod openebs-local-test-0
Code language: JavaScript (javascript)
A control-plane taint is normal. Make sure at least one Ready worker is available. Do not remove a control-plane taint simply to get this lab running unless it is an intentionally single-node training cluster and you understand the implications.
16.3 PVC Pending because provisioner is missing or broken
kubectl get pods -n openebs -o wide
kubectl get deployments -n openebs
kubectl logs -n openebs deployment/openebs-localpv-provisioner --tail=150
kubectl describe pvc local-vol-openebs-local-test-0
Code language: JavaScript (javascript)
Check for image pull issues, CRD/RBAC failures, unhealthy controller Pods, or incorrect StorageClass provisioner values.
16.4 Wrong StorageClass
kubectl get sc
kubectl get pvc local-vol-openebs-local-test-0 -o yaml
Code language: JavaScript (javascript)
For this tutorial the claim’s storageClassName must be openebs-hostpath-lab and that StorageClass’s provisioner must be openebs.io/local.
If the claim still requests your old local-storage (kubernetes.io/no-provisioner) class, you are running the old manifest or an old StatefulSet/PVC. For a disposable lab, recreate those resources after confirming there is no data to preserve. Do not delete persistent claims containing valuable data.
16.5 Immediate mode prevents local provisioning
OpenEBS HostPath needs:
volumeBindingMode: WaitForFirstConsumer
Code language: HTTP (http)
If you created a StorageClass with Immediate, create a corrected one with a new name and update a newly created application/PVC to use it. StorageClass binding/provisioner behavior cannot be reliably changed in place.
16.6 Using nodeName with WaitForFirstConsumer
Do not set:
spec:
template:
spec:
nodeName: some-worker
This bypasses normal scheduler selection and can prevent local provisioning. Use a nodeSelector, node affinity, or the StorageClass’s allowedTopologies if placement restrictions are needed.
16.7 Permission denied in the mounted path
kubectl exec openebs-local-test-0 -- sh -c \
'id; ls -ld /usr/test-pod; touch /usr/test-pod/write-test'
Code language: JavaScript (javascript)
Check the volume directory mode and the container’s UID/GID. With a non-root workload, set appropriate file permissions / fsGroup where supported, rather than blindly making all directories world-writable.
16.8 Pod stays Pending after PV is Bound
kubectl describe pod openebs-local-test-0
kubectl get pv "$PV0" -o yaml
kubectl get nodes -o wide
Code language: JavaScript (javascript)
A local PV has nodeAffinity. The Pod must be able to run on that specific node. Node unavailability, taints, or conflicting node selectors can block it. Data does not automatically move to another node.
16.9 Pod ImagePullBackOff
kubectl describe pod openebs-local-test-0
kubectl get events --sort-by=.lastTimestamp | tail -30
Code language: JavaScript (javascript)
The test image is busybox:1.36, not the retired k8s.gcr.io/busybox reference. Verify node egress to the image registry or use an approved mirror.
16.10 Disk-full or capacity concerns
On the actual worker:
df -h /var/openebs/local
sudo du -sh /var/openebs/local/* 2>/dev/null
Code language: JavaScript (javascript)
OpenEBS HostPath is directory-backed. Without project quotas, applications can use more than the PVC request and may fill the node’s filesystem. Plan free space and disk-pressure alerts.
16.11 Directory exists but data appears missing
Check Pod-to-node placement, the PV’s spec.local.path, and the mount inside the Pod. Do not assume all cluster nodes have identical data at /var/openebs/local.
kubectl get pod openebs-local-test-0 -o wide
kubectl get pvc local-vol-openebs-local-test-0
kubectl get pv "$PV0" -o jsonpath='{.spec.local.path}{"\n"}'
Code language: PHP (php)
16.12 Common issue checklist
kubectl get nodes -o wide
kubectl get sc
kubectl get pods -n openebs -o wide
kubectl get sts,pods,pvc -l app=openebs-local-test
kubectl get pvc local-vol-openebs-local-test-0 -o yaml
kubectl describe pod openebs-local-test-0
kubectl get events -A --sort-by=.lastTimestamp | tail -40
helm status openebs -n openebs
Code language: JavaScript (javascript)
17. Storage lifecycle, retention, and cleanup
17.1 Understand what gets deleted
Deleting a StatefulSet does not normally delete its PVCs. The PVCs may remain to preserve data. Deleting a PVC backed by a PV with reclaimPolicy: Delete generally triggers deletion of that PV and its backing directory. The exact cleanup depends on the controller operating correctly and access to the node where the directory resides.
WARNING — destructive: The cleanup commands below intentionally remove the test PVCs and their stored data. Only run them on disposable lab resources and after backing up anything important.
17.2 Remove the application first
kubectl delete statefulset openebs-local-test
kubectl get pvc -l app=openebs-local-test
Code language: JavaScript (javascript)
17.3 Remove only this lab’s claims
kubectl delete pvc \
local-vol-openebs-local-test-0 \
local-vol-openebs-local-test-1 \
local-vol-openebs-local-test-2
Code language: JavaScript (javascript)
Confirm the corresponding PVs are gone:
kubectl get pv
Code language: JavaScript (javascript)
Do not delete unrelated manually provisioned PVs such as your earlier hostpath, hostpath2, or deehostpath volumes.
17.4 Delete the custom StorageClass
kubectl delete storageclass openebs-hostpath-lab
Code language: JavaScript (javascript)
If you created the optional headless Service:
kubectl delete service openebs-local-service
Code language: JavaScript (javascript)
17.5 Optionally uninstall OpenEBS (ONLY if no workloads use it)
helm uninstall openebs -n openebs
Before uninstalling, check whether any PVCs/PVs across the cluster still use openebs.io/local. Do not uninstall a provisioner while important PVs rely on it, because reclaim and operational actions can fail afterward.
kubectl get sc
kubectl get pv -o custom-columns=NAME:.metadata.name,SC:.spec.storageClassName,STATUS:.status.phase
kubectl get pvc -A
Code language: JavaScript (javascript)
Namespace deletion and CRD removal may affect other OpenEBS workloads and should not be treated as routine lab cleanup.
18. Operational considerations and limitations
| Concern | OpenEBS Local PV HostPath behavior |
|---|---|
| Dynamic PV/PVC provisioning | Yes. |
| Cloud provider needed | No. |
| Dedicated raw block device needed | No; directory on an existing mounted filesystem is sufficient. |
| Data available from all nodes | No, data lives on one node. |
| Transparent node-failure recovery | No. A failed/lost node means the local volume is inaccessible. |
| Replication | No. |
| Access mode | ReadWriteOnce; not a shared ReadWriteMany filesystem. |
| Scheduling | Uses WaitForFirstConsumer and local PV node affinity. |
| Hard 1Gi quota by default | No. Optional project quotas require explicit filesystem configuration. |
| Backups | Your responsibility (e.g. application-aware backups or compatible backup tooling). |
| Common use cases | Learning, CI labs, caches, disposable development databases, single-node/local workloads. |
| Production suitability | Only if you explicitly accept node-local failure characteristics and implement backups and monitoring. |
Security: directory-backed node volumes should be considered powerful local storage access. Restrict who may create StorageClasses, Pods with host mounts, and privileged workloads. Avoid storing sensitive data without appropriate host filesystem permissions and backup controls.
18.1 When not to use OpenEBS Local PV HostPath
For a multi-node database requiring automatic failover with replicated storage, use a storage system designed for replication, such as Longhorn or OpenEBS Mayastor (subject to prerequisites), rather than expecting local hostpath volumes to follow Pods between nodes.
18.2 1Gi is a request, not a reservation guarantee
The requested capacity shows in Kubernetes API objects, but the backing filesystem is shared with other directories. Monitor node free space; do not infer that three 1Gi PVCs are physically isolated 1Gi partitions.
19. Frequently asked questions
Q1. Can kubernetes.io/no-provisioner dynamically create local PVs?
No. It explicitly means no dynamic provisioner. Use OpenEBS or another installed dynamic provisioner.
Q2. Do I need to create PersistentVolumes manually?
No. Once OpenEBS is running and a suitable PVC is requested, it creates the PV dynamically.
Q3. Do I need a standalone PVC YAML?
No, because volumeClaimTemplates in the StatefulSet automatically creates one PVC per Pod replica.
Q4. Can I use only StorageClass + StatefulSet YAML?
Yes for the application storage demonstration. OpenEBS itself must already be installed, and the optional headless Service is for StatefulSet DNS, not for volume provisioning.
Q5. Does openebs.io/local mean Kubernetes PV spec.hostPath?
No. OpenEBS HostPath normally uses spec.local.path with node affinity; the actual data is stored in a host filesystem directory.
Q6. What happens if I delete a Pod?
The StatefulSet recreates it and reuses the existing PVC/PV. Data should persist if the same underlying node/disk remains available.
Q7. What happens if the worker node disappears permanently?
The volume is not automatically replicated or migrated. Recovery depends on restoring that node/disk or recovering from backups.
Q8. Can three replicas share the same PVC?
Not in this design. volumeClaimTemplates gives each replica its own PVC, which is the right model for independent local storage.
Q9. Can I use my old local-storage StorageClass name?
Only after planning migration/removal of the existing StorageClass with kubernetes.io/no-provisioner and any dependent resources. The provisioner field is immutable. This tutorial uses openebs-hostpath-lab so you do not disturb existing workloads.
Q10. Is 1Gi strictly enforced?
Not by default. OpenEBS documents optional filesystem project quotas for supported XFS/ext4 setups.
Q11. Do I need to untaint the control plane?
Usually no. An ordinary worker node is sufficient for the example.
Q12. Does the default openebs-hostpath StorageClass already work?
Yes, after a successful OpenEBS HostPath installation. Your custom class is useful for learning and customizing the base path.
20. Command cheat sheet
<em># PRECHECK</em>
kubectl config current-context
kubectl get nodes -o wide
kubectl get sc
kubectl get pvc -A
helm version
<em># HELM REPOSITORY</em>
helm repo add openebs https://openebs.github.io/openebs
helm repo update
<em># HOSTPATH-ONLY INSTALL</em>
helm upgrade --install openebs openebs/openebs \
-n openebs --create-namespace --version 4.6.2 \
--set engines.replicated.mayastor.enabled=false \
--set engines.local.lvm.enabled=false \
--set engines.local.zfs.enabled=false \
--set engines.local.rawfile.enabled=false \
--set loki.enabled=false \
--set alloy.enabled=false
<em># VERIFY PROVISIONER</em>
helm status openebs -n openebs
kubectl get pods -n openebs
kubectl get sc openebs-hostpath
<em># CREATE LAB SC + STATEFULSET</em>
kubectl apply -f sc.yaml
kubectl apply -f sfs.yaml
<em># CHECK DYNAMIC STORAGE</em>
kubectl get pods -l app=openebs-local-test -o wide
kubectl get pvc -l app=openebs-local-test
kubectl get pv
<em># FIND HOST-LOCAL DIRECTORY</em>
PV0=$(kubectl get pvc local-vol-openebs-local-test-0 -o jsonpath='{.spec.volumeName}')
kubectl get pv "$PV0" -o jsonpath='{.spec.local.path}{"\n"}'
kubectl get pv "$PV0" -o yaml
<em># PERSISTENCE TEST</em>
kubectl exec openebs-local-test-0 -- sh -c \
'echo "Hello from OpenEBS" > /usr/test-pod/hello.txt'
kubectl delete pod openebs-local-test-0
kubectl rollout status statefulset/openebs-local-test --timeout=180s
kubectl exec openebs-local-test-0 -- cat /usr/test-pod/hello.txt
<em># TROUBLESHOOT</em>
kubectl describe pod openebs-local-test-0
kubectl describe pvc local-vol-openebs-local-test-0
kubectl logs -n openebs deployment/openebs-localpv-provisioner --tail=150
<em># CLEANUP (DELETES THIS LAB'S DATA)</em>
kubectl delete statefulset openebs-local-test
kubectl delete pvc local-vol-openebs-local-test-{0,1,2}
kubectl delete sc openebs-hostpath-lab
Code language: HTML, XML (xml)
21. Official references
The examples and behaviors were checked against these primary sources:
- OpenEBS 4.6 installation guide — https://openebs.io/docs/quickstart-guide/installation
- OpenEBS Local PV HostPath overview — https://openebs.io/docs/user-guides/localpv-hostpath
- Create HostPath StorageClasses — https://openebs.io/docs/user-guides/local-storage-user-guide/local-pv-hostpath/configuration/hostpath-create-storageclass
- HostPath StorageClass parameters — https://openebs.io/docs/main/user-guides/local-storage-user-guide/local-pv-hostpath/configuration/hostpath-storageclass-parameters
- HostPath deployment example — https://openebs.io/docs/quickstart-guide/deployment
- Local storage troubleshooting — https://openebs.io/docs/troubleshooting
- OpenEBS releases — https://github.com/openebs/openebs/releases
- Official OpenEBS Helm chart repository — https://openebs.github.io/openebs/
- Helm installation documentation — https://helm.sh/docs/intro/install/
Tutorial result: On a self-managed Ubuntu Kubernetes cluster, OpenEBS openebs.io/local dynamically creates separate host-local PersistentVolumes for StatefulSet PVCs. Your application configuration requires only a StorageClass and a StatefulSet (plus an optional headless Service for normal StatefulSet DNS), while the OpenEBS controller must be installed and healthy in the cluster.
I’m Rajesh Kumar, a DevOps, SRE, DevSecOps, Cloud, and Platform Engineering expert passionate about sharing practical knowledge, real-world experiences, and industry best practices. I have worked at Cotocus and regularly write about technology, travel, investing, health, product reviews, and digital marketing through my various platforms.
I publish technical articles at DevOps School, travel stories at Holiday Landmark, stock market insights at Stocks Mantra, health and fitness guidance at My Medic Plus, product reviews at TrueReviewNow, and SEO and digital marketing strategies at Wizbrand.
Find Trusted Cardiac Hospitals
Compare heart hospitals by city and services — all in one place.
Explore Hospitals