diff --git a/src/frontend/src/content/docs/deployment/kubernetes/persistent-volumes.mdx b/src/frontend/src/content/docs/deployment/kubernetes/persistent-volumes.mdx index fb1ef176b..46b4a0b80 100644 --- a/src/frontend/src/content/docs/deployment/kubernetes/persistent-volumes.mdx +++ b/src/frontend/src/content/docs/deployment/kubernetes/persistent-volumes.mdx @@ -185,6 +185,71 @@ await api.withKubernetesPersistentVolumeMount(media, '/srv/media'); Any workload bound to a persistent volume is rendered as a `StatefulSet` instead of a `Deployment`, regardless of its default workload kind. There is no opt-out. A `Deployment` with a `ReadWriteOnce` claim and more than one replica is broken by design because the second pod can't mount the same volume. Therefore, binding to a durable volume automatically selects the `StatefulSet` path, which gives pods stable identity and ordered rollout. +## Default pod security context + +A Kubernetes access mode such as `ReadWriteOnce` controls how a volume can be attached and mounted; it doesn't grant the container's Linux process permission to write to the mounted filesystem. Without further configuration, a freshly provisioned volume is typically owned by `root:root`, so a workload that runs as a non-root user fails on its first write even though the PVC is `Bound` and the pod is `Running`. + +To avoid that out-of-the-box failure, both `WithPersistentVolume(...)` overloads add the following pod security context to the workload automatically: + +```yaml +securityContext: + fsGroup: 2000 + fsGroupChangePolicy: OnRootMismatch +``` + +`fsGroup` adds a supplemental group to the pod's processes and, for supported volume types, instructs Kubernetes or the CSI driver to make the mounted volume accessible to that group. It doesn't change the image-defined UID or primary GID, so Aspire doesn't need to know which identity the image uses. Aspire always uses the same group ID, `2000`, rather than choosing a new value on each deployment, because group ownership is persisted on the volume. `OnRootMismatch` avoids an unnecessary recursive ownership change when the volume already has the expected group. + +This default only applies to workloads bound through `WithPersistentVolume(...)`. Ordinary workloads and legacy PVC generation through the Kubernetes environment's default storage type are unaffected, and read-only mounts remain read-only. + +### Overriding or removing the default + +The default security context is applied before any `PublishAsKubernetesService` callback runs, so a workload or cluster that needs a different group can replace it: + + + + +```csharp title="AppHost.cs" +builder.AddProject("webfrontend") + .WithPersistentVolume(data, "/data") + .PublishAsKubernetesService(resource => + { + var podSpec = resource.Workload?.PodTemplate.Spec + ?? throw new InvalidOperationException("The Kubernetes workload was not generated."); + + podSpec.SecurityContext ??= new(); + podSpec.SecurityContext.FsGroup = 3000; + }); +``` + + + + +The generated `OnRootMismatch` change policy is retained unless the callback also replaces it. + +To remove the generated pod security context entirely — for example, when ownership is managed by the image, an admission controller, or storage-specific configuration — set it to `null` in the same callback: + + + + +```csharp title="AppHost.cs" +builder.AddProject("webfrontend") + .WithPersistentVolume(data, "/data") + .PublishAsKubernetesService(resource => + { + var podSpec = resource.Workload?.PodTemplate.Spec + ?? throw new InvalidOperationException("The Kubernetes workload was not generated."); + + podSpec.SecurityContext = null; + }); +``` + + + + + + ## 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.