Skip to content

Update the "Persist .NET Aspire project data using volumes" article #2903

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 60 additions & 26 deletions docs/fundamentals/persist-data-volumes.md
Original file line number Diff line number Diff line change
@@ -1,32 +1,48 @@
---
title: Persist data with .NET Aspire using volume mounts
description: Learn about .NET Aspire volume configurations.
ms.date: 04/26/2024
title: Persist .NET Aspire project data using volumes or bind mounts
description: Learn about .NET Aspire configurations that persist data across restarts by using volumes and bind mounts in containers.
ms.date: 03/28/2025
ms.topic: how-to
uid: fundamentals/persist-data-volumes
---

# Persist .NET Aspire project data using volumes
# Persist .NET Aspire project data using volumes or bind mounts

In this article, you learn how to configure .NET Aspire projects to persist data across app launches using volumes. A continuous set of data during local development is useful in many scenarios. Various .NET Aspire resource container types are able to leverage volume storage, such as PostgreSQL, Redis and Azure Storage.
Every time you start and stop a .NET Aspire project, the app also creates and destroys the app resource containers. Any data or files stored in those containers during a debugging session is lost for subsequent sessions. Many development teams prefer to keep this data across debugging sessions so that, for example, they don't have to repopulate a database with sample data for each run.

## When to use volumes
In this article, you learn how to configure .NET Aspire projects to persist data across app launches. A continuous set of data during local development is useful in many scenarios. Various .NET Aspire resource container types are able to leverage volumes and bind mounts, such as PostgreSQL, Redis and Azure Storage.

By default, every time you start and stop a .NET Aspire project, the app also creates and destroys the app resource containers. This setup creates problems when you want to persist data in a database or storage services between app launches for testing or debugging. For example, you may want to handle the following scenarios:
## When to persist project data

- Work with a continuous set of data in a database during an extended development session.
Suppose you have a .NET Aspire solution with a database resource. By default, data is saved in the container for that resource. Because all the resource containers are destroyed when you stop your app, you lose that data and won't see it the next time you run the solution. This setup creates problems when you want to persist data in a database or storage services between app launches for testing or debugging. For example, you may want to:

- Work with a continuous set of data in a database during an extended development session across multiple restarts.
- Test or debug a changing set of files in an Azure Blob Storage emulator.
- Maintain cached data or messages in a Redis instance across app launches.

These goals can all be accomplished using volumes. With volumes, you decide which services retain data between launches of your .NET Aspire project.
You can accomplish these goals using volumes or bind mounts. These objects store data outside the container in a directory on the container host, so it's not destroyed with the container. This way, you decide which services retain data between launches of your .NET Aspire project.

> [!NOTE]
> Volumes and bind mounts are features of your container runtime: Docker or Podman. .NET Aspire includes methods that make it easy to work with those features.

## Compare volumes and bind mounts

Both volumes and bind mounts store data in a directory on the container host. Because this directory is outside the container, data isn't destroyed when the container stops. Volumes and bind mounts, however behave differently:

- **Volumes**: The container runtime creates and controls volumes. Volumes are isolated from the core functionality of the container host.
- **Bind mounts**: The container runtime mounts a file or directory on the host machine. Both the container and the host machine can access the contents of the bind mount.

## Understand volumes
Volumes are more secure and portable than bind mounts. They also perform better and you should use them wherever possible. Use bind mounts only if you need to access or modify the data from your host machine.

Volumes are the recommended way to persist data generated by containers and supported on both Windows and Linux. Volumes can store data from multiple containers at a time, offer high performance and are easy to back up or migrate. With .NET Aspire, you configure a volume for each resource container using the <xref:Aspire.Hosting.ContainerResourceBuilderExtensions.WithBindMount%2A?displayProperty=nameWithType> method, which accepts three parameters:
## Use volumes

- **Source**: The source path of the volume, which is the physical location on the host.
- **Target**: The target path in the container of the data you want to persist.
Volumes are the recommended way to persist data generated by containers and they're supported on both Windows and Linux. Volumes can store data from multiple containers at a time, offer high performance, and are easy to back up or migrate. With .NET Aspire, you configure a volume for each resource container using the <xref:Aspire.Hosting.ContainerResourceBuilderExtensions.WithVolume%2A?displayProperty=nameWithType> method, which accepts three parameters:

