diff --git a/src/frontend/config/sidebar/deployment.topics.ts b/src/frontend/config/sidebar/deployment.topics.ts
index c682e7cab..4b2fdef53 100644
--- a/src/frontend/config/sidebar/deployment.topics.ts
+++ b/src/frontend/config/sidebar/deployment.topics.ts
@@ -150,6 +150,10 @@ export const deploymentTopics: StarlightSidebarTopicsUserConfig = {
label: 'External Helm charts',
slug: 'deployment/kubernetes/helm-charts',
},
+ {
+ label: 'Persistent volumes',
+ slug: 'deployment/kubernetes/persistent-volumes',
+ },
{
label: 'Ingress & Gateway API',
slug: 'deployment/kubernetes-ingress',
diff --git a/src/frontend/src/content/docs/deployment/kubernetes.mdx b/src/frontend/src/content/docs/deployment/kubernetes.mdx
index 1985873d1..003f03f72 100644
--- a/src/frontend/src/content/docs/deployment/kubernetes.mdx
+++ b/src/frontend/src/content/docs/deployment/kubernetes.mdx
@@ -50,6 +50,8 @@ When you publish or deploy, Aspire translates your application model into Kubern
| Endpoints | Services |
| Volumes | PersistentVolumes and PersistentVolumeClaims |
+For durable storage that you model as first-class resources and bind to workloads, see [Persistent volumes on Kubernetes](/deployment/kubernetes/persistent-volumes/).
+
## Publish vs deploy
Both Kubernetes targets support `aspire publish`. The key difference is whether the target also supports `aspire deploy`:
@@ -91,4 +93,9 @@ For more on the pipeline model and how publish and deploy relate, see [Pipelines
description="Install pre-existing Helm charts alongside your application using AddHelmChart."
href="/deployment/kubernetes/helm-charts/"
/>
+
\ No newline at end of file
diff --git a/src/frontend/src/content/docs/deployment/kubernetes/clusters.mdx b/src/frontend/src/content/docs/deployment/kubernetes/clusters.mdx
index 551ec65a3..511f48eed 100644
--- a/src/frontend/src/content/docs/deployment/kubernetes/clusters.mdx
+++ b/src/frontend/src/content/docs/deployment/kubernetes/clusters.mdx
@@ -350,6 +350,7 @@ See [Install external Helm charts](/deployment/kubernetes/helm-charts/) for a fu
- [Kubernetes integration](/integrations/compute/kubernetes/)
- [Deploy to AKS](/deployment/kubernetes/aks/)
- [Install external Helm charts](/deployment/kubernetes/helm-charts/)
+- [Persistent volumes on Kubernetes](/deployment/kubernetes/persistent-volumes/)
- [Pipelines and app topology](/deployment/pipelines/)
- [Publishing and deployment overview](/deployment/deploy-with-aspire/)
- [Kubernetes documentation](https://kubernetes.io/docs/)
diff --git a/src/frontend/src/content/docs/deployment/kubernetes/persistent-volumes.mdx b/src/frontend/src/content/docs/deployment/kubernetes/persistent-volumes.mdx
new file mode 100644
index 000000000..951c05da8
--- /dev/null
+++ b/src/frontend/src/content/docs/deployment/kubernetes/persistent-volumes.mdx
@@ -0,0 +1,241 @@
+---
+title: Persistent volumes on Kubernetes
+seoTitle: 'Persistent volumes for Aspire on Kubernetes clusters'
+description: Model durable storage as first-class persistent volume resources in your Aspire AppHost, then bind them to Kubernetes workloads with a storage class, capacity, and access modes.
+---
+
+import { Tabs, TabItem, Aside } from '@astrojs/starlight/components';
+import LearnMore from '@components/LearnMore.astro';
+
+Stateful workloads — databases, message brokers, or any service that writes durable data — need storage that survives pod restarts and rescheduling. Call `AddPersistentVolume` on a Kubernetes environment to describe a durable disk once in your AppHost, configure its storage class, capacity, access modes, and annotations with fluent methods, then bind it to one or more workloads:
+
+
+
+
+```csharp title="AppHost.cs"
+var k8s = builder.AddKubernetesEnvironment("k8s");
+
+var pgData = k8s.AddPersistentVolume("pg-data")
+ .WithStorageClass("managed-csi")
+ .WithCapacity("20Gi");
+```
+
+
+
+
+```typescript title="apphost.mts"
+const k8s = await builder.addKubernetesEnvironment('k8s');
+
+const pgData = await k8s.addPersistentVolume('pg-data');
+await pgData.withStorageClass('managed-csi');
+await pgData.withCapacity('20Gi');
+```
+
+
+
+
+At publish time, the volume renders as a `v1.PersistentVolumeClaim` in the generated Helm chart. Like [ingress and gateway resources](/deployment/kubernetes-ingress/), you configure the volume once and reference it from workloads, rather than relying on a single environment-wide storage shape for every mount.
+
+
+
+## Prerequisites
+
+- The [Aspire.Hosting.Kubernetes](/integrations/compute/kubernetes/) hosting integration installed in your AppHost.
+- A [Kubernetes environment](/integrations/compute/kubernetes/#add-kubernetes-environment) added to your AppHost.
+- A cluster with a storage class that can provision the volumes you request. Most managed clusters ship a default storage class.
+
+## Add a persistent volume
+
+The full set of configuration methods lets you pin the storage class, capacity, access modes, and provisioner annotations. A complete AppHost that defines a volume looks like this:
+
+
+
+
+```csharp title="AppHost.cs"
+using Aspire.Hosting.Kubernetes;
+
+var builder = DistributedApplication.CreateBuilder(args);
+
+var k8s = builder.AddKubernetesEnvironment("k8s");
+
+var pgData = k8s.AddPersistentVolume("pg-data")
+ .WithStorageClass("managed-csi")
+ .WithCapacity("20Gi")
+ .WithAccessMode(PersistentVolumeAccessMode.ReadWriteOnce)
+ .WithVolumeAnnotation("disk.csi.azure.com/skuName", "Premium_LRS");
+
+builder.Build().Run();
+```
+
+
+
+
+```typescript title="apphost.mts"
+import { createBuilder } from './.aspire/modules/aspire.mjs';
+
+const builder = await createBuilder();
+
+const k8s = await builder.addKubernetesEnvironment('k8s');
+
+const pgData = await k8s.addPersistentVolume('pg-data');
+await pgData.withStorageClass('managed-csi');
+await pgData.withCapacity('20Gi');
+await pgData.withVolumeAnnotation('disk.csi.azure.com/skuName', 'Premium_LRS');
+
+await builder.build().run();
+```
+
+
+
+
+Every configuration method is optional. When you don't set a storage class, the cluster's default storage class provisions the backing disk. When you don't set a capacity or access mode, the environment's default storage size and read-write policy apply.
+
+The available configuration methods are:
+
+| C# | TypeScript | Description |
+|---|---|---|
+| `WithStorageClass(string)` | `withStorageClass` / `withPvStorageClassParam` | Sets `spec.storageClassName` on the PVC. In C#, the single method accepts a literal string or an Aspire parameter; in TypeScript, use `withStorageClass` for a literal and `withPvStorageClassParam` for a parameter. |
+| `WithCapacity(string)` | `withCapacity` / `withPvCapacityParam` | Sets the requested storage on `spec.resources.requests.storage` (for example, `"20Gi"`). In TypeScript, use `withCapacity` for a literal and `withPvCapacityParam` for a parameter. |
+| `WithAccessMode(PersistentVolumeAccessMode)` | `withAccessMode` | Adds an entry to `spec.accessModes`. Call multiple times to declare more than one mode. |
+| `WithVolumeAnnotation(string, string)` | `withVolumeAnnotation` / `withVolumeAnnotationParam` | Adds a key-value pair to the PVC's `metadata.annotations`. Useful for CSI driver hints, dynamic provisioner parameters, or backup tooling tags. In TypeScript, use `withVolumeAnnotation` for a literal value and `withVolumeAnnotationParam` for a parameter. |
+
+The `PersistentVolumeAccessMode` values map directly to the Kubernetes access modes:
+
+| Access mode | Description |
+|---|---|
+| `ReadWriteOnce` | Mounted as read-write by a single node. Most common for block-storage-backed databases. |
+| `ReadOnlyMany` | Mounted as read-only by many nodes simultaneously. |
+| `ReadWriteMany` | Mounted as read-write by many nodes simultaneously. Typically used for shared file stores such as Azure Files or NFS. |
+| `ReadWriteOncePod` | Mounted as read-write by a single pod. Requires Kubernetes 1.27 or later. |
+
+## Bind a volume to a workload
+
+After defining a volume, bind it to the workloads that mount it. There are two overloads, depending on whether the workload already declares a named volume.
+
+### Bind by name
+
+Use the name-match overload when the workload already declares a volume — for example, through `WithVolume("name", "/path")` or an integration helper such as Postgres' `WithDataVolume()`. The persistent volume's name must match the workload volume's name so the publisher can route the pod's `volumes[]` entry through the generated PVC:
+
+
+
+
+```csharp title="AppHost.cs"
+var pgData = k8s.AddPersistentVolume("pg-data")
+ .WithStorageClass("managed-csi")
+ .WithCapacity("20Gi");
+
+builder.AddPostgres("pg")
+ .WithDataVolume("pg-data")
+ .WithPersistentVolume(pgData);
+```
+
+
+
+
+```typescript title="apphost.mts"
+const pgData = await k8s.addPersistentVolume('pg-data');
+await pgData.withStorageClass('managed-csi');
+await pgData.withCapacity('20Gi');
+
+const pg = await builder.addPostgres('pg');
+await pg.withDataVolume({ name: 'pg-data' });
+await pg.withKubernetesPersistentVolume(pgData);
+```
+
+
+
+
+### Bind with a mount path
+
+Use the mount-path overload when the workload doesn't already declare a named volume. It creates the mount itself, so it works for projects and any compute resource. Pass the mount path inside the container, and optionally mount read-only:
+
+
+
+
+```csharp title="AppHost.cs"
+var media = k8s.AddPersistentVolume("media")
+ .WithStorageClass("azurefile-csi")
+ .WithCapacity("100Gi")
+ .WithAccessMode(PersistentVolumeAccessMode.ReadWriteMany);
+
+builder.AddProject("api")
+ .WithPersistentVolume(media, "/srv/media");
+```
+
+
+
+
+```typescript title="apphost.mts"
+const media = await k8s.addPersistentVolume('media');
+await media.withStorageClass('azurefile-csi');
+await media.withCapacity('100Gi');
+
+const api = await builder.addProject('api');
+await api.withKubernetesPersistentVolumeMount(media, '/srv/media');
+```
+
+
+
+
+
+
+## Unbound volumes and default storage
+
+Volumes on a workload that aren't bound to a `KubernetesPersistentVolumeResource` continue to use the environment's default storage type. You can mix a first-class persistent volume and an unbound ephemeral volume on the same workload — each is resolved independently at publish time.
+
+## Generated output
+
+Binding a container to a persistent volume by name produces a `StatefulSet` whose pod spec references the generated claim, alongside the `PersistentVolumeClaim` itself:
+
+```yaml title="StatefulSet (excerpt)"
+apiVersion: "apps/v1"
+kind: "StatefulSet"
+metadata:
+ name: "service-statefulset"
+spec:
+ template:
+ spec:
+ containers:
+ - image: "nginx:latest"
+ name: "service"
+ volumeMounts:
+ - name: "data"
+ mountPath: "/var/lib/data"
+ volumes:
+ - name: "data"
+ persistentVolumeClaim:
+ claimName: "data"
+ replicas: 1
+```
+
+```yaml title="PersistentVolumeClaim"
+apiVersion: "v1"
+kind: "PersistentVolumeClaim"
+metadata:
+ name: "data"
+ annotations:
+ volume.beta.kubernetes.io/storage-provisioner: "disk.csi.azure.com"
+spec:
+ storageClassName: "managed-csi"
+ accessModes:
+ - "ReadWriteOnce"
+ resources:
+ requests:
+ storage: "20Gi"
+```
+
+
+ To generate and inspect the Helm chart for yourself, see [Deploy to Kubernetes clusters](/deployment/kubernetes/clusters/).
+
+
+## See also
+
+- [Kubernetes integration](/integrations/compute/kubernetes/)
+- [Deploy to Kubernetes clusters](/deployment/kubernetes/clusters/)
+- [Deploy to AKS](/deployment/kubernetes/aks/)
+- [Persist data with volumes](/fundamentals/persist-data-volumes/)
+- [Kubernetes persistent volumes documentation](https://kubernetes.io/docs/concepts/storage/persistent-volumes/)
diff --git a/src/frontend/src/content/docs/fundamentals/persist-data-volumes.mdx b/src/frontend/src/content/docs/fundamentals/persist-data-volumes.mdx
index b3608b682..8516303e3 100644
--- a/src/frontend/src/content/docs/fundamentals/persist-data-volumes.mdx
+++ b/src/frontend/src/content/docs/fundamentals/persist-data-volumes.mdx
@@ -272,3 +272,4 @@ You can apply the volume concepts in the preceding code to a variety of services
- [Tutorial: Connect an ASP.NET Core app to SQL Server using Aspire and Entity Framework Core](/integrations/databases/efcore/sql-server/sql-server-get-started/)
- [Aspire orchestration overview](/get-started/app-host/)
+- [Persistent volumes on Kubernetes](/deployment/kubernetes/persistent-volumes/)
diff --git a/src/frontend/src/content/docs/integrations/compute/kubernetes.mdx b/src/frontend/src/content/docs/integrations/compute/kubernetes.mdx
index 5b1b82391..770c89d23 100644
--- a/src/frontend/src/content/docs/integrations/compute/kubernetes.mdx
+++ b/src/frontend/src/content/docs/integrations/compute/kubernetes.mdx
@@ -213,6 +213,49 @@ await k8s.withContainerRegistry(registry);
The container registry APIs are currently in preview. In C#, suppress the `ASPIRECOMPUTE003` diagnostic to use them.
+## Persistent volumes
+
+Model durable storage as a first-class `KubernetesPersistentVolumeResource` with `AddPersistentVolume`, configure its storage class, capacity, and access modes, then bind it to workloads. At publish time the volume renders as a `v1.PersistentVolumeClaim`, and any workload bound to it is promoted to a `StatefulSet`.
+
+
+
+
+```csharp title="AppHost.cs"
+using Aspire.Hosting.Kubernetes;
+
+var k8s = builder.AddKubernetesEnvironment("k8s");
+
+var pgData = k8s.AddPersistentVolume("pg-data")
+ .WithStorageClass("managed-csi")
+ .WithCapacity("20Gi");
+
+builder.AddPostgres("pg")
+ .WithDataVolume("pg-data")
+ .WithPersistentVolume(pgData);
+```
+
+
+
+
+```typescript title="apphost.mts"
+const k8s = await builder.addKubernetesEnvironment('k8s');
+
+const pgData = await k8s.addPersistentVolume('pg-data');
+await pgData.withStorageClass('managed-csi');
+await pgData.withCapacity('20Gi');
+
+const pg = await builder.addPostgres('pg');
+await pg.withDataVolume({ name: 'pg-data' });
+await pg.withKubernetesPersistentVolume(pgData);
+```
+
+
+
+
+
+ For the full configuration surface, binding overloads, access modes, and generated output, see [Persistent volumes on Kubernetes](/deployment/kubernetes/persistent-volumes/).
+
+
## Customize individual resources
Use `PublishAsKubernetesService` to modify the generated Kubernetes resources for individual services:
@@ -356,6 +399,7 @@ For complete walkthroughs, see [Deploy to Kubernetes clusters](/deployment/kuber
## See also
- [Deploy to Kubernetes](/deployment/kubernetes/)
+- [Persistent volumes on Kubernetes](/deployment/kubernetes/persistent-volumes/)
- [Expose services with Ingress and Gateway API](/deployment/kubernetes-ingress/)
- [Configure Ingress on AKS](/deployment/kubernetes-ingress-aks/)
- [Configure Gateway API on AKS](/deployment/kubernetes-gateway-aks/)