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 3

If Pod 1 is deleted, Kubernetes can create a replacement:

Old Pod
   |
   X
   |
   v
New Pod

Data 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 Backend

This distinction is critical when developing stateful .NET services.

Stateless vs Stateful Services

Before adding persistent storage, determine whether the application actually needs it.

CharacteristicStateless ServiceStateful Service
Local application dataTemporaryPersistent
Pod replacementUsually simpleRequires storage consideration
Horizontal scalingGenerally easierMore complex
Persistent volumeUsually unnecessaryOften required
Example workloadREST APIFile-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
   |
   +-- Database

The 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 Result

Suppose generated files must remain available after a pod restart.

The application now needs durable storage.

Conceptually:

Document Worker
      |
      v
 /data/output
      |
      v
Persistent Volume

The 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
Kubernetes

without changing application code.

Configure the Storage Path

For local development, you might use:

{
  "Storage": {
    "Path": "./data"
  }
}

Inside a container, the application can use:

/data

Kubernetes can then mount persistent storage at that location.

This creates a clean separation:

Application
    |
    v
Configured Path
    |
    v
Container Mount
    |
    v
Persistent Storage

Define 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: 10Gi

The 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-storage

The important relationship is:

PVC
 |
 v
Pod Volume
 |
 v
/data
 |
 v
.NET Application

The 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:/data

The directory survives because it exists on the developer's machine.

Kubernetes storage works differently.

The pod may disappear:

Pod A
  |
  X

but the persistent volume remains:

Persistent Storage
       |
       v
Pod B
       |
       v
/data

Therefore, 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.txt

The file might contain:

Persistence test successful

Step 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 File

If 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 Data

This 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 pods

Then:

kubectl delete pod <pod-name>

After Kubernetes creates the replacement:

kubectl get pods

Check 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:

A useful troubleshooting path is:

Pod
 |
 v
PVC
 |
 v
PV
 |
 v
Storage Backend

Find 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 File

Do 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: 3

and the volume uses:

accessModes:
  - ReadWriteOnce

The 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
Workers

rather than:

Multiple Pods
      |
      v
Shared Local Filesystem

The 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:

For transactional business data, a database is often more appropriate.

The architecture might therefore be:

.NET API
   |
   +------> PostgreSQL
   |
   +------> Persistent File Storage

Each 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:

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
Retention

Do 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 Available

Verify that application updates do not accidentally:

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:

/data

to:

/storage

the volume mount must change as well.

Otherwise:

Application
   |
   v
/storage
   |
   X
Volume mounted at /data

The 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 pvc

Then:

kubectl describe pvc document-storage

Look for:

Application Cannot Write to /data

Check:

  1. Volume mount.

  2. Container user.

  3. Directory ownership.

  4. Filesystem permissions.

  5. Security context.

The application should run with the minimum permissions necessary.

Data Disappears After Pod Restart

Verify that the application writes to:

/data

and that /data is actually backed by the PVC.

A common mistake is writing to:

/app/data

while the persistent volume is mounted at:

/data

Multiple 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

  1. Determine whether the service genuinely needs persistent storage.

  2. Keep storage paths configurable.

  3. Separate application logic from infrastructure-specific storage details.

  4. Use PersistentVolumeClaims for Kubernetes persistence.

  5. Test pod deletion and replacement.

  6. Test application restarts.

  7. Validate file permissions.

  8. Test rolling deployments.

  9. Verify storage behavior with the intended replica count.

  10. Treat persistence and backup as separate concerns.

  11. Monitor storage capacity.

  12. Avoid storing sensitive data without appropriate protection.

  13. Document the recovery process.

  14. Test storage failure scenarios where practical.

  15. Keep stateful and stateless workloads architecturally distinct.

Advantages

Disadvantages

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 Storage

The 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:

TestExpected Result
Application startsPass
Volume mountsPass
Application writes dataPass
Application reads dataPass
Pod restartsData remains
Pod is deletedData remains
New pod startsData accessible
Rolling deploymentData remains
PermissionsCorrect
Multiple replicasSupported configuration
Storage capacityMonitored
Backup processDocumented
Recovery processTested 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.