For the remainder of this article, imagine that your exploring a `Program` class in a .NET Aspire [app host project](app-host-overview.md) that's already defined the distributed app builder bits:
- **`name`**: An optional name for the volume.
- **`target`**: The target path in the container of the data you want to persist.
- **`isReadOnly`**: A Boolean flag that indicates whether the data in the volume can be changed. The default value is `false`.

For the remainder of this article, imagine that you're exploring a `Program` class in a .NET Aspire [app host project](app-host-overview.md) that's already defined the distributed app builder bits:

```csharp
var builder = DistributedApplication.CreateBuilder(args);
Expand All @@ -38,28 +54,46 @@ var builder = DistributedApplication.CreateBuilder(args);
builder.Build().Run();
```

The first code snippet to consider uses the `WithBindMount` API to configure a volume for a SQL Server resource. The following code demonstrates how to configure a volume for a SQL Server resource in a .NET Aspire app host project:

:::code language="csharp" source="snippets/volumes/VolumeMounts.AppHost/Program.WithBindMount.cs" id="mount":::
The first code snippet to consider uses the <xref:Aspire.Hosting.ContainerResourceBuilderExtensions.WithVolume%2A?displayProperty=nameWithType> API to configure a volume for a SQL Server resource. The following code demonstrates how to configure a volume for a SQL Server resource in a .NET Aspire app host project:

In this example:
:::code language="csharp" source="snippets/volumes/VolumeMounts.AppHost/Program.WithVolume.cs" id="volume":::

- `VolumeMount.AppHost-sql-data` sets where the volume will be stored on the host.
- `/var/opt/mssql` sets the path to the database files in the container.
In this example `/var/opt/mssql` sets the path to the database files in the container.

All .NET Aspire container resources can utilize volume mounts, and some provide convenient APIs for adding named volumes derived from resources. Using the `WithDataVolume` as an example, the following code is functionally equivalent to the previous example but more succinct:
All .NET Aspire container resources can utilize volumes, and some provide convenient APIs for adding named volumes derived from resources. Using the <xref:Aspire.Hosting.SqlServerBuilderExtensions.WithDataVolume*> method as an example, the following code is functionally equivalent to the previous example but more succinct:

:::code language="csharp" source="snippets/volumes/VolumeMounts.AppHost/Program.Implicit.cs" id="implicit":::
:::code language="csharp" source="snippets/volumes/VolumeMounts.AppHost/Program.ImplicitVolume.cs" id="implicitvolume":::

With the app host project being named `VolumeMount.AppHost`, the `WithDataVolume` method automatically creates a named volume as `VolumeMount.AppHost-sql-data` and is mounted to the `/var/opt/mssql` path in the SQL Server container. The naming convention is as follows:

- `{appHostProjectName}-{resourceName}-data`: The volume name is derived from the app host project name and the resource name.

## Create a persistent password
## Use bind mounts

Bind mounts enable access to the data from both within the container and from processes on the host machine. For example, once a bind mount is established, you can copy a file into it on your host computer. The file is then available at the bound path within the container for your resource. With .NET Aspire, you configure a bind mount for each resource container using the <xref:Aspire.Hosting.ContainerResourceBuilderExtensions.WithBindMount*> method, which accepts three parameters:

- **`source`**: The path to the folder on the host machine to mount in the container.
- **`target`**: The target path in the container for the folder.
- **`isReadOnly`**: A Boolean flag that indicates whether the data in the bind mount can be changed. The default value is `false`.

Consider this code snippet, which uses the <xref:Aspire.Hosting.ContainerResourceBuilderExtensions.WithBindMount*> API to configure a bind mount for a SQL Server resource:

:::code language="csharp" source="snippets/volumes/VolumeMounts.AppHost/Program.WithBindMount.cs" id="bindmount":::

In this example:

- `source: @"C:\SqlServer\Data"` sets the folder on the host computer that will be bound.
- `target: "/var/opt/mssql"` sets the path to the database files in the container.

As for volumes, some .NET Aspire container resources provide convenient APIs for adding bind mounts. Using the <xref:Aspire.Hosting.SqlServerBuilderExtensions.WithDataBindMount*> method as an example, the following code is functionally equivalent to the previous example but more succinct:

