Introduction
Not every .NET application is completely stateless.
Many modern services need to retain data beyond the lifetime of a container or pod. Background workers may generate files, applications may maintain local working data, and stateful services may need persistent storage for information that must survive restarts.
Running these workloads on Kubernetes introduces an important question: how should persistent storage be represented and tested when the application is developed and orchestrated with .NET Aspire?
.NET Aspire provides a code-first approach for describing distributed applications and their resources. Kubernetes, on the other hand, provides the infrastructure required to run those applications at scale. When persistent storage is involved, developers need to understand where the application definition ends and where Kubernetes storage management begins.
Aspire 13.5 adds capabilities that make Kubernetes-oriented application modeling more useful, but persistent workloads still require careful validation.
This article walks through a practical approach to building a .NET service that uses persistent storage, modeling the application with Aspire, deploying it to Kubernetes, and testing whether data survives pod restarts.
Why Persistent Storage Matters
Containers are designed to be replaceable.
A typical Kubernetes deployment may look like:
Deployment
|
+-- Pod 1
+-- Pod 2
+-- Pod 3If Pod 1 is deleted, Kubernetes can create a replacement:
Old Pod
|
X
|
v
New PodData written only to the container's local filesystem should not be treated as durable application storage.
For persistent workloads, the architecture needs a storage layer:
.NET Application
|
v
Container
|
v
Persistent Volume Claim
|
v
Persistent Volume
|
v
Storage BackendThis distinction is critical when developing stateful .NET services.
Stateless vs Stateful Services
Before adding persistent storage, determine whether the application actually needs it.
| Characteristic | Stateless Service | Stateful Service |
|---|---|---|
| Local application data | Temporary | Persistent |
| Pod replacement | Usually simple | Requires storage consideration |
| Horizontal scaling | Generally easier | More complex |
| Persistent volume | Usually unnecessary | Often required |
| Example workload | REST API | File-processing service |
A normal ASP.NET Core API that stores all business data in a database may not need a persistent volume.
A service that creates files that must survive pod replacement may need one.
Do not add persistent storage simply because Kubernetes supports it.
Understand Aspire's Role
.NET Aspire helps developers define and orchestrate distributed applications.
A simplified application can contain:
AppHost
|
+-- Orders API
|
+-- Worker
|
+-- DatabaseThe AppHost describes relationships between application resources.
For example:
var builder = DistributedApplication.CreateBuilder(args);
var ordersApi = builder.AddProject<Projects.OrdersApi>("orders-api");
builder.AddProject<Projects.OrderWorker>("order-worker")
.WithReference(ordersApi);
builder.Build().Run();The exact resources in a production application will depend on its architecture.
When Kubernetes becomes the deployment target, the important question is how those application resources map to Kubernetes infrastructure.
Add a Persistent Storage Requirement
Consider a document-processing service.
Its responsibility is:
Upload
|
v
Process Document
|
v
Generate Output
|
v
Store ResultSuppose generated files must remain available after a pod restart.
The application now needs durable storage.
Conceptually:
Document Worker
|
v
/data/output
|
v
Persistent VolumeThe application should not need to know which physical storage system Kubernetes uses.
It should simply work with its mounted filesystem path.
Design the .NET Service Around a Storage Path
A useful pattern is to make the storage location configurable.
var builder = WebApplication.CreateBuilder(args);
var storagePath =
builder.Configuration["Storage:Path"]
?? "/data";
builder.Services.AddSingleton(
new StorageOptions(storagePath));
var app = builder.Build();
app.MapPost("/documents", async (
IFormFile file,
StorageOptions options) =>
{
var filePath = Path.Combine(
options.Path,
Path.GetFileName(file.FileName));
await using var stream =
File.Create(filePath);
await file.CopyToAsync(stream);
return Results.Ok(new
{
Path = filePath
});
});
app.Run();
public sealed record StorageOptions(string Path);The important design decision is that the application does not hard-code a host-specific storage location.
The storage path is configuration.
That makes the service easier to run in:
Development
Container
Kuberneteswithout changing application code.
Configure the Storage Path
For local development, you might use:
{
"Storage": {
"Path": "./data"
}
}Inside a container, the application can use:
/dataKubernetes can then mount persistent storage at that location.
This creates a clean separation:
Application
|
v
Configured Path
|
v
Container Mount
|
v
Persistent StorageDefine a Kubernetes PersistentVolumeClaim
A Kubernetes workload normally requests persistent storage through a PersistentVolumeClaim.
For example:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: document-storage
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10GiThe exact storage class and capacity should be selected according to the Kubernetes environment and workload.
The application should not assume that 10Gi is appropriate for every deployment.
Mount the Volume Into the Pod
The Deployment can mount the claim:
apiVersion: apps/v1
kind: Deployment
metadata:
name: document-worker
spec:
replicas: 1
selector:
matchLabels:
app: document-worker
template:
metadata:
labels:
app: document-worker
spec:
containers:
- name: document-worker
image: document-worker:latest
volumeMounts:
- name: document-data
mountPath: /data
volumes:
- name: document-data
persistentVolumeClaim:
claimName: document-storageThe important relationship is:
PVC
|
v
Pod Volume
|
v
/data
|
v
.NET ApplicationThe application simply reads and writes /data.
Connect Application Configuration to Kubernetes
The application's configuration should match the mounted location.
For example:
env:
- name: Storage__Path
value: "/data"ASP.NET Core configuration maps the double underscore to a nested configuration key.
The application can therefore read:
builder.Configuration["Storage:Path"]without knowing that Kubernetes is providing the directory.
This is a useful configuration boundary.
Model the Application With Aspire
Aspire can define the application resources and their relationships.
For example:
var builder = DistributedApplication.CreateBuilder(args);
var worker =
builder.AddProject<Projects.DocumentWorker>(
"document-worker");
builder.Build().Run();For local development, the application can use a local directory.
The Kubernetes deployment then provides the persistent volume.
This distinction is important because local orchestration and production infrastructure do not necessarily have identical storage semantics.
Local Storage Is Not the Same as Kubernetes Persistence
During local development, you might mount:
./data:/dataThe directory survives because it exists on the developer's machine.
Kubernetes storage works differently.
The pod may disappear:
Pod A
|
Xbut the persistent volume remains:
Persistent Storage
|
v
Pod B
|
v
/dataTherefore, the actual persistence test must happen in the Kubernetes environment.
Test Data Persistence
A simple persistence test should follow these steps.
Step 1: Deploy the Application
Start the application with its persistent volume.
Step 2: Write Data
Create a test file:
/data/persistence-test.txtThe file might contain:
Persistence test successfulStep 3: Verify the File
Confirm that the application can read the file.
Step 4: Delete the Pod
For example:
kubectl delete pod <pod-name>Kubernetes should create a replacement pod.
Step 5: Read the File Again
The new pod should be able to access the same persistent volume.
The expected flow is:
Write File
|
v
Pod A
|
v
Persistent Volume
|
X
Pod A Deleted
|
v
Pod B Created
|
v
Persistent Volume
|
v
Read FileIf the file disappears, the application is not using persistent storage correctly.
Test Application Restart Separately
Pod deletion is not the only failure scenario.
Also test a normal application restart.
For example:
Application
|
v
Restart
|
v
Same Pod
|
v
Read Persistent DataThis verifies that the application's initialization logic does not accidentally delete or overwrite existing data.
For example, avoid startup logic that always executes:
Directory.Delete(
storagePath,
recursive: true);unless that behavior is explicitly required.
Test Pod Replacement
A stronger test recreates the pod entirely.
kubectl get podsThen:
kubectl delete pod <pod-name>After Kubernetes creates the replacement:
kubectl get podsCheck the new pod and verify that the expected data is still present.
This test is more representative of Kubernetes operations than simply restarting the .NET process.
Test Volume Mount Failures
Persistent storage can fail before the application even starts.
Inspect the pod:
kubectl describe pod <pod-name>Look for events related to:
Volume mounting
PersistentVolumeClaim
Storage class
Permissions
Scheduling
A useful troubleshooting path is:
Pod
|
v
PVC
|
v
PV
|
v
Storage BackendFind the first failed layer.
Test File Permissions
The application container may run as a non-root user.
For example:
FROM mcr.microsoft.com/dotnet/aspnet:10.0
WORKDIR /app
COPY ./publish/ .
USER app
ENTRYPOINT ["dotnet", "DocumentWorker.dll"]The mounted directory must be writable by the application identity.
Otherwise, the application may start successfully but fail when attempting to write a file.
Test the complete operation:
Application Starts
|
v
Volume Mounted
|
v
Write File
|
v
Read FileDo not assume that a successful mount means the application has permission to use it.
Consider Replica Count Carefully
Persistent storage introduces additional considerations when scaling.
Suppose:
replicas: 3and the volume uses:
accessModes:
- ReadWriteOnceThe storage model may not support multiple pods writing to the same volume in the way the application expects.
This is an architectural decision, not simply a Kubernetes configuration detail.
For a file-processing service, a better architecture might be:
API
|
v
Object Storage
|
v
Workersrather than:
Multiple Pods
|
v
Shared Local FilesystemThe correct choice depends on the workload.
Persistent Storage Does Not Replace a Database
A mounted volume is not automatically a replacement for a database.
Use persistent storage for workloads that genuinely require filesystem semantics.
For example:
Generated files
Temporary processing state that must survive restart
Stateful application data where filesystem persistence is explicitly required
For transactional business data, a database is often more appropriate.
The architecture might therefore be:
.NET API
|
+------> PostgreSQL
|
+------> Persistent File StorageEach storage mechanism serves a different purpose.
Test Backup and Recovery Assumptions
Persistence and backup are different concepts.
A persistent volume can survive pod deletion while still being vulnerable to:
Storage failure
Accidental deletion
Cluster failure
Operator mistakes
Data corruption
Therefore, a production design should define how the stored data is backed up and restored.
The application compatibility test should at least document:
Data Location
Backup Strategy
Recovery Procedure
RetentionDo not describe a persistent volume as a complete disaster-recovery solution.
Test Deployment Updates
A persistent workload should also be tested during an application deployment.
For example:
Version 1
|
v
Persistent Data
|
v
Rolling Deployment
|
v
Version 2
|
v
Persistent Data Still AvailableVerify that application updates do not accidentally:
Delete files
Change storage paths
Change permissions
Mount a different volume
Initialize storage incorrectly
This is especially important when the application image changes.
Test Configuration Changes
Storage paths should be treated as deployment configuration.
For example:
env:
- name: Storage__Path
value: "/data"If the path changes:
/datato:
/storagethe volume mount must change as well.
Otherwise:
Application
|
v
/storage
|
X
Volume mounted at /dataThe application may appear healthy but write data into ephemeral container storage.
Common Mistakes
Writing to the Container Filesystem
Files written outside the persistent mount can disappear when the pod is replaced.
Assuming a PVC Means the Application Is Persistent
The application must actually write to the mounted path.
Ignoring File Permissions
A volume can mount successfully while the application cannot write to it.
Using ReadWriteOnce With an Incompatible Scaling Model
The storage access mode must match how the application scales.
Treating Persistent Storage as a Backup
Persistence does not automatically provide backup or disaster recovery.
Hard-Coding Storage Paths
Hard-coded paths make local, container, and Kubernetes environments harder to manage.
Testing Only Pod Startup
The application should be tested after pod replacement, not just after initial deployment.
Troubleshooting
Pod Is Stuck Waiting for a Volume
Check:
kubectl get pvcThen:
kubectl describe pvc document-storageLook for:
Pending claims
Storage class problems
Capacity issues
Access mode problems
Application Cannot Write to /data
Check:
Volume mount.
Container user.
Directory ownership.
Filesystem permissions.
Security context.
The application should run with the minimum permissions necessary.
Data Disappears After Pod Restart
Verify that the application writes to:
/dataand that /data is actually backed by the PVC.
A common mistake is writing to:
/app/datawhile the persistent volume is mounted at:
/dataMultiple Pods Cannot Use the Volume
Review the storage access mode and application architecture.
If the service requires multiple replicas to write simultaneously, the storage solution must support the required access pattern.
Data Exists in One Pod but Not Another
Confirm that both pods are mounting the same persistent storage resource and that the storage backend supports the requested access mode.
Best Practices
Determine whether the service genuinely needs persistent storage.
Keep storage paths configurable.
Separate application logic from infrastructure-specific storage details.
Use PersistentVolumeClaims for Kubernetes persistence.
Test pod deletion and replacement.
Test application restarts.
Validate file permissions.
Test rolling deployments.
Verify storage behavior with the intended replica count.
Treat persistence and backup as separate concerns.
Monitor storage capacity.
Avoid storing sensitive data without appropriate protection.
Document the recovery process.
Test storage failure scenarios where practical.
Keep stateful and stateless workloads architecturally distinct.
Advantages
Data can survive pod replacement.
Applications can use normal filesystem APIs.
Storage configuration can remain separate from application code.
Kubernetes can manage the lifecycle of the application and its storage relationship.
Aspire provides a structured way to model the distributed application during development.
Disadvantages
Stateful workloads are more complex than stateless services.
Storage configuration varies between Kubernetes environments.
Scaling can become more complicated.
Persistent volumes do not automatically solve backup and disaster recovery.
Storage failures can prevent application startup.
File permissions and security contexts require additional testing.
A Practical Aspire and Kubernetes Architecture
A simple production-oriented design can look like:
.NET Aspire
|
v
Application
Model
|
v
Kubernetes Deployment
|
+------------+------------+
| |
v v
.NET Service Kubernetes Service
|
v
/data Mount
|
v
PersistentVolumeClaim
|
v
Persistent StorageThe application remains focused on its business functionality.
Kubernetes manages the runtime and storage infrastructure.
Aspire provides the application-oriented orchestration model used during development and deployment workflows.
Persistence Validation Checklist
Before considering the service ready, verify:
| Test | Expected Result |
|---|---|
| Application starts | Pass |
| Volume mounts | Pass |
| Application writes data | Pass |
| Application reads data | Pass |
| Pod restarts | Data remains |
| Pod is deleted | Data remains |
| New pod starts | Data accessible |
| Rolling deployment | Data remains |
| Permissions | Correct |
| Multiple replicas | Supported configuration |
| Storage capacity | Monitored |
| Backup process | Documented |
| Recovery process | Tested where applicable |
This checklist is more valuable than simply verifying that the Deployment reaches the Running state.
Conclusion
Persistent workloads require a different mindset from ordinary stateless .NET services. Kubernetes can replace pods at any time, so application data that must survive those replacements needs to live outside the container's ephemeral filesystem.
.NET Aspire provides a useful application-oriented way to model distributed .NET services, while Kubernetes provides the infrastructure primitives required for persistent workloads. The key is to keep those responsibilities clearly separated. The .NET application should work with a configurable storage path, while Kubernetes determines how that path is backed by persistent infrastructure.
The most important validation is simple: write data, replace the pod, and verify that the data is still available. From there, test permissions, rolling deployments, replica behavior, storage failures, and recovery requirements. A service should only be considered production-ready when its persistence behavior has been tested under the same lifecycle events that Kubernetes will perform in the real environment.

Join the conversation! Your thoughts help the community grow.