---
title: How to Dynamically Provision NFS Storage on Kubernetes Clusters (private)
description: Enable the NFS CSI plugin to dynamically create and manage DigitalOcean Network File Storage shares from Kubernetes PersistentVolumeClaims.
product: Kubernetes
url: https://docs.digitalocean.com/products/kubernetes/how-to/provision-nfs-storage-dynamically/
---

> **For AI agents:** The documentation index is at [https://docs.digitalocean.com/llms.txt](https://docs.digitalocean.com/llms.txt). Markdown versions of pages use the same URL with `index.html.md` in place of the HTML page (for example, append `index.html.md` to the directory path instead of opening the HTML document).

# How to Dynamically Provision NFS Storage on Kubernetes Clusters (private)

DigitalOcean Kubernetes (DOKS) is a Kubernetes service with a fully managed control plane, high availability, and autoscaling. DOKS integrates with standard Kubernetes toolchains and DigitalOcean’s load balancers, volumes, CPU and GPU Droplets, API, and CLI.

DigitalOcean Kubernetes (DOKS) includes an optional NFS CSI plugin that installs the `nfs.csi.digitalocean.com` driver on your cluster. With the plugin enabled, you can create [DigitalOcean Network File Storage (NFS)](https://docs.digitalocean.com/products/nfs/index.html.md) shares directly from Kubernetes by requesting storage with a PersistentVolumeClaim (PVC), the same way you [add block storage volumes](https://docs.digitalocean.com/products/kubernetes/how-to/add-volumes/index.html.md). The driver creates the share, and multiple pods across multiple nodes can then read and write to it simultaneously using the `ReadWriteMany` access mode.

The plugin is disabled by default. Without it, you can still use NFS shares with DOKS by [statically provisioning them](https://docs.digitalocean.com/products/kubernetes/how-to/use-nfs-storage/index.html.md): you create the share yourself and write a PersistentVolume with its connection details. Static provisioning remains supported, and shares you created statically are unaffected by the plugin. You can [enable or disable the plugin](#enable-or-disable-the-plugin) using `doctl`, the DigitalOcean API, Godo, or Terraform, and [verify that it’s running](#verify-the-plugin-is-running) with `kubectl`.

This feature is in [private preview](https://docs.digitalocean.com/platform/product-lifecycle/index.html.md#private-preview). To request access, contact Support, your account team, or your Customer Success team.

**Note**:

  You can enable the plugin only on clusters in regions where [Network File Storage is available](https://docs.digitalocean.com/products/nfs/details/availability/index.html.md). The driver creates each share on the same VPC network as the cluster.

## Feedback and Support

To give us feedback during the private preview, contact your Customer Success Manager or Solutions Architect.

[Private preview releases](https://docs.digitalocean.com/platform/product-lifecycle/index.html.md#private-preview) may not be suitable for production workloads. We recommend avoiding sensitive workloads in preview features.

Your use of DigitalOcean services is governed by our [terms of service](https://www.digitalocean.com/legal/terms-of-service-agreement), which cover eligibility, content, use, payments and billing, and warranties. This document is not a contract and all use of DigitalOcean’s services is subject to the terms of service.

## Enable or Disable the Plugin

In the API, the plugin is controlled through the `nfs_csi_plugin` object on the Kubernetes cluster resource. Set `enabled` to `true` to turn the plugin on, or `false` to turn it off. Omitting the object from an update request leaves the plugin’s current state unchanged.

Disabling the plugin removes the driver from the cluster but does not delete any existing shares. Workloads that currently mount a dynamically provisioned share keep their mounts, but the cluster can no longer create, resize, or delete shares through PVCs until you re-enable the plugin.

### doctl

To enable the plugin when [creating a cluster](https://docs.digitalocean.com/reference/doctl/reference/kubernetes/cluster/create/index.html.md), set the `--enable-nfs-csi-plugin` flag to `true`.

```shell
doctl kubernetes cluster create example-cluster --region nyc2 --version latest --enable-nfs-csi-plugin=true
```

To enable or disable the plugin on an existing cluster, [update the cluster](https://docs.digitalocean.com/reference/doctl/reference/kubernetes/cluster/update/index.html.md) with the same flag:

```shell
doctl kubernetes cluster update example-cluster --enable-nfs-csi-plugin=false
```

To check whether the plugin is enabled on a cluster:

```shell
doctl kubernetes cluster get example-cluster --format NfsCsiPlugin
```

The output shows whether the plugin is enabled:

```text
Output
NFS CSI Plugin
true
```

### cURL

To enable the plugin when creating a cluster, send a `POST` request to `https://api.digitalocean.com/v2/kubernetes/clusters` with a request body similar to the following:

```shell
curl --location 'https://api.digitalocean.com/v2/kubernetes/clusters' \
--header 'Content-Type: application/json' \
--header "Authorization: Bearer $DIGITALOCEAN_TOKEN" \
--data '{
    "name": "example-cluster",
    "region": "nyc2",
    "version": "1.36.1-do.0",
    "node_pools": [
        {
            "size": "s-2vcpu-4gb",
            "count": 3,
            "name": "worker-pool"
        }
    ],
    "nfs_csi_plugin": {
        "enabled": true
    }
}'
```

To enable or disable the plugin on an existing cluster, send a `PUT` request to `https://api.digitalocean.com/v2/kubernetes/clusters/{cluster_id}` with a request body similar to the following:

```shell
curl --location --request PUT 'https://api.digitalocean.com/v2/kubernetes/clusters/{cluster_id}' \
--header 'Content-Type: application/json' \
--header "Authorization: Bearer $DIGITALOCEAN_TOKEN" \
--data '{
    "name": "example-cluster",
    "nfs_csi_plugin": {
        "enabled": true
    }
}'
```

### Go

Go developers can use [Godo](https://github.com/digitalocean/godo), the official DigitalOcean V2 API client for Go. To enable the plugin when creating a Kubernetes cluster with Godo, use code similar to the following:

```go
package main

import (
	"context"
	"fmt"
	"os"

	"github.com/digitalocean/godo"
)

func main() {
	client := godo.NewFromToken("your-digitalocean-token")

	cluster, _, err := client.Kubernetes.Create(context.Background(), &godo.KubernetesClusterCreateRequest{
		Name:        "example-cluster",
		RegionSlug:  "nyc2",
		VersionSlug: "1.36.1-do.0",
		NodePools: []*godo.KubernetesNodePoolCreateRequest{
			{
				Name:  "worker-pool",
				Count: 3,
				Size:  "s-2vcpu-4gb",
			},
		},
		NfsCsiPlugin: &godo.KubernetesNfsCsiPlugin{
			Enabled: godo.PtrTo(true),
		},
	})
	if err != nil {
		fmt.Printf("Error creating cluster: %s\n", err)
		os.Exit(1)
	}

	isEnabled := *cluster.NfsCsiPlugin.Enabled
	fmt.Printf("Cluster creation successfully issued with NFS CSI plugin enabled=%v\n", isEnabled)
}
```

To enable or disable the plugin on an existing cluster with Godo, use code similar to the following:

```go
package main

import (
	"context"
	"fmt"
	"os"

	"github.com/digitalocean/godo"
)

func main() {
	client := godo.NewFromToken("your-digitalocean-token")

	cluster, _, err := client.Kubernetes.Update(context.Background(), "your-cluster-id", &godo.KubernetesClusterUpdateRequest{
		Name: "example-cluster",
		NfsCsiPlugin: &godo.KubernetesNfsCsiPlugin{
			Enabled: godo.PtrTo(true),
		},
	})
	if err != nil {
		fmt.Printf("Error updating cluster: %s\n", err)
		os.Exit(1)
	}

	isEnabled := *cluster.NfsCsiPlugin.Enabled
	fmt.Printf("Cluster update successfully issued with NFS CSI plugin enabled=%v\n", isEnabled)
}
```

### Terraform

To enable the plugin, add an `nfs_csi_plugin` block to the `digitalocean_kubernetes_cluster` resource:

```hcl
resource "digitalocean_kubernetes_cluster" "example" {
  name    = "example-cluster"
  region  = "nyc2"
  version = "1.36.1-do.0"

  node_pool {
    name       = "worker-pool"
    size       = "s-2vcpu-4gb"
    node_count = 3
  }

  nfs_csi_plugin {
    enabled = true
  }
}
```

To disable the plugin, set `enabled = false` in the same block.

## Verify the Plugin Is Running

When the plugin is enabled, DOKS runs the driver’s node component as a DaemonSet named `csi-nfs-node` in the `kube-system` namespace. To confirm it’s running on every node:

```shell
kubectl get daemonset csi-nfs-node -n kube-system
```

The output looks similar to the following:

```text
Output
NAME           DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR   AGE
csi-nfs-node   3         3         3       3            3           <none>          2m
```

The `DESIRED` and `READY` columns should match your node count.

The plugin also installs four storage classes. To list them:

```shell
kubectl get storageclass
```

The NFS storage classes appear alongside the default block storage classes:

```text
Output
NAME                            PROVISIONER                 RECLAIMPOLICY   VOLUMEBINDINGMODE   ALLOWVOLUMEEXPANSION   AGE
do-block-storage (default)      dobs.csi.digitalocean.com   Delete          Immediate           true                   30m
do-nfs-high-performance         nfs.csi.digitalocean.com    Delete          Immediate           true                   2m
do-nfs-high-performance-retain  nfs.csi.digitalocean.com    Retain          Immediate           true                   2m
do-nfs-standard                 nfs.csi.digitalocean.com    Delete          Immediate           true                   2m
do-nfs-standard-retain          nfs.csi.digitalocean.com    Retain          Immediate           true                   2m
```

## Choose a Storage Class

Each storage class maps to a Network File Storage [performance tier](https://docs.digitalocean.com/products/nfs/details/features/index.html.md) and a [reclaim policy](https://kubernetes.io/docs/concepts/storage/persistent-volumes/#reclaiming) that controls what happens to the share when you delete its PVC:

| Storage class | Performance tier | Minimum size | Reclaim policy |
|---|---|---|---|
| `do-nfs-standard` | Standard | 50 GiB | `Delete` |
| `do-nfs-standard-retain` | Standard | 50 GiB | `Retain` |
| `do-nfs-high-performance` | High Performance | 500 GiB | `Delete` |
| `do-nfs-high-performance-retain` | High Performance | 500 GiB | `Retain` |

**Warning**:

  With the `Delete` reclaim policy, deleting the PVC permanently destroys the NFS share, including all data and snapshots on it. This differs from the static provisioning workflow, where deleting Kubernetes objects never deletes the share. Use a `-retain` storage class if the data must outlive the claim.

With a `Retain` storage class, deleting the PVC leaves the share intact, and you continue to be billed for it until you [delete the share](https://docs.digitalocean.com/products/nfs/how-to/delete/index.html.md) yourself.

All four storage classes mount shares with NFS protocol version 4.1 and `nconnect=8`, which opens 8 parallel TCP connections to the NFS server to improve throughput.

## Create a PersistentVolumeClaim

To provision a share, create a PVC that references one of the NFS storage classes. If you request less storage than the minimum size for the storage class’s performance tier, the driver provisions a share at the minimum size instead. Shares can be no larger than 32 TiB. Create the following config file named `nfs-dynamic-pvc.yaml`:

```yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: nfs-dynamic-pvc
spec:
  storageClassName: do-nfs-standard
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 50Gi
```

Use `kubectl apply` to create the PVC:

```shell
kubectl apply -f nfs-dynamic-pvc.yaml
```

The driver creates a new share named `pvc-` followed by a unique identifier, such as `pvc-1ab2c3d4-e5f6-7890-abcd-ef1234567890`, on the cluster’s VPC network. Provisioning a share takes a few minutes; the PVC stays in the `Pending` state until the share is active. To watch its progress:

```shell
kubectl get pvc nfs-dynamic-pvc
```

Once the share is active, the PVC’s status changes to `Bound`:

```text
Output
NAME              STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS      AGE
nfs-dynamic-pvc   Bound    pvc-1ab2c3d4-e5f6-7890-abcd-ef1234567890   50Gi       RWX            do-nfs-standard   3m
```

The share also appears on the **Network File Storage** page in the [control panel](https://cloud.digitalocean.com), and billing for it begins when it’s created. If the PVC stays `Pending`, run `kubectl describe pvc nfs-dynamic-pvc` and check the events; errors from the DigitalOcean API, such as reaching your account’s share limit, appear there.

## Mount the PVC in Your Workload

Mount the PVC the same way as any other volume. Because Network File Storage shares enforce root squashing, workloads that write to the share must run as a non-root user; for details, see [Run Workloads as a Non-Root User](https://docs.digitalocean.com/products/kubernetes/how-to/use-nfs-storage/index.html.md#run-workloads-as-a-non-root-user).

The following config file, `pod-with-nfs.yaml`, mounts the PVC to a pod running as UID and GID 1000, and writes the current date to a log file on the share every 5 seconds:

```yaml
apiVersion: v1
kind: Pod
metadata:
  name: nfs-test-pod
spec:
  securityContext:
    runAsUser: 1000
    runAsGroup: 1000
  volumes:
    - name: my-nfs-share
      persistentVolumeClaim:
        claimName: nfs-dynamic-pvc
  containers:
    - name: my-app-container
      image: busybox
      command: ["/bin/sh", "-c", "while true; do date >> /data/test.log; sleep 5; done"]
      volumeMounts:
        - name: my-nfs-share
          mountPath: "/data"
```

Use `kubectl apply` to create the pod:

```shell
kubectl apply -f pod-with-nfs.yaml
```

Any number of pods, on any node in the cluster, can mount the same PVC and read and write to the share simultaneously.

## Resize a Share

The NFS storage classes support online volume expansion, so you can increase a share’s size without unmounting it or restarting workloads. To resize, update the `storage` request on the PVC:

```shell
kubectl patch pvc nfs-dynamic-pvc -p '{ "spec": { "resources": { "requests": { "storage": "100Gi" }}}}'
```

Shares can only be increased in size, never decreased.

## Delete a Share

How you delete a dynamically provisioned share depends on the storage class’s reclaim policy:

- With a `Delete` storage class, delete the PVC with `kubectl delete pvc <your-pvc-name>`. The driver destroys the share, including its data and snapshots, and billing stops.
- With a `Retain` storage class, deleting the PVC leaves the share running. To stop billing, [delete the share](https://docs.digitalocean.com/products/nfs/how-to/delete/index.html.md) from the control panel or API.

The reclaim policy only applies when you delete a PVC on a running cluster. [Destroying the cluster](https://docs.digitalocean.com/products/kubernetes/how-to/destroy-clusters/index.html.md) does not delete the cluster’s dynamically provisioned shares by default, regardless of their storage class’s reclaim policy. The shares appear in the cluster’s associated resources in the destroy flow, where you can select them for deletion along with the cluster; otherwise they remain in your account and continue to accrue charges.

**Note**:

  You can select individual NFS shares for deletion from the control panel’s destroy flow, or delete all of a cluster’s associated resources, including its NFS shares, using `doctl kubernetes cluster delete --dangerous` or the API’s `destroy_with_associated_resources/dangerous` endpoint. Selectively deleting specific NFS shares by ID through `doctl kubernetes cluster delete-selective` or the API’s `destroy_with_associated_resources/selective` endpoint is not yet supported.