:::code language="csharp" source="snippets/volumes/VolumeMounts.AppHost/Program.ImplicitBindMount.cs" id="implicitbindmount":::

## Create persistent passwords

Named volumes require a consistent password between app launches. .NET Aspire conveniently provides random password generation functionality. Consider the previous example once more, where a password is generated automatically:

:::code language="csharp" source="snippets/volumes/VolumeMounts.AppHost/Program.Implicit.cs" id="implicit":::
:::code language="csharp" source="snippets/volumes/VolumeMounts.AppHost/Program.ImplicitVolume.cs" id="implicitvolume":::

Since the `password` parameter isn't provided when calling `AddSqlServer`, .NET Aspire automatically generates a password for the SQL Server resource.

Expand All @@ -86,11 +120,11 @@ The same pattern applies to the other server-based resource types, such as those
| RabbitMQ | [📦 Aspire.Hosting.RabbitMq](https://www.nuget.org/packages/Aspire.Hosting.RabbitMq) | `rabbitmq` | `Parameters:rabbitmq-password` |
| SQL Server | [📦 Aspire.Hosting.SqlServer](https://www.nuget.org/packages/Aspire.Hosting.SqlServer) | `sql` | `Parameters:sql-password` |

By overriding the generated password, you can ensure that the password remains consistent between app launches, thus creating a persistent password. An alternative approach is to use the `AddParameter` method to create a parameter that can be used as a password. The following code demonstrates how to create a persistent password for a SQL Server resource:
By overriding the generated password, you can ensure that the password remains consistent between app launches. An alternative approach is to use the <xref:Aspire.Hosting.ParameterResourceBuilderExtensions.AddParameter*> method to create a parameter that can be used as a password. The following code demonstrates how to create a persistent password for a SQL Server resource:

:::code language="csharp" source="snippets/volumes/VolumeMounts.AppHost/Program.ExplicitStable.cs" id="explicit":::

The preceding code snippet demonstrates how to create a persistent password for a SQL Server resource. The `AddParameter` method is used to create a parameter named `sql-password` that's considered a secret. The `AddSqlServer` method is then called with the `password` parameter to set the password for the SQL Server resource. For more information, see [External parameters](external-parameters.md).
The `AddParameter` method is used to create a parameter named `sql-password` that's considered a secret. The `AddSqlServer` method is then called with the `password` parameter to set the password for the SQL Server resource. For more information, see [External parameters](external-parameters.md).

## Next steps

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
internal partial class Program
{
private static IResourceBuilder<SqlServerDatabaseResource> ImplicitBindMount(IDistributedApplicationBuilder builder)
{
// <implicitbindmount>
var sql = builder.AddSqlServer("sql")
.WithDataBindMount(source: @"C:\SqlServer\Data")
.AddDatabase("sqldb");
// </implicitbindmount>

return sql;
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
internal partial class Program
{
private static IResourceBuilder<SqlServerDatabaseResource> Implicit(IDistributedApplicationBuilder builder)
private static IResourceBuilder<SqlServerDatabaseResource> ImplicitVolume(IDistributedApplicationBuilder builder)
{
// <implicit>
// <implicitvolume>
var sql = builder.AddSqlServer("sql")
.WithDataVolume()
.AddDatabase("sqldb");
// </implicit>
// </implicitvolume>

return sql;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
internal partial class Program
{
private static IResourceBuilder<SqlServerDatabaseResource> WithDataMount(IDistributedApplicationBuilder builder)
private static IResourceBuilder<SqlServerDatabaseResource> WithBindMount(IDistributedApplicationBuilder builder)
{
// <mount>
// <bindmount>
var sql = builder.AddSqlServer("sql")
.WithBindMount("VolumeMount.AppHost-sql-data", "/var/opt/mssql")
.WithBindMount(source: @"C:\SqlServer\Data", target: "/var/opt/mssql")
.AddDatabase("sqldb");
// </mount>
// </bindmount>

return sql;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
internal partial class Program
{
private static IResourceBuilder<SqlServerDatabaseResource> WithVolume(IDistributedApplicationBuilder builder)
{
// <volume>
var sql = builder.AddSqlServer("sql")
.WithVolume(target: "/var/opt/mssql")
.AddDatabase("sqldb");
// </volume>

return sql;
}
}
Loading