Edit

Share via


Persist .NET Aspire project data using volumes or bind mounts

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.

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.

When to persist project data

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.

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.

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.

Use volumes

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 ContainerResourceBuilderExtensions.WithVolume method, which accepts three parameters:

  • 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 that's already defined the distributed app builder bits:

var builder = DistributedApplication.CreateBuilder(args);

// TODO:
//   Consider various code snippets for configuring 
//   volumes here and persistent passwords.

builder.Build().Run();

The first code snippet to consider uses the ContainerResourceBuilderExtensions.WithVolume 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:

var sql = builder.AddSqlServer("sql")
                 .WithVolume(target: "/var/opt/mssql")
                 .AddDatabase("sqldb");

In this example /var/opt/mssql sets the path to the database files in the container.

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

var sql = builder.AddSqlServer("sql")
                 .WithDataVolume()
                 .AddDatabase("sqldb");

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.

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 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 WithBindMount API to configure a bind mount for a SQL Server resource:

var sql = builder.AddSqlServer("sql")
                 .WithBindMount(source: @"C:\SqlServer\Data", target: "/var/opt/mssql")
                 .AddDatabase("sqldb");

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 WithDataBindMount method as an example, the following code is functionally equivalent to the previous example but more succinct:

var sql = builder.AddSqlServer("sql")
                 .WithDataBindMount(source: @"C:\SqlServer\Data")
                 .AddDatabase("sqldb");

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:

var sql = builder.AddSqlServer("sql")
                 .WithDataVolume()
                 .AddDatabase("sqldb");

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

Important

This isn't a persistent password! Instead, it changes every time the app host runs.

To create a persistent password, you must override the generated password. To do this, run the following command in your app host project directory to set a local password in your .NET user secrets:

dotnet user-secrets set Parameters:sql-password <password>

The naming convention for these secrets is important to understand. The password is stored in configuration with the Parameters:sql-password key. The naming convention follows this pattern:

  • Parameters:{resourceName}-password: In the case of the SQL Server resource (which was named "sql"), the password is stored in the configuration with the key Parameters:sql-password.

The same pattern applies to the other server-based resource types, such as those shown in the following table:

Resource type Hosting package Example resource name Override key
MySQL 📦 Aspire.Hosting.MySql mysql Parameters:mysql-password
Oracle 📦 Aspire.Hosting.Oracle oracle Parameters:oracle-password
PostgreSQL 📦 Aspire.Hosting.PostgreSQL postgresql Parameters:postgresql-password
RabbitMQ 📦 Aspire.Hosting.RabbitMq rabbitmq Parameters:rabbitmq-password
SQL Server 📦 Aspire.Hosting.SqlServer sql Parameters:sql-password

By overriding the generated password, you can ensure that the password remains consistent between app launches. 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:

var sqlPassword = builder.AddParameter("sql-password", secret: true);

var sql = builder.AddSqlServer("sql", password: sqlPassword)
                 .WithDataVolume()
                 .AddDatabase("sqldb");

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.

Next steps

You can apply the volume concepts in the preceding code to a variety of services, including seeding a database with data that will persist across app launches. Try combining these techniques with the resource implementations demonstrated in the following tutorials: