This is the full developer documentation for Aspire
# Compose distributed apps in code.
> Model, run, observe, and deploy distributed applications from one code-first control plane.
01 Polyglot by design
## Bring every service into one model.
Use the language and framework that fit each service. Aspire brings the whole stack into one model and one workflow.
* C#
* TypeScript
* JavaScript
* Python
* Java
* Go
* Rust
* Containers
[Explore application resources](/get-started/resources/)
02 Agent-ready
## Give AI the full application context.
Aspire supplies live context from your application model, running resources, and telemetry.
* ### AppHost
Services + dependencies
* ### Runtime
Resources + health
* ### Telemetry
Logs + traces + errors
[Set up Aspire for your agent](/get-started/ai-coding-agents/)
Aspire**AppHost**
Live application context
* GitHub Copilot
* Claude
* OpenAI
One connection. Full application context.
Works with any AI tool.
03 One application model
## From repository to running system.
The AppHost is the source of truth for how the application fits together across development, diagnostics, and deployment.
Built for your AI agent
**Context from Aspire**
Aspire gives connected AI tools the resource graph, references, configuration, and commands represented here.
********terminal**aspire-shop
```
$ aspire run
AppHost:apphost.mts
Dashboard:https://localhost:17178/login?t=b3f9c1e0
Logs:~/.aspire/cli/logs/apphost-2f8c.log
Press CTRL+C to stop the AppHost and exit.
```
Running aspire run turns the AppHost application model into a live resource topology, then exposes every healthy resource in the Aspire dashboard.
AppHostTopologyDashboard
```typescript
import { createBuilder } from "./.aspire/modules/aspire.mjs";
const builder = await createBuilder();
const postgres = await builder
.addPostgres("postgres")
.addDatabase("catalogdb");
const cache = await builder.addRedis("basketcache");
const catalog = await builder
.addNodeApp("catalogservice", "./catalog", "src/index.ts")
.withReference(postgres);
await builder.addViteApp("frontend", "./frontend")
.withReference(catalog)
.withReference(cache);
await builder.build().run();
```
```csharp
var builder = DistributedApplication.CreateBuilder(args);
var postgres = builder.AddPostgres("postgres")
.AddDatabase("catalogdb");
var cache = builder.AddRedis("basketcache");
var catalog = builder
.AddProject("catalogservice")
.WithReference(postgres);
builder.AddProject("frontend")
.WithReference(catalog)
.WithReference(cache);
builder.Build().Run();
```
Frontendfrontend
Servicecatalogservice
Databasepostgres
Cachebasketcache
service discoveryconnection stringcache client
NameStateSourceURLs
postgresdocker.io/library/postgres:18.3[tcp://localhost:5432]()
basketcachedocker.io/library/redis:8.6[tcp://localhost:6379]()
catalogserviceAspireShop.CatalogService.csproj[https://localhost:7099]()
frontendAspireShop.Frontend.csproj[https://localhost:7188]()
**0** of 4 resources running
1. 01
### Compose the app in code
Define services, containers, data stores, references, and configuration in a readable AppHost.
[Explore the AppHost](/get-started/app-host/)
2. 02
### Run the whole system
Start every dependency together, with service discovery, local endpoints, and dev-time configuration already connected.
[Learn about aspire run](/reference/cli/commands/aspire-run/)
3. 03
### Observe from the first request
Use built-in OpenTelemetry to move from a resource to its logs, traces, metrics, and health without extra setup.
[Explore the dashboard](/dashboard/)
4. 04
### Deploy with the same application model
Create deployment artifacts for cloud services, containers, Kubernetes, or the infrastructure path your team already uses.
[Explore deployment options](/deployment/)
04 Local first, production ready
## One model. Every environment.
Development, testing, and deployment stay connected to one application topology without forcing the architecture to drift.
Agents see every environment
**Context from Aspire**
Aspire exposes the selected environment's resources, endpoints, health, and available commands to connected AI tools.
01Local02Test03Production
********app topology**Local
Fast feedback
### Run everything locally.
Start projects, containers, and data services together. Aspire wires local endpoints, configuration, and service discovery from the AppHost.
`aspire run`**4 resources healthy
AppHost
**frontend**local process
**api**local process
**database**local container
**cache**local container
Repeatable validation
### Test the topology you ship.
Use the application model in automated tests and CI workflows so dependencies and relationships match the system developers run locally.
[Testing docs](/testing/overview/)**isolated test resources
AppHost
**frontend**dev tunnel
**api**staging environment
**database**staged database
**cache**short-lived container
Deployment ready
### Deploy the model you know.
Move the application to cloud targets without changing its architecture. Choose managed services, containers, Kubernetes, or your own infrastructure.
`aspire deploy`**release ready
AppHost
**frontend**Azure App ServiceAWS Amplify
**api**Azure Container AppsAWS App Runner
**database**Azure DatabaseAWS RDS
**cache**Azure Managed RedisAWS ElastiCache
05 Observability included
## See the whole application.
View OpenTelemetry logs, traces, and metrics locally. Follow requests across services with your team or coding agent.
Agents act on these signals
**Context from Aspire**
Aspire gives connected AI tools dashboard context, including logs, traces, metrics, health, and resource commands.
[Explore the Aspire dashboard](/dashboard/overview/)[Standalone dashboard](/dashboard/standalone/)[OpenTelemetry concepts](/fundamentals/telemetry/)[Debug with coding agents](/dashboard/ai-coding-agents/)
********Aspire dashboard**Resources / Graph







06 Extensible by default
## Use the services you already trust.
Add databases, caches, messaging, cloud resources, and community integrations through the same fluent application model.
[](/integrations/gallery/?search=redis)[](/integrations/gallery/?search=postgresql)[](/integrations/gallery/?search=docker)[](/integrations/gallery/?search=kubernetes)[](/integrations/gallery/?search=python)[](/integrations/gallery/?search=nodejs)[](/integrations/gallery/?search=rabbitmq)[](/integrations/gallery/?search=azure)[](/integrations/gallery/?search=aws)[](/integrations/gallery/?search=kafka)[](/integrations/gallery/?search=openai)[](/integrations/gallery/?search=activemq)
[](/integrations/gallery/?search=redis)[](/integrations/gallery/?search=postgresql)[](/integrations/gallery/?search=docker)[](/integrations/gallery/?search=kubernetes)[](/integrations/gallery/?search=python)[](/integrations/gallery/?search=nodejs)[](/integrations/gallery/?search=rabbitmq)[](/integrations/gallery/?search=azure)[](/integrations/gallery/?search=aws)[](/integrations/gallery/?search=kafka)[](/integrations/gallery/?search=openai)[](/integrations/gallery/?search=activemq)
[](/integrations/gallery/?search=mongodb)[](/integrations/gallery/?search=mysql)[](/integrations/gallery/?search=sqlite)[](/integrations/gallery/?search=nats)[](/integrations/gallery/?search=dapr)[](/integrations/gallery/?search=keycloak)[](/integrations/gallery/?search=clickhouse)[](/integrations/gallery/?search=meilisearch)[](/integrations/gallery/?search=elasticsearch)[](/integrations/gallery/?search=ollama)[](/integrations/gallery/?search=qdrant)[](/integrations/gallery/?search=milvus)
[](/integrations/gallery/?search=mongodb)[](/integrations/gallery/?search=mysql)[](/integrations/gallery/?search=sqlite)[](/integrations/gallery/?search=nats)[](/integrations/gallery/?search=dapr)[](/integrations/gallery/?search=keycloak)[](/integrations/gallery/?search=clickhouse)[](/integrations/gallery/?search=meilisearch)[](/integrations/gallery/?search=elasticsearch)[](/integrations/gallery/?search=ollama)[](/integrations/gallery/?search=qdrant)[](/integrations/gallery/?search=milvus)
[](/integrations/gallery/?search=nodejs)[](/integrations/gallery/?search=powershell)[](/integrations/gallery/?search=deno)[](/integrations/gallery/?search=golang%20gofeature)[](/integrations/gallery/?search=java)[](/integrations/gallery/?search=rust)[](/integrations/gallery/?search=typescript)[](/integrations/gallery/?search=cosmos)[](/integrations/gallery/?search=servicebus)[](/integrations/gallery/?search=foundry)[](/integrations/gallery/?search=seq)[](/integrations/gallery/?search=kurrentdb)
[](/integrations/gallery/?search=nodejs)[](/integrations/gallery/?search=powershell)[](/integrations/gallery/?search=deno)[](/integrations/gallery/?search=golang%20gofeature)[](/integrations/gallery/?search=java)[](/integrations/gallery/?search=rust)[](/integrations/gallery/?search=typescript)[](/integrations/gallery/?search=cosmos)[](/integrations/gallery/?search=servicebus)[](/integrations/gallery/?search=foundry)[](/integrations/gallery/?search=seq)[](/integrations/gallery/?search=kurrentdb)
[Browse every integration](/integrations/)
07 Developer momentum
## Keep teams moving forward.
Aspire shortens onboarding, clarifies distributed behavior, and keeps the local loop productive as systems grow.
“
> Aspire lets developers be developers again.
**[Steven Price](https://www.linkedin.com/in/steve-m-price/)**Software Engineering Manager, Iceland Foods
“
> I had someone start on a Monday morning and they were contributing code by lunch.
**[Russ Harding](https://www.linkedin.com/in/russharding1/)**VP Engineering, EQengineered
“
> I was surprised by how quickly Aspire got me from idea to running services.
**[Milan Jovanović](https://www.milanjovanovic.tech)**Educator & Content Creator, MJ Tech
“
> I've never wanted to commit to a Microsoft technology this much.
**[Nk54 (Reddit User)](https://www.reddit.com/user/Nk54)**
“
> Hit F5 to begin. Skip the setup boss fight and ship code faster.
**[Craig Taylor](https://www.linkedin.com/in/craig-taylor-2594895/)**Principal Architect, Xbox Live
“
> Aspire was easy to integrate with our existing container orchestration.
**[Sean Killeen](https://www.linkedin.com/in/seankilleen/)**VP Innovation, SCT Software
“
> OpenTelemetry out-of-the-box in the Aspire dashboard is a game changer for observability!
**[Dan Clarke](https://www.danclarke.com)**Developer & Podcaster, Everstack
[Join the Aspire community→](/community/)
08 Start with your stack
## Start building with less friction.
Install the Aspire CLI, start with an existing repository or a new one, and bring every resource into one development loop.
[Install Aspire](/get-started/install-cli/)[Build your first app](/get-started/first-app/)
[View on GitHub](https://github.com/microsoft/aspire)
### Find your next step.
Guides, working samples, and clear explanations to help you build your next Aspire app.
[Explore Aspire resources](/hub/)
# Page not found
404 / Not found
# Wrong route?
The page you're looking for isn't available. Explore the docs or retrace your steps to a familiar route.
[Go home](/)[Go back](/)

# Certificate configuration
> Configure HTTPS endpoints and certificate trust for Aspire resources to enable secure local development, container-to-container TLS, and trusted browser connections.
Aspire provides two complementary sets of certificate APIs:
1. **HTTPS endpoint APIs**: Configure the certificates that resources use for their own HTTPS endpoints (server authentication)
2. **Certificate trust APIs**: Configure which certificates resources trust when making outbound HTTPS connections (client authentication)
Both sets of APIs work together to enable secure HTTPS communication during local development. For example, a Vite frontend might use [`WithHttpsDeveloperCertificate`](/reference/api/csharp/aspire.hosting/resourcebuilderextensions/methods/#withhttpsdevelopercertificate "Indicates that a resource should use the developer certificate key pair for HTTPS endpoints at run time. Currently this indicates use of the ASP.NET Core...")[`withHttpsDeveloperCertificate`](/reference/api/typescript/aspire.hosting/withhttpsdevelopercertificate/ "Indicates that a resource should use the developer certificate key pair for HTTPS endpoints at run time. Currently this indicates use of the ASP.NET Core...") to serve HTTPS traffic, while also using [`WithDeveloperCertificateTrust`](/reference/api/csharp/aspire.hosting/resourcebuilderextensions/methods/#withdevelopercertificatetrust "Indicates whether developer certificates should be treated as trusted certificate authorities for the resource at run time. Currently this indicates trust...")[`withDeveloperCertificateTrust`](/reference/api/typescript/aspire.hosting/withdevelopercertificatetrust/ "Indicates whether developer certificates should be treated as trusted certificate authorities for the resource at run time. Currently this indicates trust...") to trust the dashboard’s OTLP endpoint certificate.
Caution
Certificate customization only applies at run time. Custom certificates aren’t included in publish or deployment artifacts.
## Why HTTPS matters
[Section titled “Why HTTPS matters”](#why-https-matters)
HTTPS is essential for protecting the security and privacy of data transmitted between services. It encrypts traffic to prevent eavesdropping, tampering, and man-in-the-middle attacks. For production environments, HTTPS is a fundamental security requirement.
However, enabling HTTPS during local development to match the production configuration presents unique challenges. Development environments typically use self-signed certificates that browsers and applications don’t trust by default. Managing these certificates across multiple services, containers, and different language runtimes can be complex and time-consuming, often creating friction in the development workflow.
Aspire simplifies HTTPS configuration for local development by providing APIs to:
* Configure HTTPS endpoints with appropriate certificates for server authentication
* Manage certificate trust so resources can communicate with services using self-signed certificates
* Automatically handle the development certificate (a per-user self-signed certificate valid only for local domains) across different resource types
## Trusting the development certificate
[Section titled “Trusting the development certificate”](#trusting-the-development-certificate)
Many of the certificate features in Aspire rely on a development certificate. Before using these features, you need to ensure that a trusted development certificate is installed on your machine.
### Using the Aspire CLI (recommended)
[Section titled “Using the Aspire CLI (recommended)”](#using-the-aspire-cli-recommended)
The preferred way to manage the development certificate is to use the [Aspire CLI](/get-started/install-cli/). When you run `aspire run` in an interactive session, the CLI automatically ensures the development certificate is created and trusted. No additional manual steps are required.
For non-C# AppHosts (such as [TypeScript](/app-host/typescript-apphost/) or Python AppHosts), the `dotnet` first-run experience that normally creates the HTTPS development certificate never runs, because these AppHosts launch a prebuilt native binary instead of invoking `dotnet`. The Aspire CLI fills this gap when `aspire run` starts and no development certificate exists:
* In an interactive session—and on Linux, where establishing trust doesn’t require a prompt—the CLI creates *and* trusts the certificate, just as it does for C# AppHosts.
* In a non-interactive session on macOS or Windows (for example, in CI), the CLI can’t show the macOS Keychain password prompt or the Windows trust dialog, so it *generates* the certificate without trusting it. This lets servers such as Kestrel load the certificate from the personal store, even though it isn’t trusted. If the certificate can’t be generated, a warning is displayed and the run continues.
To opt out of automatic certificate generation, set the `ASPIRE_CLI_GENERATE_HTTPS_CERTIFICATE` environment variable to `false`. This mirrors the .NET SDK’s `DOTNET_GENERATE_ASPNET_CERTIFICATE` opt-out:
* Bash
Disable automatic HTTPS certificate generation
```bash
ASPIRE_CLI_GENERATE_HTTPS_CERTIFICATE=false aspire run
```
* PowerShell
Disable automatic HTTPS certificate generation
```powershell
$env:ASPIRE_CLI_GENERATE_HTTPS_CERTIFICATE="false"; aspire run
```
You can also manage certificates explicitly with the Aspire CLI:
Trust the development certificate
```bash
aspire certs trust
```
Remove and re-trust (refresh)
```bash
aspire certs clean
aspire certs trust
```
Tip
If you encounter unexpected HTTPS or certificate trust errors during local development, running `aspire certs clean` followed by `aspire certs trust` is a good first troubleshooting step.
Linux certificate trust
On Linux, the development certificate is exported to `~/.aspnet/dev-certs/trust`, but applications using OpenSSL won’t discover it unless the `SSL_CERT_DIR` environment variable includes that path. You can set `SSL_CERT_DIR` in your shell profile (\~/.bashrc, \~/.zshrc, \~/.profile, etc.):
\~/.bashrc, \~/.zshrc, or \~/.profile
```bash
# Confirm /usr/lib/ssl/certs is the correct certificate directory for
# your Linux distribution. Common alternatives include:
# /etc/ssl/certs
# /etc/pki/tls/certs
if [ -z "$SSL_CERT_DIR" ]; then
export SSL_CERT_DIR="/usr/lib/ssl/certs:$HOME/.aspnet/dev-certs/trust"
else
export SSL_CERT_DIR="$SSL_CERT_DIR:$HOME/.aspnet/dev-certs/trust"
fi
```
You may need to reload your profile or start a new terminal session for the change to take effect.
Firefox certificate trust on Linux
On Linux, `aspire certs trust` and `aspire certs clean` also import and remove the development certificate from any Firefox and Chromium-family (Chrome, Chromium, Edge) NSS certificate databases discovered on the machine, so browser-based access to HTTPS endpoints secured by the development certificate works without a manual import.
Browser certificate trust depends on the `certutil` tool to update browser-specific NSS certificate databases. If certificate trust isn’t working in your browser after running `aspire certs trust`, run [`aspire doctor`](/reference/cli/commands/aspire-doctor/) to check for missing prerequisites, including `certutil`.
Firefox profiles are discovered under the legacy `~/.mozilla/firefox` directory when it exists; if that directory does not exist, the CLI falls back to `$XDG_CONFIG_HOME/mozilla/firefox` (or `~/.config/mozilla/firefox` when `XDG_CONFIG_HOME` isn’t set). Firefox Snap profiles are discovered separately under `~/snap/firefox/common/.mozilla/firefox`. Chromium-family databases are discovered under `~/.pki/nssdb` and the Chromium Snap package’s `~/snap/chromium/current/.pki/nssdb`.
To trust or clean specific NSS databases instead of relying on auto-discovery, configure `certificates.nssDbPaths` as a `:`-separated list of NSS database directories. Firefox and Chromium-family browsers require different trust settings; prefix a custom NSS database path with `firefox=` to apply the correct trust settings for Firefox-based browsers or prefix it with `chromium=` to apply Chromium-family trust settings. If no prefix is specified, Chromium-family trust settings are the default.
Override the NSS databases used for certificate trust
```bash
aspire config set --global certificates.nssDbPaths "firefox=/path/to/firefox-profile:chromium=/path/to/chromium-nssdb"
```
This setting takes precedence over the `DOTNET_DEV_CERTS_NSSDB_PATHS` environment variable used by the .NET SDK’s `dotnet dev-certs`. To stop using the custom paths and restore the environment variable or automatic discovery fallback, set the global value to an empty string:
Restore NSS database auto-discovery
```bash
aspire config set --global certificates.nssDbPaths ""
```
### Developer certificate for DCP communication
[Section titled “Developer certificate for DCP communication”](#developer-certificate-for-dcp-communication)
By default, Aspire uses the ASP.NET Core developer certificate to secure communication with its internal Developer Control Plane (DCP) server. This replaces the ephemeral localhost certificate that DCP would otherwise generate itself, and avoids certificate trust errors caused by that certificate not being in the system trust store.
If no trusted developer certificate is found, Aspire automatically falls back to DCP’s ephemeral certificate.
To opt out and use DCP’s default ephemeral certificate instead, set `ASPIRE_DCP_USE_DEVELOPER_CERTIFICATE` to `false` in your AppHost’s `launchSettings.json` or as an environment variable:
Properties/launchSettings.json
```json
{
"profiles": {
"https": {
"commandName": "Project",
"environmentVariables": {
"ASPIRE_DCP_USE_DEVELOPER_CERTIFICATE": "false"
}
}
}
}
```
Note
Prior to Aspire 13.4, this setting defaulted to `false` and was only supported on Windows. As of 13.4, it defaults to `true` and is supported on Windows, macOS, and Linux.
## HTTPS endpoint configuration
[Section titled “HTTPS endpoint configuration”](#https-endpoint-configuration)
HTTPS endpoint configuration determines which certificate a resource presents when serving HTTPS traffic. This is server-side certificate configuration for resources that host HTTPS/TLS endpoints.
### Default behavior
[Section titled “Default behavior”](#default-behavior)
For resources that have a certificate configuration defined with [`WithHttpsCertificateConfiguration`](/reference/api/csharp/aspire.hosting/resourcebuilderextensions/methods/#withhttpscertificateconfiguration "Adds a callback that allows configuring the resource to use a specific HTTPS/TLS certificate key pair for server authentication.")[`withHttpsCertificateConfiguration`](/reference/api/typescript/aspire.hosting/withhttpscertificateconfiguration/ "Adds a callback that allows configuring the resource to use a specific HTTPS/TLS certificate key pair for server authentication."), Aspire attempts to configure it to use the development certificate if available. This automatic configuration works for many common resource types including YARP, Redis, and Keycloak containers; Vite based JavaScript apps; and Python apps using Uvicorn.
You can control this behavior using the HTTPS endpoint APIs described below.
### Use the development certificate
[Section titled “Use the development certificate”](#use-the-development-certificate)
To explicitly configure a resource to use the development certificate for its HTTPS endpoints:
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
// Explicitly use the developer certificate
const nodeApp = await builder.addViteApp("frontend", "../frontend")
.withHttpsDeveloperCertificate();
// Use developer certificate with an encrypted private key
const certPassword = await builder.addParameter("cert-password", { secret: true });
const pythonApp = await builder.addUvicornApp("api", "../api", "app:main")
.withHttpsDeveloperCertificate({ password: certPassword });
await builder.build().run();
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
// Explicitly use the developer certificate
var nodeApp = builder.AddViteApp("frontend", "../frontend")
.WithHttpsDeveloperCertificate();
// Use developer certificate with an encrypted private key
var certPassword = builder.AddParameter("cert-password", secret: true);
var pythonApp = builder.AddUvicornApp("api", "../api", "app:main")
.WithHttpsDeveloperCertificate(certPassword);
builder.Build().Run();
```
The `WithHttpsDeveloperCertificate` method:
* Configures the resource to use the development certificate
* Only applies in run mode (local development)
* Optionally accepts a password parameter for encrypted certificate private keys
* Works with containers, Node.js, Python, and other resource types
### Use a custom certificate
[Section titled “Use a custom certificate”](#use-a-custom-certificate)
To configure a resource to use a specific X.509 certificate for HTTPS endpoints:
* TypeScript
apphost.mts
```typescript
import { createBuilder, refExpr } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const api = await builder.addContainer("api", {
image: "my-api",
tag: "latest",
});
api.createExecutionConfiguration()
.withArgumentsConfig()
.withEnvironmentVariablesConfig()
.withHttpsCertificateConfig(async () => ({
certificatePath: refExpr`/certs/tls.crt`,
keyPath: refExpr`/certs/tls.key`,
pfxPath: refExpr`/certs/tls.pfx`,
}));
await builder.build().run();
```
* C#
AppHost.cs
```csharp
using System.Security.Cryptography.X509Certificates;
var builder = DistributedApplication.CreateBuilder(args);
// Load your certificate
var certificate = new X509Certificate2("path/to/certificate.pfx", "password");
// Use the certificate for HTTPS endpoints
builder.AddContainer("api", "my-api:latest")
.WithHttpsCertificate(certificate);
// Use certificate with a password parameter
var certPassword = builder.AddParameter("cert-password", secret: true);
builder.AddNpmApp("frontend", "../frontend")
.WithHttpsCertificate(certificate, certPassword);
builder.Build().Run();
```
The certificate must:
* Include a private key
* Be a valid X.509 certificate
* Be appropriate for server authentication
### Disable HTTPS certificate configuration
[Section titled “Disable HTTPS certificate configuration”](#disable-https-certificate-configuration)
To prevent Aspire from configuring any HTTPS certificate for a resource:
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
// Disable automatic HTTPS certificate configuration
const redis = await builder.addRedis("cache")
.withoutHttpsCertificate();
await builder.build().run();
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
// Disable automatic HTTPS certificate configuration
var redis = builder.AddRedis("cache")
.WithoutHttpsCertificate();
builder.Build().Run();
```
Use [`WithoutHttpsCertificate`](/reference/api/csharp/aspire.hosting/resourcebuilderextensions/methods/#withouthttpscertificate "Disable HTTPS/TLS server certificate configuration for the resource. No HTTPS/TLS termination configuration will be applied.")[`withoutHttpsCertificate`](/reference/api/typescript/aspire.hosting/withouthttpscertificate/ "Disable HTTPS/TLS server certificate configuration for the resource. No HTTPS/TLS termination configuration will be applied.") when:
* The resource doesn’t support HTTPS
* You want to manually configure certificates
* The resource has its own certificate management
### Customize certificate configuration
[Section titled “Customize certificate configuration”](#customize-certificate-configuration)
For resources that need custom certificate configuration logic, use `WithHttpsCertificateConfiguration` to specify how certificate files should be passed to the resource:
* TypeScript
apphost.mts
```typescript
import { createBuilder, refExpr } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const api = await builder.addContainer("api", {
image: "myimage",
tag: "latest",
});
api.createExecutionConfiguration()
.withArgumentsConfig()
.withEnvironmentVariablesConfig()
.withCertificateTrustConfig(async () => ({
certificateBundlePath: refExpr`/certs/ca-bundle.crt`,
certificateDirectoriesPath: refExpr`/certs`,
rootCertificatesPath: "/etc/ssl/certs",
isContainer: true,
}));
await builder.build().run();
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
builder.AddContainer("api", "my-api:latest")
.WithHttpsCertificateConfiguration(ctx =>
{
// Pass certificate paths as command line arguments
ctx.Arguments.Add("--tls-cert");
ctx.Arguments.Add(ctx.CertificatePath);
ctx.Arguments.Add("--tls-key");
ctx.Arguments.Add(ctx.KeyPath);
// Or set environment variables
ctx.EnvironmentVariables["TLS_CERT_FILE"] = ctx.CertificatePath;
ctx.EnvironmentVariables["TLS_KEY_FILE"] = ctx.KeyPath;
// Use PFX format if the resource requires it
ctx.EnvironmentVariables["TLS_PFX_FILE"] = ctx.PfxPath;
// Include password if needed
if (ctx.Password is not null)
{
ctx.EnvironmentVariables["TLS_KEY_PASSWORD"] = ctx.Password;
}
return Task.CompletedTask;
});
builder.Build().Run();
```
The callback receives an `HttpsCertificateConfigurationCallbackAnnotationContext` that provides:
* `CertificatePath`: Path to the certificate file in PEM format
* `KeyPath`: Path to the private key file in PEM format
* `PfxPath`: Path to the certificate in PFX/PKCS#12 format
* `Password`: The password for the private key, if configured
* `Arguments`: Command line arguments list to modify
* `EnvironmentVariables`: Environment variables dictionary to modify
* `ExecutionContext`: The current execution context
* `Resource`: The resource being configured
Note
For .NET project resources with a TLS-enabled endpoint, `WithProjectDefaults` maps the available HTTPS certificate to Kestrel’s default PFX certificate environment variables (`Kestrel__Certificates__Default__Path` and `Kestrel__Certificates__Default__Password`). If an earlier `WithHttpsCertificateConfiguration` callback already set `Kestrel__Certificates__Default__Path`, `Kestrel__Certificates__Default__KeyPath`, or `Kestrel__Certificates__Default__Subject` (matched case-insensitively), the default PFX mapping is skipped entirely and the existing certificate configuration — including any password — is left untouched. This prevents Aspire from mixing an explicit PEM certificate/key pair with a replaced PFX path, which Kestrel can’t load.
## Certificate trust configuration
[Section titled “Certificate trust configuration”](#certificate-trust-configuration)
Certificate trust configuration determines which certificates a resource trusts when making outbound HTTPS connections. This is client-side certificate configuration.
### When to use certificate trust
[Section titled “When to use certificate trust”](#when-to-use-certificate-trust)
Certificate trust customization is valuable when:
* Resources need to trust the development certificate for local HTTPS communication
* Containerized services must communicate with the dashboard over HTTPS
* Python or Node.js applications need to trust custom certificate authorities
* You’re working with services that have specific certificate trust requirements
* Resources need to establish secure telemetry connections to the Aspire dashboard
### Development certificate trust
[Section titled “Development certificate trust”](#development-certificate-trust)
By default, Aspire attempts to add trust for the development certificate to resources that wouldn’t otherwise trust it. This enables resources to communicate with the dashboard OTLP collector endpoint over HTTPS and any other HTTPS endpoints secured by the development certificate.
You can control this behavior per resource using the `WithDeveloperCertificateTrust` API or through AppHost configuration settings.
#### Configure development certificate trust per resource
[Section titled “Configure development certificate trust per resource”](#configure-development-certificate-trust-per-resource)
To explicitly enable or disable development certificate trust for a specific resource:
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
// Explicitly enable development certificate trust
const nodeApp = await builder.addNodeApp("frontend", "../frontend", "index.js")
.withDeveloperCertificateTrust(true);
// Disable development certificate trust
const pythonApp = await builder.addPythonApp("api", "../api", "main.py")
.withDeveloperCertificateTrust(false);
await builder.build().run();
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
// Explicitly enable development certificate trust
var nodeApp = builder.AddNpmApp("frontend", "../frontend")
.WithDeveloperCertificateTrust(trust: true);
// Disable development certificate trust
var pythonApp = builder.AddPythonApp("api", "../api", "main.py")
.WithDeveloperCertificateTrust(trust: false);
builder.Build().Run();
```
### Certificate authority collections
[Section titled “Certificate authority collections”](#certificate-authority-collections)
Certificate authority collections allow you to bundle custom certificates and make them available to resources. You create a collection using the `AddCertificateAuthorityCollection` method and then reference it from resources that need to trust those certificates.
#### Create and use a certificate authority collection
[Section titled “Create and use a certificate authority collection”](#create-and-use-a-certificate-authority-collection)
AppHost.cs
```csharp
using System.Security.Cryptography.X509Certificates;
var builder = DistributedApplication.CreateBuilder(args);
// Load your custom certificates
var certificates = new X509Certificate2Collection();
certificates.ImportFromPemFile("path/to/certificate.pem");
// Create a certificate authority collection
var certBundle = builder.AddCertificateAuthorityCollection("my-bundle")
.WithCertificates(certificates);
// Apply the certificate bundle to resources
builder.AddNpmApp("my-project", "../myapp")
.WithCertificateAuthorityCollection(certBundle);
builder.Build().Run();
```
Note
This API is not yet available in TypeScript AppHosts.
In the preceding example, the certificate bundle is created with custom certificates and then applied to a Node.js application, enabling it to trust those certificates.
### Certificate trust scopes
[Section titled “Certificate trust scopes”](#certificate-trust-scopes)
Certificate trust scopes control how custom certificates interact with a resource’s default trusted certificates. Different scopes provide flexibility in managing certificate trust based on your application’s requirements.
The [`WithCertificateTrustScope`](/reference/api/csharp/aspire.hosting/resourcebuilderextensions/methods/#withcertificatetrustscope "Sets the Aspire.Hosting.ApplicationModel.CertificateTrustScope for custom certificate authorities associated with the resource. The scope specifies how...")[`withCertificateTrustScope`](/reference/api/typescript/aspire.hosting/withcertificatetrustscope/ "Sets the certificate trust scope") API accepts a `CertificateTrustScope` value to specify the trust behavior.
#### Available trust scopes
[Section titled “Available trust scopes”](#available-trust-scopes)
Aspire supports the following certificate trust scopes:
* **Append**: Appends custom certificates to the default trusted certificates
* **Override**: Replaces the default trusted certificates with only the configured certificates
* **System**: Combines custom certificates with system root certificates and uses them to override the defaults
* **None**: Disables all custom certificate trust configuration
#### Append mode
[Section titled “Append mode”](#append-mode)
Attempts to append the configured certificates to the default trusted certificates for a given resource. This mode is useful when you want to add trust for additional certificates while maintaining trust for the system’s default certificates.
This is the default scope for most resources. For Python resources, only OTEL trust configuration will be applied in this mode.
* TypeScript
apphost.mts
```typescript
import { createBuilder, CertificateTrustScope } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
await builder.addNodeApp("api", "../api", "index.js")
.withCertificateTrustScope(CertificateTrustScope.Append);
await builder.build().run();
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
builder.AddNodeApp("api", "../api")
.WithCertificateTrustScope(CertificateTrustScope.Append);
builder.Build().Run();
```
Note
Not all languages and runtimes support Append mode. For example, Python doesn’t natively support appending certificates to the default trust store.
Linux system trust preservation
On Linux, executable resources configured with Append mode preserve access to the system’s OpenSSL certificate roots in addition to Aspire’s generated development certificate. If `SSL_CERT_DIR` is set in the AppHost process environment, Aspire adds those directories after its generated certificate directory. An explicitly empty value is preserved by not inferring system directories. If `SSL_CERT_DIR` is unset, Aspire adds any well-known system certificate directories that exist on the machine (for example, `/etc/ssl/certs`) after its own. Aspire reads this value from the AppHost process environment, not from an individual resource’s configured environment. This avoids a scenario where Linux workloads launched with `dotnet run` or an IDE lose OpenSSL’s implicit system certificate roots and fail outbound HTTPS requests once Aspire configures the resource with only its generated certificate directory.
#### Override mode
[Section titled “Override mode”](#override-mode)
Attempts to override a resource to only trust the configured certificates, replacing the default trusted certificates entirely. This mode is useful when you need strict control over which certificates are trusted.
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
var certBundle = builder.AddCertificateAuthorityCollection("custom-certs")
.WithCertificates(myCertificates);
builder.AddPythonModule("api", "./api", "uvicorn")
.WithCertificateAuthorityCollection(certBundle)
.WithCertificateTrustScope(CertificateTrustScope.Override);
builder.Build().Run();
```
Note
This API is not yet available in TypeScript AppHosts.
#### System mode
[Section titled “System mode”](#system-mode)
Attempts to combine the configured certificates with the default system root certificates and use them to override the default trusted certificates for a resource. This mode is intended to support Python and similar runtimes that don’t work well with Append mode.
This is the default scope for Python projects because Python only has mechanisms to fully override certificate trust.
* TypeScript
apphost.mts
```typescript
import { createBuilder, CertificateTrustScope } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
await builder.addPythonApp("worker", "../worker", "main.py")
.withCertificateTrustScope(CertificateTrustScope.System);
await builder.build().run();
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
builder.AddPythonApp("worker", "../worker", "main.py")
.WithCertificateTrustScope(CertificateTrustScope.System);
builder.Build().Run();
```
#### None mode
[Section titled “None mode”](#none-mode)
Disables all custom certificate trust for the resource, causing it to rely solely on its default certificate trust behavior.
This is the default scope for .NET projects on Windows, as there’s no way to automatically change the default system store source.
* TypeScript
apphost.mts
```typescript
import { createBuilder, CertificateTrustScope } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
await builder.addContainer("service", { image: "myimage", tag: "latest" })
.withCertificateTrustScope(CertificateTrustScope.None);
await builder.build().run();
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
builder.AddContainer("service", "myimage")
.WithCertificateTrustScope(CertificateTrustScope.None);
builder.Build().Run();
```
### Custom certificate trust configuration
[Section titled “Custom certificate trust configuration”](#custom-certificate-trust-configuration)
For advanced scenarios, you can specify custom certificate trust behavior using a callback API. This callback allows you to customize the command line arguments and environment variables required to configure certificate trust for different resource types.
#### Configure certificate trust with a callback
[Section titled “Configure certificate trust with a callback”](#configure-certificate-trust-with-a-callback)
Use `WithCertificateTrustConfiguration` to customize how certificate trust is configured for a resource:
* TypeScript
Note
This API is not yet available in TypeScript AppHosts.
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
builder.AddContainer("api", "myimage")
.WithCertificateTrustConfiguration(ctx =>
{
// Add a command line argument
ctx.Arguments.Add("--use-system-ca");
// Set environment variables with certificate paths
// CertificateBundlePath resolves to the path of the custom certificate bundle file
ctx.EnvironmentVariables["MY_CUSTOM_CERT_VAR"] = ctx.CertificateBundlePath;
// CertificateDirectoriesPath resolves to paths containing individual certificates
ctx.EnvironmentVariables["CERTS_DIR"] = ctx.CertificateDirectoriesPath;
return Task.CompletedTask;
});
builder.Build().Run();
```
The callback receives a `CertificateTrustConfigurationCallbackAnnotationContext` that provides:
* `Scope`: The `CertificateTrustScope` for the resource.
* `Arguments`: Command line arguments for the resource. Values can be strings or path providers like `CertificateBundlePath` or `CertificateDirectoriesPath`.
* `EnvironmentVariables`: Environment variables for configuring certificate trust. The dictionary key is the environment variable name; values can be strings or path providers. By default, includes `SSL_CERT_DIR` and may include `SSL_CERT_FILE` if Override or System scope is configured.
* `CertificateBundlePath`: A value provider that resolves to the path of a custom certificate bundle file.
* `CertificateDirectoriesPath`: A value provider that resolves to paths containing individual certificates.
Default implementations are provided for Node.js, Python, and container resources. Container resources rely on standard OpenSSL configuration options, with default values that support the majority of common Linux distributions.
#### Configure container certificate paths
[Section titled “Configure container certificate paths”](#configure-container-certificate-paths)
For container resources, you can customize where certificates are stored and accessed using [`WithContainerCertificatePaths`](/reference/api/csharp/aspire.hosting/containerresourcebuilderextensions/methods/#withcontainercertificatepaths "Adds a Aspire.Hosting.ApplicationModel.ContainerCertificatePathsAnnotation to the resource that allows overriding the default paths in the container used for...")[`withContainerCertificatePaths`](/reference/api/typescript/aspire.hosting/withcontainercertificatepaths/ "Adds container certificate path overrides used for certificate trust at run time."):
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
builder.AddContainer("api", "myimage")
.WithContainerCertificatePaths(
customCertificatesDestination: "/custom/certs/path",
defaultCertificateBundlePaths: ["/etc/ssl/certs/ca-certificates.crt"],
defaultCertificateDirectoryPaths: ["/etc/ssl/certs"]);
builder.Build().Run();
```
Note
This API is not yet available in TypeScript AppHosts.
The `WithContainerCertificatePaths` API accepts three optional parameters:
* `customCertificatesDestination`: Overrides the base path in the container where custom certificate files are placed. If not set or set to `null`, the default path of `/usr/lib/ssl/aspire` is used.
* `defaultCertificateBundlePaths`: Overrides the path(s) in the container where a default certificate authority bundle file is located. When the `CertificateTrustScope` is Override or System, the custom certificate bundle is additionally written to these paths. If not set or set to `null`, a set of default certificate paths for common Linux distributions is used.
* `defaultCertificateDirectoryPaths`: Overrides the path(s) in the container where individual trusted certificate files are found. When the `CertificateTrustScope` is Append, these paths are concatenated with the path to the uploaded certificate artifacts. If not set or set to `null`, a set of default certificate paths for common Linux distributions is used.
Note
All desired paths must be configured in a single call to `WithContainerCertificatePaths` as only the most recent call to the API is honored.
## Common scenarios
[Section titled “Common scenarios”](#common-scenarios)
This section demonstrates common patterns for configuring HTTPS endpoints and certificate trust together.
### Configure a service with HTTPS and enable dashboard telemetry
[Section titled “Configure a service with HTTPS and enable dashboard telemetry”](#configure-a-service-with-https-and-enable-dashboard-telemetry)
A typical scenario is configuring a Node.js service to serve HTTPS traffic while also enabling it to send telemetry to the dashboard:
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
// Configure the service to use developer certificate for HTTPS endpoints
// and trust the developer certificate for outbound connections (like dashboard telemetry)
const frontend = await builder.addNodeApp("frontend", "../frontend", "index.js")
.withHttpsDeveloperCertificate() // Server cert for HTTPS endpoints
.withDeveloperCertificateTrust(true); // Client trust for dashboard
await builder.build().run();
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
// Configure the service to use developer certificate for HTTPS endpoints
// and trust the developer certificate for outbound connections (like dashboard telemetry)
var frontend = builder.AddNpmApp("frontend", "../frontend")
.WithHttpsDeveloperCertificate() // Server cert for HTTPS endpoints
.WithDeveloperCertificateTrust(true); // Client trust for dashboard
builder.Build().Run();
```
### Enable HTTPS with custom certificates
[Section titled “Enable HTTPS with custom certificates”](#enable-https-with-custom-certificates)
When working with corporate or custom CA certificates, you can configure both server and client certificates:
AppHost.cs
```csharp
using System.Security.Cryptography.X509Certificates;
var builder = DistributedApplication.CreateBuilder(args);
// Load custom certificates
var serverCert = new X509Certificate2("server-cert.pfx", "password");
var customCA = new X509Certificate2Collection();
customCA.Import("corporate-ca.pem");
var caBundle = builder.AddCertificateAuthorityCollection("corporate-certs")
.WithCertificates(customCA);
// Configure service with custom server cert and CA trust
builder.AddContainer("api", "my-api:latest")
.WithHttpsCertificate(serverCert) // Server cert for HTTPS
.WithCertificateAuthorityCollection(caBundle); // Trust corporate CA
builder.Build().Run();
```
Note
This API is not yet available in TypeScript AppHosts.
### Configure Redis with TLS
[Section titled “Configure Redis with TLS”](#configure-redis-with-tls)
Redis resources can be configured to use HTTPS (TLS) for secure connections:
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
// Configure Redis to use the developer certificate for TLS
const redis = await builder.addRedis("cache")
.withHttpsDeveloperCertificate();
// Or disable TLS entirely
const redisNoTls = await builder.addRedis("cache-notls")
.withoutHttpsCertificate();
await builder.build().run();
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
// Configure Redis to use the developer certificate for TLS
var redis = builder.AddRedis("cache")
.WithHttpsDeveloperCertificate();
// Or disable TLS entirely
var redisNoTls = builder.AddRedis("cache-notls")
.WithoutHttpsCertificate();
builder.Build().Run();
```
### Disable certificate configuration for specific resources
[Section titled “Disable certificate configuration for specific resources”](#disable-certificate-configuration-for-specific-resources)
To disable both HTTPS endpoint configuration and certificate trust for a resource that manages its own certificates:
* TypeScript
apphost.mts
```typescript
import { createBuilder, CertificateTrustScope } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
// Disable all automatic certificate configuration
await builder.addPythonModule("api", "./api", "uvicorn")
.withoutHttpsCertificate() // No server cert config
.withCertificateTrustScope(CertificateTrustScope.None); // No client trust config
await builder.build().run();
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
// Disable all automatic certificate configuration
builder.AddPythonModule("api", "./api", "uvicorn")
.WithoutHttpsCertificate() // No server cert config
.WithCertificateTrustScope(CertificateTrustScope.None); // No client trust config
builder.Build().Run();
```
## Limitations
[Section titled “Limitations”](#limitations)
Certificate configuration has the following limitations:
* Currently supported only in run mode, not in publish mode
* Not all languages and runtimes support all trust scope modes
* Python applications don’t natively support Append mode for certificate trust
* Custom certificate configuration requires appropriate runtime support within the resource
* HTTPS endpoint APIs are marked as experimental (`ASPIRECERTIFICATES001`)
# AppHost configuration
> Configure the Aspire AppHost — environment variables, launch profiles, network ports, container runtime selection, and the options that change orchestration behavior.
The AppHost project configures and starts your distributed application. Configuration includes settings for the resource service, the [Aspire dashboard](/dashboard/overview/), and internal settings used by integrations.
AppHost configuration is provided through launch profiles:
Select your programming language
TypeScriptC#
C# AppHosts come in two forms, and each stores launch profiles differently:
* **Project-based AppHost** (the default `dotnet new aspire-apphost` template): profiles live in `Properties/launchSettings.json`.
* **File-based AppHost** (created with `aspire new` using the empty C# template): profiles live in `apphost.run.json`. In this template, `aspire.config.json` only points at the entry file — it does **not** contain a `profiles` block.
Project-based AppHost — Properties/launchSettings.json
```json
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:17134;http://localhost:15170",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21030",
"ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22057"
}
}
}
}
```
File-based AppHost — apphost.run.json
```json
{
"profiles": {
"https": {
"applicationUrl": "https://localhost:17134;http://localhost:15170",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21030",
"ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22057"
}
}
}
}
```
File-based AppHost — aspire.config.json
```json
{
"appHost": {
"path": "apphost.cs"
}
}
```
Note
`aspire run`, `dotnet run apphost.cs`, and C# Dev Kit all read `apphost.run.json` for launch profiles when it is present. In the `aspire new` empty C# template, `aspire.config.json` intentionally omits `profiles` to avoid duplicating that data.
In TypeScript AppHosts, profiles live in `aspire.config.json`:
aspire.config.json
```json
{
"appHost": {
"path": "apphost.mts",
"language": "typescript/nodejs"
},
"profiles": {
"https": {
"applicationUrl": "https://localhost:17134;http://localhost:15170",
"environmentVariables": {
"ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21030",
"ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22057"
}
}
}
}
```
Note
Configuration described on this page is for the Aspire AppHost project. To configure the standalone dashboard, see [dashboard configuration](/dashboard/configuration/).
## Common configuration
[Section titled “Common configuration”](#common-configuration)
| Option | Default value | Description |
| -------------------------------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ASPIRE_ALLOW_UNSECURED_TRANSPORT` | `false` | Allows communication with the AppHost without https. `ASPNETCORE_URLS` (dashboard address) and `ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL` (AppHost resource service address) must be secured with HTTPS unless true. |
| `ASPIRE_CONTAINER_RUNTIME` | `docker` | Allows the user of alternative container runtimes for resources backed by containers. Possible values are `docker` (default) or `podman`. |
| `ASPIRE_DCP_USE_DEVELOPER_CERTIFICATE` | `true` | When `true` (the default), Aspire uses the ASP.NET Core developer certificate to secure the internal DCP server instead of an ephemeral certificate generated by DCP. On Windows, Aspire passes the certificate thumbprint to DCP. On macOS and Linux, Aspire passes the certificate and private key file paths (plus the thumbprint) so DCP can verify the loaded certificate. Set to `false` to opt out and use DCP’s default ephemeral certificate. If no trusted developer certificate is found, Aspire automatically falls back to the ephemeral certificate. For more information, see [Certificate configuration](/app-host/certificate-configuration/). |
| `ASPIRE_ENVIRONMENT` | `null` | Configures the AppHost environment when no higher-priority environment source is set. If no environment is configured, the AppHost uses `Production`. |
| `ASPIRE_VERSION_CHECK_DISABLED` | `false` | When set to `true`, Aspire doesn’t check for newer versions on startup. |
## AppHost environment
[Section titled “AppHost environment”](#apphost-environment)
Use `ASPIRE_ENVIRONMENT` to set the environment name used by the AppHost while it evaluates the application model. Precedence is `--environment`, `DOTNET_ENVIRONMENT`, `ASPIRE_ENVIRONMENT`, then `Production`. This doesn’t configure the dashboard’s `ASPNETCORE_ENVIRONMENT` or automatically flow to child resources; set framework-specific variables on resources as needed. For details, see [Aspire environments](/deployment/environments/).
## Version update notifications
[Section titled “Version update notifications”](#version-update-notifications)
When an Aspire app starts, it checks if a newer version of Aspire is available on NuGet. If a new version is found, a notification appears in the dashboard with the latest version number, [a link to upgrade instructions](https://aka.ms/dotnet/aspire/update-latest), and button to ignore that version in the future.

The version check runs only when:
* The dashboard is enabled (interaction service is available).
* At least 2 days have passed since the last check.
* The check hasn’t been disabled via the `ASPIRE_VERSION_CHECK_DISABLED` configuration setting.
* The app is not running in publish mode.
Updates are manual. You need to edit your project file to upgrade the Aspire SDK and package versions.
## Resource service
[Section titled “Resource service”](#resource-service)
A resource service is hosted by the AppHost. The resource service is used by the dashboard to fetch information about resources which are being orchestrated by Aspire.
| Option | Default value | Description |
| ----------------------------------------- | ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL` | `null` | Configures the address of the resource service hosted by the AppHost. Automatically generated with *launchSettings.json* to have a random port on localhost. For example, `https://localhost:17037`. |
| `ASPIRE_DASHBOARD_RESOURCESERVICE_APIKEY` | Automatically generated 128-bit entropy token. | The API key used to authenticate requests made to the AppHost’s resource service. The API key is required if the AppHost is in run mode, the dashboard isn’t disabled, and the dashboard isn’t configured to allow anonymous access with `ASPIRE_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS`. |
## Dashboard
[Section titled “Dashboard”](#dashboard)
By default, the dashboard is automatically started by the AppHost. The dashboard supports [its own set of configuration](/dashboard/configuration/), and some settings can be configured from the AppHost.
| Option | Default value | Description |
| -------------------------------------------- | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ASPNETCORE_URLS` | `null` | Dashboard address. Must be `https` unless `ASPIRE_ALLOW_UNSECURED_TRANSPORT` or `DistributedApplicationOptions.AllowUnsecuredTransport` is true. Automatically generated with *launchSettings.json* to have a random port on localhost. The value in launch settings is set on the `applicationUrls` property. |
| `ASPNETCORE_ENVIRONMENT` | `Production` | Configures the environment the dashboard runs as. For more information, see [Use multiple environments in ASP.NET Core](https://learn.microsoft.com/aspnet/core/fundamentals/environments). |
| `ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL` | `http://localhost:18889` if no gRPC endpoint is configured. | Configures the dashboard OTLP gRPC address. Used by the dashboard to receive telemetry over OTLP. Set on resources as the `OTEL_EXPORTER_OTLP_ENDPOINT` env var. The `OTEL_EXPORTER_OTLP_PROTOCOL` env var is `grpc`. Automatically generated with *launchSettings.json* to have a random port on localhost. |
| `ASPIRE_DASHBOARD_OTLP_HTTP_ENDPOINT_URL` | `null` | Configures the dashboard OTLP HTTP address. Used by the dashboard to receive telemetry over OTLP. If only `ASPIRE_DASHBOARD_OTLP_HTTP_ENDPOINT_URL` is configured then it is set on resources as the `OTEL_EXPORTER_OTLP_ENDPOINT` env var. The `OTEL_EXPORTER_OTLP_PROTOCOL` env var is `http/protobuf`. |
| `ASPIRE_DASHBOARD_CORS_ALLOWED_ORIGINS` | `null` | Overrides the CORS allowed origins configured in the dashboard. This setting replaces the default behavior of calculating allowed origins based on resource endpoints. |
| `ASPIRE_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS` | `false` | Configures the dashboard to not use authentication and accept anonymous access. Sets frontend, OTLP, MCP, and API auth modes to `Unsecured`. See [Dashboard security considerations](/dashboard/security-considerations/#anonymous-access) for the security implications. |
| `ASPIRE_DASHBOARD_FRONTEND_BROWSERTOKEN` | Automatically generated 128-bit entropy token. | Configures the frontend browser token. This is the value that must be entered to access the dashboard when the auth mode is BrowserToken. If no browser token is specified then a new token is generated each time the AppHost is launched. |
| `ASPIRE_DASHBOARD_TELEMETRY_OPTOUT` | `false` | Configures the dashboard to never send [usage telemetry](/dashboard/microsoft-collected-dashboard-telemetry/). |
| `ASPIRE_DASHBOARD_API_ENABLED` | `true` | Enables the dashboard [telemetry API](/dashboard/configuration/#api) (`/api/telemetry/*`) endpoints. The AppHost always sets this to `true`. |
| `ASPIRE_DASHBOARD_FORWARDEDHEADERS_ENABLED` | `false` | Enables the Forwarded headers middleware that replaces the scheme and host values on the Request context with the values coming from the `X-Forwarded-Proto` and `X-Forwarded-Host` headers. |
| `ASPIRE_DASHBOARD_PERSISTENCE_MODE` | `Run` | Configures [dashboard data persistence](/dashboard/data-persistence/). Valid values are `None`, `Run`, and `Resume`. The AppHost automatically sets this value to `Run`. |
| `ASPIRE_DASHBOARD_DATA_DIRECTORY` | `/dashboard` | Configures the root directory for persistent dashboard data. The directory must be writable by the account running the dashboard and have restrictive file-system permissions because it can contain sensitive data. |
## Internal
[Section titled “Internal”](#internal)
Internal settings are used by the AppHost and integrations. Internal settings aren’t designed to be configured directly.
| Option | Default value | Description |
| ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `AppHost:Directory` | The content root if there’s no project. | Directory of the project where the AppHost is located. Accessible from the `IDistributedApplicationBuilder.AppHostDirectory`. |
| `AppHost:Path` | The directory combined with the application name. | The path to the AppHost. It combines the directory with the application name. |
| `AppHost:Sha256` | It is created from the AppHost name when the AppHost is in publish mode. Otherwise it is created from the AppHost path. | Hex encoded hash for the current application. The hash is based on the location of the app on the current machine so it is stable between launches of the AppHost. |
| `AppHost:OtlpApiKey` | Automatically generated 128-bit entropy token. | The API key used to authenticate requests sent to the dashboard OTLP service. The value is present if needed: the AppHost is in run mode, the dashboard isn’t disabled, and the dashboard isn’t configured to allow anonymous access with `ASPIRE_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS`. |
| `AppHost:DashboardApiKey` | Automatically generated 128-bit entropy token. | The API key used to authenticate requests to the dashboard telemetry API. Also used as a fallback for MCP authentication if `AppHost:McpApiKey` is not set. The value is present if needed: the AppHost is in run mode, the dashboard isn’t disabled, and the dashboard isn’t configured to allow anonymous access. |
| `AppHost:BrowserToken` | Automatically generated 128-bit entropy token. | The browser token used to authenticate browsing to the dashboard when it is launched by the AppHost. The browser token can be set by `ASPIRE_DASHBOARD_FRONTEND_BROWSERTOKEN`. The value is present if needed: the AppHost is in run mode, the dashboard isn’t disabled, and the dashboard isn’t configured to allow anonymous access with `ASPIRE_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS`. |
| `AppHost:ResourceService:AuthMode` | `ApiKey`. If `ASPIRE_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS` is true then the value is `Unsecured`. | The authentication mode used to access the resource service. The value is present if needed: the AppHost is in run mode and the dashboard isn’t disabled. |
| `AppHost:ResourceService:ApiKey` | Automatically generated 128-bit entropy token. | The API key used to authenticate requests made to the AppHost’s resource service. The API key can be set by `ASPIRE_DASHBOARD_RESOURCESERVICE_APIKEY`. The value is present if needed: the AppHost is in run mode, the dashboard isn’t disabled, and the dashboard isn’t configured to allow anonymous access with `ASPIRE_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS`. |
## Advanced orchestration properties
[Section titled “Advanced orchestration properties”](#advanced-orchestration-properties)
Caution
The following MSBuild properties and command-line arguments are intended for internal use by the Aspire SDK and tooling. You should not need to set these manually under normal circumstances. Incorrectly configuring these values can break your AppHost.
### DcpCliPath
[Section titled “DcpCliPath”](#dcpclipath)
The `DcpCliPath` property specifies the path to the **Developer Control Plane (DCP)** executable. The DCP is the core orchestration engine that Aspire uses to run and manage distributed application resources locally during development.
#### How it works
[Section titled “How it works”](#how-it-works)
When you use the [Aspire SDK](/get-started/aspire-sdk/), the build system automatically:
1. Imports the platform-specific `Aspire.Hosting.Orchestration` NuGet package (for example, `Aspire.Hosting.Orchestration.win-x64`).
2. Sets `DcpCliPath` to point to the `dcp` executable within that package.
3. Embeds this path as assembly metadata in your compiled AppHost.
At runtime, the AppHost reads this metadata to locate and start the DCP process, which then orchestrates your application’s resources.
#### Override options
[Section titled “Override options”](#override-options)
In rare cases, you may need to override the default DCP path:
| Method | Example |
| --------------------- | --------------------------------------------- |
| MSBuild property | `C:\path\to\dcp.exe` |
| Command-line argument | `--dcp-cli-path /path/to/dcp` |
| Configuration | `DcpPublisher:CliPath` |
#### When to use
[Section titled “When to use”](#when-to-use)
You might override `DcpCliPath` in these scenarios:
* **Aspire contributors**: Testing with a custom or debug build of DCP when developing Aspire itself.
* **CI/CD pipelines**: Non-standard SDK layouts where automatic discovery doesn’t work.
* **Troubleshooting**: Temporarily pointing to a specific DCP version to diagnose issues.
Tip
If your AppHost fails to start with an error about missing DCP or orchestration dependencies, ensure you have the [Aspire SDK](/get-started/aspire-sdk/) properly configured rather than manually setting `DcpCliPath`.
# Console and worker apps
> Learn how to integrate console applications and background worker services into Aspire, including service discovery, resource references, and interactive console patterns.
Console applications and background workers can participate in an Aspire application regardless of the language they’re written in. Add them to your AppHost to manage their lifecycle alongside your other resources, provide connection strings and service endpoints, and view their console output in the dashboard.
## Adding a console app to the AppHost
[Section titled “Adding a console app to the AppHost”](#adding-a-console-app-to-the-apphost)
Choose the resource type that matches how your app runs. Use an executable resource for a command-line program, a Python app resource for a script or module, or a JavaScript app resource for a Node.js app. For a .NET console app or Worker Service project, use a project resource. None of these apps needs to expose an HTTP endpoint to be orchestrated by Aspire.
For more information on executable, Python, and JavaScript resources, see [Executable resources](/app-host/executable-resources/), [Python integration](/integrations/frameworks/python/), and [JavaScript integration](/integrations/frameworks/javascript/).
The following examples show how to add a .NET or Node.js console app, connect it to other resources, and run it as a background worker. The examples use three sets of tabs, which you choose independently:
* **Aspire CLI**, **apphost.cs (C# file-based app)**, and **PackageReference(\*.csproj)** tabs select an installation method for Aspire integrations. If you’re using a TypeScript AppHost, only the **Aspire CLI** tab is applicable.
* **.NET** and **Node.js** tabs select the language of the console app or executable that the AppHost orchestrates.
* **C#** and **TypeScript** tabs select the language of the AppHost itself.
For example, to orchestrate a Node.js console app from a C# AppHost, choose any installation method, then **Node.js**, and then **C#**. Your selections stay the same as you move through the page.
These examples assume an existing AppHost.
* **.NET:** The examples also assume a shared service defaults project. Create the named console, worker, and API projects before referencing them. In a C# AppHost, add project references so the corresponding `Projects.*` types are generated; in a TypeScript AppHost, use each project’s actual path.
* **Node.js:** Create each app folder with a *package.json* file before referencing it. The AppHost resolves the app folder path relative to the AppHost directory, and the script path relative to the app folder.
For the Redis examples, install the hosting integration in your AppHost. Running Redis also requires a supported [container runtime](/get-started/prerequisites/).
* TypeScript
* Aspire CLI
Aspire CLI — Add Aspire.Hosting.Redis package
```bash
aspire add redis
```
The Aspire CLI is interactive, be sure to select the appropriate search result when prompted:
Aspire CLI — Example output prompt
```bash
Select an integration to add:
> redis (Aspire.Hosting.Redis)
> Other results listed as selectable options...
```
Note
If there's only one result, the CLI selects it automatically. You'll still need to confirm the version.
* C#
Aspire CLI — Add Aspire.Hosting.Redis package
```bash
aspire add redis
```
The Aspire CLI is interactive, be sure to select the appropriate search result when prompted:
Aspire CLI — Example output prompt
```bash
Select an integration to add:
> redis (Aspire.Hosting.Redis)
> Other results listed as selectable options...
```
Note
If there's only one result, the CLI selects it automatically. You'll still need to confirm the version.
* Aspire CLI
* Aspire CLI
Aspire CLI — Add Aspire.Hosting.Redis package
```bash
aspire add redis
```
The Aspire CLI is interactive, be sure to select the appropriate search result when prompted:
Aspire CLI — Example output prompt
```bash
Select an integration to add:
> redis (Aspire.Hosting.Redis)
> Other results listed as selectable options...
```
Note
If there's only one result, the CLI selects it automatically. You'll still need to confirm the version.
* apphost.cs (C# file-based app)
C# — AppHost.cs
```csharp
#:package Aspire.Hosting.Redis@*
```
* PackageReference (\*.csproj)
XML — Add Aspire.Hosting.Redis package reference
```xml
```
* Aspire CLI
Aspire CLI — Add Aspire.Hosting.Redis package
```bash
aspire add redis
```
The Aspire CLI is interactive, be sure to select the appropriate search result when prompted:
Aspire CLI — Example output prompt
```bash
Select an integration to add:
> redis (Aspire.Hosting.Redis)
> Other results listed as selectable options...
```
Note
If there's only one result, the CLI selects it automatically. You'll still need to confirm the version.
* apphost.cs (C# file-based app)
C# — AppHost.cs
```csharp
#:package Aspire.Hosting.Redis@*
```
* PackageReference (\*.csproj)
XML — Add Aspire.Hosting.Redis package reference
```xml
```
- .NET
Add the console app project to your AppHost:
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const cache = await builder.addRedis('cache');
// Add a console app project to the AppHost
await builder
.addProject('processor', '../MyConsoleApp/MyConsoleApp.csproj')
.withReference(cache);
await builder.build().run();
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
var cache = builder.AddRedis("cache");
// Add a console app project to the AppHost
var processor = builder.AddProject("processor")
.WithReference(cache);
builder.Build().Run();
```
The `processor` project receives the `ConnectionStrings__cache` environment variable at runtime, just like any other project type.
Note
The console app project should target a .NET version supported by Aspire (.NET 8 or later) and reference the service defaults project to benefit from telemetry and service discovery. For more information, see [Service defaults](/get-started/csharp-service-defaults/).
- Node.js
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const cache = await builder.addRedis('cache');
// Add a console app project to the AppHost
await builder
.addProject('processor', '../MyConsoleApp/MyConsoleApp.csproj')
.withReference(cache);
await builder.build().run();
```
- TypeScript
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
var cache = builder.AddRedis("cache");
// Add a console app project to the AppHost
var processor = builder.AddProject("processor")
.WithReference(cache);
builder.Build().Run();
```
- C#
Install the JavaScript hosting integration in your AppHost. It provides `AddNodeApp` / `addNodeApp`, which runs a JavaScript file with Node.js:
* TypeScript
* Aspire CLI
Aspire CLI — Add Aspire.Hosting.JavaScript package
```bash
aspire add javascript
```
The Aspire CLI is interactive, be sure to select the appropriate search result when prompted:
Aspire CLI — Example output prompt
```bash
Select an integration to add:
> javascript (Aspire.Hosting.JavaScript)
> Other results listed as selectable options...
```
Note
If there's only one result, the CLI selects it automatically. You'll still need to confirm the version.
* C#
Aspire CLI — Add Aspire.Hosting.JavaScript package
```bash
aspire add javascript
```
The Aspire CLI is interactive, be sure to select the appropriate search result when prompted:
Aspire CLI — Example output prompt
```bash
Select an integration to add:
> javascript (Aspire.Hosting.JavaScript)
> Other results listed as selectable options...
```
Note
If there's only one result, the CLI selects it automatically. You'll still need to confirm the version.
* Aspire CLI
* Aspire CLI
Aspire CLI — Add Aspire.Hosting.JavaScript package
```bash
aspire add javascript
```
The Aspire CLI is interactive, be sure to select the appropriate search result when prompted:
Aspire CLI — Example output prompt
```bash
Select an integration to add:
> javascript (Aspire.Hosting.JavaScript)
> Other results listed as selectable options...
```
Note
If there's only one result, the CLI selects it automatically. You'll still need to confirm the version.
* apphost.cs (C# file-based app)
C# — AppHost.cs
```csharp
#:package Aspire.Hosting.JavaScript@*
```
* PackageReference (\*.csproj)
XML — Add Aspire.Hosting.JavaScript package reference
```xml
```
* Aspire CLI
Aspire CLI — Add Aspire.Hosting.JavaScript package
```bash
aspire add javascript
```
The Aspire CLI is interactive, be sure to select the appropriate search result when prompted:
Aspire CLI — Example output prompt
```bash
Select an integration to add:
> javascript (Aspire.Hosting.JavaScript)
> Other results listed as selectable options...
```
Note
If there's only one result, the CLI selects it automatically. You'll still need to confirm the version.
* apphost.cs (C# file-based app)
C# — AppHost.cs
```csharp
#:package Aspire.Hosting.JavaScript@*
```
* PackageReference (\*.csproj)
XML — Add Aspire.Hosting.JavaScript package reference
```xml
```
Then add the console app to your AppHost:
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const cache = await builder.addRedis('cache');
// Add a Node.js console app to the AppHost
await builder
.addNodeApp('processor', '../processor', 'index.js')
.withReference(cache);
await builder.build().run();
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
var cache = builder.AddRedis("cache");
// Add a Node.js console app to the AppHost
var processor = builder.AddNodeApp("processor", "../processor", "index.js")
.WithReference(cache);
builder.Build().Run();
```
The console app uses the [ioredis](https://github.com/redis/ioredis) client. Aspire installs the dependencies listed in *package.json* before starting the app:
processor/package.json
```json
{
"name": "processor",
"private": true,
"type": "module",
"dependencies": {
"ioredis": "^5.4.0"
}
}
```
processor/index.js
```javascript
import Redis from 'ioredis';
const redis = new Redis(process.env.CACHE_URI);
await redis.set('processor:last-run', new Date().toISOString());
console.log('Last run:', await redis.get('processor:last-run'));
await redis.quit();
```
The `processor` app receives the `CACHE_URI`, `CACHE_HOST`, `CACHE_PORT`, and `CACHE_PASSWORD` environment variables at runtime, along with `ConnectionStrings__cache`. For more information, see [Connect to Redis](/integrations/caching/redis/redis-connect/).
Tip
To start a Node.js app with a *package.json* script (for example, `npm run start`) instead of a specific file, use `AddJavaScriptApp` / `addJavaScriptApp`. For more information, see [Add JavaScript application](/integrations/frameworks/javascript/#add-javascript-application).
- TypeScript
* Aspire CLI
Aspire CLI — Add Aspire.Hosting.JavaScript package
```bash
aspire add javascript
```
The Aspire CLI is interactive, be sure to select the appropriate search result when prompted:
Aspire CLI — Example output prompt
```bash
Select an integration to add:
> javascript (Aspire.Hosting.JavaScript)
> Other results listed as selectable options...
```
Note
If there's only one result, the CLI selects it automatically. You'll still need to confirm the version.
- C#
Aspire CLI — Add Aspire.Hosting.JavaScript package
```bash
aspire add javascript
```
The Aspire CLI is interactive, be sure to select the appropriate search result when prompted:
Aspire CLI — Example output prompt
```bash
Select an integration to add:
> javascript (Aspire.Hosting.JavaScript)
> Other results listed as selectable options...
```
Note
If there's only one result, the CLI selects it automatically. You'll still need to confirm the version.
- Aspire CLI
* Aspire CLI
Aspire CLI — Add Aspire.Hosting.JavaScript package
```bash
aspire add javascript
```
The Aspire CLI is interactive, be sure to select the appropriate search result when prompted:
Aspire CLI — Example output prompt
```bash
Select an integration to add:
> javascript (Aspire.Hosting.JavaScript)
> Other results listed as selectable options...
```
Note
If there's only one result, the CLI selects it automatically. You'll still need to confirm the version.
* apphost.cs (C# file-based app)
C# — AppHost.cs
```csharp
#:package Aspire.Hosting.JavaScript@*
```
* PackageReference (\*.csproj)
XML — Add Aspire.Hosting.JavaScript package reference
```xml
```
- Aspire CLI
Aspire CLI — Add Aspire.Hosting.JavaScript package
```bash
aspire add javascript
```
The Aspire CLI is interactive, be sure to select the appropriate search result when prompted:
Aspire CLI — Example output prompt
```bash
Select an integration to add:
> javascript (Aspire.Hosting.JavaScript)
> Other results listed as selectable options...
```
Note
If there's only one result, the CLI selects it automatically. You'll still need to confirm the version.
- apphost.cs (C# file-based app)
C# — AppHost.cs
```csharp
#:package Aspire.Hosting.JavaScript@*
```
- PackageReference (\*.csproj)
XML — Add Aspire.Hosting.JavaScript package reference
```xml
```
- TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const cache = await builder.addRedis('cache');
// Add a Node.js console app to the AppHost
await builder
.addNodeApp('processor', '../processor', 'index.js')
.withReference(cache);
await builder.build().run();
```
- C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
var cache = builder.AddRedis("cache");
// Add a Node.js console app to the AppHost
var processor = builder.AddNodeApp("processor", "../processor", "index.js")
.WithReference(cache);
builder.Build().Run();
```
## Service discovery in console apps
[Section titled “Service discovery in console apps”](#service-discovery-in-console-apps)
Console apps can call HTTP services that the AppHost manages. The following walkthrough assumes a `CatalogService` API project that exposes an HTTP endpoint at `/`, and a console app named `processor` that calls it.
* .NET
Console apps using the .NET Generic Host can use the same service discovery infrastructure as web apps. This walkthrough uses a `MyConsoleApp` console project.
1. From the console project directory, install the hosting package:
* .NET CLI
.NET CLI — Add Microsoft.Extensions.Hosting package
```bash
dotnet add package Microsoft.Extensions.Hosting
```
* Program.cs (C# file-based app)
Program.cs
```csharp
#:package Microsoft.Extensions.Hosting@*
```
* PackageReference (\*.csproj)
XML — Add Microsoft.Extensions.Hosting package reference
```xml
```
Add a reference to your existing service defaults project in the console project’s *MyConsoleApp.csproj*. Replace the example path with your project’s actual path:
MyConsoleApp.csproj
```xml
```
2. Add a typed HTTP client to the console project:
CatalogClient.cs
```csharp
using System.Net.Http;
public sealed class CatalogClient(HttpClient httpClient)
{
public Task GetAsync(CancellationToken cancellationToken = default)
=> httpClient.GetStringAsync("/", cancellationToken);
}
```
3. Configure the host in your console app’s *Program.cs*. This example starts the host, calls the catalog API, and then stops:
Program.cs
```csharp
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
var builder = Host.CreateApplicationBuilder(args);
// Add service defaults (telemetry, health checks, service discovery)
builder.AddServiceDefaults();
// Register your services
builder.Services.AddHttpClient(
client => client.BaseAddress = new Uri("https+http://catalog"));
using var app = builder.Build();
await app.StartAsync();
var catalogClient = app.Services.GetRequiredService();
Console.WriteLine(await catalogClient.GetAsync());
await app.StopAsync();
```
4. In the AppHost, reference the services this app depends on:
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const catalog = await builder.addProject(
'catalog',
'../CatalogService/CatalogService.csproj'
);
const processor = await builder.addProject(
'processor',
'../MyConsoleApp/MyConsoleApp.csproj'
);
await processor.withReference(catalog);
await processor.waitFor(catalog);
await builder.build().run();
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
var catalog = builder.AddProject("catalog");
var processor = builder.AddProject("processor")
.WithReference(catalog)
.WaitFor(catalog);
builder.Build().Run();
```
The `WithReference(catalog)` call injects service discovery configuration so `processor` can resolve `https+http://catalog` to the running catalog service endpoint.
* Node.js
.NET CLI — Add Microsoft.Extensions.Hosting package
```bash
dotnet add package Microsoft.Extensions.Hosting
```
* .NET CLI
Program.cs
```csharp
#:package Microsoft.Extensions.Hosting@*
```
* Program.cs (C# file-based app)
XML — Add Microsoft.Extensions.Hosting package reference
```xml
```
* PackageReference (\*.csproj)
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const catalog = await builder.addProject(
'catalog',
'../CatalogService/CatalogService.csproj'
);
const processor = await builder.addProject(
'processor',
'../MyConsoleApp/MyConsoleApp.csproj'
);
await processor.withReference(catalog);
await processor.waitFor(catalog);
await builder.build().run();
```
* TypeScript
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
var catalog = builder.AddProject("catalog");
var processor = builder.AddProject("processor")
.WithReference(catalog)
.WaitFor(catalog);
builder.Build().Run();
```
* C#
Node.js apps don’t use the .NET service discovery resolver. Instead, they read the catalog service’s endpoint URLs from environment variables that Aspire injects.
1. Create a *processor* folder next to your AppHost with a *package.json* file. This example uses the built-in `fetch` API, so it has no dependencies:
processor/package.json
```json
{
"name": "processor",
"private": true,
"type": "module"
}
```
2. Add an *index.js* file that calls the catalog API and then exits. It prefers the HTTPS endpoint and falls back to HTTP:
processor/index.js
```javascript
const catalogUrl = process.env.CATALOG_HTTPS ?? process.env.CATALOG_HTTP;
if (!catalogUrl) {
throw new Error(
'The catalog endpoint is not configured. Run the app through Aspire.'
);
}
const response = await fetch(new URL('/', catalogUrl));
console.log(await response.text());
```
3. In the AppHost, reference the services this app depends on:
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const catalog = await builder.addProject(
'catalog',
'../CatalogService/CatalogService.csproj'
);
const processor = await builder.addNodeApp(
'processor',
'../processor',
'index.js'
);
await processor.withReference(catalog);
await processor.waitFor(catalog);
await builder.build().run();
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
var catalog = builder.AddProject("catalog");
var processor = builder.AddNodeApp("processor", "../processor", "index.js")
.WithReference(catalog)
.WaitFor(catalog);
builder.Build().Run();
```
The `withReference(catalog)` call injects the catalog service’s endpoint URLs into `processor` as `CATALOG_HTTPS` and `CATALOG_HTTP`. Aspire also injects the same URLs in the `services__catalog__https__0` format. For more information, see [Service discovery](/fundamentals/service-discovery/).
To send traces, metrics, and logs from a Node.js app to the dashboard, configure the OpenTelemetry Node.js SDK in the app. For more information, see [Add telemetry configuration](/get-started/add-aspire-existing-app/#add-telemetry-configuration-optional).
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const catalog = await builder.addProject(
'catalog',
'../CatalogService/CatalogService.csproj'
);
const processor = await builder.addNodeApp(
'processor',
'../processor',
'index.js'
);
await processor.withReference(catalog);
await processor.waitFor(catalog);
await builder.build().run();
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
var catalog = builder.AddProject("catalog");
var processor = builder.AddNodeApp("processor", "../processor", "index.js")
.WithReference(catalog)
.WaitFor(catalog);
builder.Build().Run();
```
## Background worker services
[Section titled “Background worker services”](#background-worker-services)
Background workers run for as long as the AppHost runs and stop when it shuts down. In .NET, Worker Service projects are the recommended pattern for long-running background tasks with Aspire. In Node.js, use a script that loops until it receives a shutdown signal.
### Creating a worker
[Section titled “Creating a worker”](#creating-a-worker)
* .NET
Worker Service projects use `IHostedService` (or `BackgroundService`) and integrate naturally with the .NET Generic Host.
Create a `DataWorker` project with the Worker Service template, then add a reference to your shared service defaults project as shown in the console walkthrough:
.NET CLI
```bash
dotnet new worker --name DataWorker --output DataWorker
```
From the `DataWorker` directory, install the Redis **client** integration. This package supplies `AddRedisClient`; the AppHost’s hosting package doesn’t register a client in the worker:
* .NET CLI
.NET CLI — Add Aspire.StackExchange.Redis package
```bash
dotnet add package Aspire.StackExchange.Redis
```
* Program.cs (C# file-based app)
Program.cs
```csharp
#:package Aspire.StackExchange.Redis@*
```
* PackageReference (\*.csproj)
XML — Add Aspire.StackExchange.Redis package reference
```xml
```
Replace the generated worker class with *DataProcessor.cs*. Its `DoWorkAsync` method is a placeholder for your application’s processing logic; registering the Redis client doesn’t itself perform Redis operations.
DataProcessor.cs
```csharp
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
public class DataProcessor : BackgroundService
{
private readonly ILogger _logger;
public DataProcessor(ILogger logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
_logger.LogInformation("Processing data at: {Time}", DateTimeOffset.UtcNow);
await DoWorkAsync(stoppingToken);
await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
}
}
private Task DoWorkAsync(CancellationToken stoppingToken)
{
// Replace with your background work.
return Task.CompletedTask;
}
}
```
Program.cs
```csharp
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
var builder = Host.CreateApplicationBuilder(args);
builder.AddServiceDefaults();
// Register resource connections
builder.AddRedisClient("cache");
// Register the background worker
builder.Services.AddHostedService();
var app = builder.Build();
await app.RunAsync();
```
* Node.js
.NET CLI — Add Aspire.StackExchange.Redis package
```bash
dotnet add package Aspire.StackExchange.Redis
```
* .NET CLI
Program.cs
```csharp
#:package Aspire.StackExchange.Redis@*
```
* Program.cs (C# file-based app)
XML — Add Aspire.StackExchange.Redis package reference
```xml
```
* PackageReference (\*.csproj)
Create a *data-worker* folder next to your AppHost with a *package.json* file that includes the [ioredis](https://github.com/redis/ioredis) client:
data-worker/package.json
```json
{
"name": "data-worker",
"private": true,
"type": "module",
"dependencies": {
"ioredis": "^5.4.0"
}
}
```
Add a *worker.js* file. The worker runs every 10 seconds until Aspire stops the process, then closes its Redis connection. Its `doWork` function is a placeholder for your application’s processing logic.
data-worker/worker.js
```javascript
import { setTimeout as delay } from 'node:timers/promises';
import Redis from 'ioredis';
const redis = new Redis(process.env.CACHE_URI);
// Stop the loop when Aspire stops the process
const controller = new AbortController();
for (const signal of ['SIGINT', 'SIGTERM']) {
process.once(signal, () => controller.abort());
}
async function doWork(redis, signal) {
// Replace with your background work.
}
try {
while (!controller.signal.aborted) {
console.log(`Processing data at: ${new Date().toISOString()}`);
await doWork(redis, controller.signal);
await delay(10_000, undefined, { signal: controller.signal });
}
} catch (error) {
if (error.name !== 'AbortError') {
throw error;
}
} finally {
await redis.quit();
}
```
### Adding to the AppHost
[Section titled “Adding to the AppHost”](#adding-to-the-apphost)
The following model also provides a PostgreSQL database. Install its hosting integration in the AppHost before using this example:
* TypeScript
* Aspire CLI
Aspire CLI — Add Aspire.Hosting.PostgreSQL package
```bash
aspire add postgresql
```
The Aspire CLI is interactive, be sure to select the appropriate search result when prompted:
Aspire CLI — Example output prompt
```bash
Select an integration to add:
> postgresql (Aspire.Hosting.PostgreSQL)
> Other results listed as selectable options...
```
Note
If there's only one result, the CLI selects it automatically. You'll still need to confirm the version.
* C#
Aspire CLI — Add Aspire.Hosting.PostgreSQL package
```bash
aspire add postgresql
```
The Aspire CLI is interactive, be sure to select the appropriate search result when prompted:
Aspire CLI — Example output prompt
```bash
Select an integration to add:
> postgresql (Aspire.Hosting.PostgreSQL)
> Other results listed as selectable options...
```
Note
If there's only one result, the CLI selects it automatically. You'll still need to confirm the version.
* Aspire CLI
* Aspire CLI
Aspire CLI — Add Aspire.Hosting.PostgreSQL package
```bash
aspire add postgresql
```
The Aspire CLI is interactive, be sure to select the appropriate search result when prompted:
Aspire CLI — Example output prompt
```bash
Select an integration to add:
> postgresql (Aspire.Hosting.PostgreSQL)
> Other results listed as selectable options...
```
Note
If there's only one result, the CLI selects it automatically. You'll still need to confirm the version.
* apphost.cs (C# file-based app)
C# — AppHost.cs
```csharp
#:package Aspire.Hosting.PostgreSQL@*
```
* PackageReference (\*.csproj)
XML — Add Aspire.Hosting.PostgreSQL package reference
```xml
```
* Aspire CLI
Aspire CLI — Add Aspire.Hosting.PostgreSQL package
```bash
aspire add postgresql
```
The Aspire CLI is interactive, be sure to select the appropriate search result when prompted:
Aspire CLI — Example output prompt
```bash
Select an integration to add:
> postgresql (Aspire.Hosting.PostgreSQL)
> Other results listed as selectable options...
```
Note
If there's only one result, the CLI selects it automatically. You'll still need to confirm the version.
* apphost.cs (C# file-based app)
C# — AppHost.cs
```csharp
#:package Aspire.Hosting.PostgreSQL@*
```
* PackageReference (\*.csproj)
XML — Add Aspire.Hosting.PostgreSQL package reference
```xml
```
The database reference supplies connection information, but the worker above doesn’t yet use it. To add database operations, configure a client as described in [Connect to PostgreSQL](/integrations/databases/postgres/postgres-connect/).
* .NET
Add the Worker Service project to your AppHost:
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const cache = await builder.addRedis('cache');
const postgres = await builder.addPostgres('postgres');
const db = await postgres.addDatabase('app-db');
const worker = await builder.addProject(
'data-worker',
'../DataWorker/DataWorker.csproj'
);
await worker.withReference(cache);
await worker.withReference(db);
await worker.waitFor(db);
await builder.build().run();
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
var cache = builder.AddRedis("cache");
var db = builder.AddPostgres("postgres").AddDatabase("app-db");
var worker = builder.AddProject("data-worker")
.WithReference(cache)
.WithReference(db)
.WaitFor(db);
builder.Build().Run();
```
* Node.js
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const cache = await builder.addRedis('cache');
const postgres = await builder.addPostgres('postgres');
const db = await postgres.addDatabase('app-db');
const worker = await builder.addProject(
'data-worker',
'../DataWorker/DataWorker.csproj'
);
await worker.withReference(cache);
await worker.withReference(db);
await worker.waitFor(db);
await builder.build().run();
```
* TypeScript
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
var cache = builder.AddRedis("cache");
var db = builder.AddPostgres("postgres").AddDatabase("app-db");
var worker = builder.AddProject("data-worker")
.WithReference(cache)
.WithReference(db)
.WaitFor(db);
builder.Build().Run();
```
* C#
Add the Node.js worker to your AppHost:
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const cache = await builder.addRedis('cache');
const postgres = await builder.addPostgres('postgres');
const db = await postgres.addDatabase('app-db');
const worker = await builder.addNodeApp(
'data-worker',
'../data-worker',
'worker.js'
);
await worker.withReference(cache);
await worker.withReference(db);
await worker.waitFor(db);
await builder.build().run();
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
var cache = builder.AddRedis("cache");
var db = builder.AddPostgres("postgres").AddDatabase("app-db");
var worker = builder.AddNodeApp("data-worker", "../data-worker", "worker.js")
.WithReference(cache)
.WithReference(db)
.WaitFor(db);
builder.Build().Run();
```
The worker receives the database connection properties with an `APP_DB_` prefix, such as `APP_DB_URI`. Aspire replaces the dash in the resource name with an underscore in these variable names. For more information, see [Resource naming conventions](/app-host/resource-naming/).
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const cache = await builder.addRedis('cache');
const postgres = await builder.addPostgres('postgres');
const db = await postgres.addDatabase('app-db');
const worker = await builder.addNodeApp(
'data-worker',
'../data-worker',
'worker.js'
);
await worker.withReference(cache);
await worker.withReference(db);
await worker.waitFor(db);
await builder.build().run();
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
var cache = builder.AddRedis("cache");
var db = builder.AddPostgres("postgres").AddDatabase("app-db");
var worker = builder.AddNodeApp("data-worker", "../data-worker", "worker.js")
.WithReference(cache)
.WithReference(db)
.WaitFor(db);
builder.Build().Run();
```
For more information on the Redis and PostgreSQL hosting integrations, see [Redis integration](/integrations/caching/redis/redis-get-started/) and [PostgreSQL integration](/integrations/databases/postgres/postgres-get-started/).
## Explicit start and console interaction
[Section titled “Explicit start and console interaction”](#explicit-start-and-console-interaction)
By default, all resources start automatically when the AppHost runs. For console apps that should only run on demand, use `WithExplicitStart`.
* .NET
In your AppHost, add the console app project with `WithExplicitStart`:
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const tool = await builder.addProject(
'tool',
'../MyInteractiveTool/MyInteractiveTool.csproj'
);
await tool.withExplicitStart();
await builder.build().run();
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
var tool = builder.AddProject("tool")
.WithExplicitStart();
builder.Build().Run();
```
* Node.js
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const tool = await builder.addProject(
'tool',
'../MyInteractiveTool/MyInteractiveTool.csproj'
);
await tool.withExplicitStart();
await builder.build().run();
```
* TypeScript
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
var tool = builder.AddProject("tool")
.WithExplicitStart();
builder.Build().Run();
```
* C#
In your AppHost, add the Node.js app with `WithExplicitStart`:
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const tool = await builder.addNodeApp('tool', '../tool', 'index.js');
await tool.withExplicitStart();
await builder.build().run();
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
var tool = builder.AddNodeApp("tool", "../tool", "index.js")
.WithExplicitStart();
builder.Build().Run();
```
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const tool = await builder.addNodeApp('tool', '../tool', 'index.js');
await tool.withExplicitStart();
await builder.build().run();
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
var tool = builder.AddNodeApp("tool", "../tool", "index.js")
.WithExplicitStart();
builder.Build().Run();
```
With `WithExplicitStart`, the resource appears in the Aspire dashboard but doesn’t start automatically. You can start it manually from the dashboard or programmatically.
Caution
Interactive console apps that read from stdin (for example, `Console.ReadLine()` in .NET or the `node:readline` module in Node.js) aren’t fully supported in the Aspire dashboard’s **Console logs** view. The dashboard captures and displays stdout/stderr, but the console logs page doesn’t forward user input to the process’s stdin.
Aspire builds that include the experimental `WithTerminal` API support interactive terminal user interfaces (TUIs) and shell-based experiences that need a live, bidirectional session, separately from the dashboard’s console log viewer.
Experimental API
`WithTerminal` is reported under diagnostic ID `ASPIRETERMINAL001`. Using it in C# requires suppressing that diagnostic, and its API surface and behavior may change. This API might not be present in your installed release; use compatible hosting packages and a CLI build that includes it.
## Viewing console output
[Section titled “Viewing console output”](#viewing-console-output)
Aspire captures stdout and stderr from all managed resources and displays them in the dashboard’s **Console logs** page. Console apps output is fully visible, including colored text (ANSI escape codes are rendered).
For more information, see [Console logs page](/dashboard/explore/#console-logs-page).
* .NET
For structured logging in console apps, use `ILogger` instead of `Console.Write`. The following fragment assumes an existing logger and item identifier:
Program.cs
```csharp
// Prefer structured logging
logger.LogInformation("Processing item {ItemId}", itemId);
// Instead of
Console.WriteLine($"Processing item {itemId}");
```
With console logging and OpenTelemetry logging/export configured, such as through the service defaults setup above, logs appear in both the **Console logs** and **Structured logs** views of the dashboard. Calling `ILogger` alone doesn’t export structured logs to the dashboard.
* Node.js
Output from `console.log` and `console.error` appears in the **Console logs** view. To also see logs in the **Structured logs** view, emit them through the OpenTelemetry Logs API. The following fragment assumes the OpenTelemetry Node.js SDK is configured with an OTLP log exporter, and an existing item identifier:
index.js
```javascript
import { logs, SeverityNumber } from '@opentelemetry/api-logs';
const logger = logs.getLogger('processor');
// Prefer structured logging
logger.emit({
severityNumber: SeverityNumber.INFO,
severityText: 'Information',
body: 'Processing item',
attributes: { itemId },
});
// Instead of
console.log(`Processing item ${itemId}`);
```
Logs emitted without a configured log exporter aren’t sent to the dashboard. For more information on setting up the SDK, see [Add telemetry configuration](/get-started/add-aspire-existing-app/#add-telemetry-configuration-optional).
## Using connection strings without service defaults
[Section titled “Using connection strings without service defaults”](#using-connection-strings-without-service-defaults)
Apps that don’t use the service defaults project, such as a minimal .NET console app without a host or any Node.js app, can read connection information directly from environment variables.
* .NET
For the configuration API example, install the environment variables configuration provider in your console project:
* .NET CLI
.NET CLI — Add Microsoft.Extensions.Configuration.EnvironmentVariables package
```bash
dotnet add package Microsoft.Extensions.Configuration.EnvironmentVariables
```
* Program.cs (C# file-based app)
Program.cs
```csharp
#:package Microsoft.Extensions.Configuration.EnvironmentVariables@*
```
* PackageReference (\*.csproj)
XML — Add Microsoft.Extensions.Configuration.EnvironmentVariables package reference
```xml
```
Program.cs
```csharp
// For .NET apps, read from configuration
using Microsoft.Extensions.Configuration;
var config = new ConfigurationBuilder()
.AddEnvironmentVariables()
.Build();
var redisConnection = config.GetConnectionString("cache");
// Or equivalently:
var redisConnection2 = Environment.GetEnvironmentVariable("ConnectionStrings__cache");
```
Tip
When using configuration APIs directly, use `GetConnectionString("cache")` rather than reading `ConnectionStrings__cache` directly. The configuration system maps the double-underscore separator to the colon used by `IConfiguration`, making `ConnectionStrings:cache` and `ConnectionStrings__cache` equivalent. For more information, see [Resource naming conventions](/app-host/resource-naming/).
* Node.js
.NET CLI — Add Microsoft.Extensions.Configuration.EnvironmentVariables package
```bash
dotnet add package Microsoft.Extensions.Configuration.EnvironmentVariables
```
* .NET CLI
Program.cs
```csharp
#:package Microsoft.Extensions.Configuration.EnvironmentVariables@*
```
* Program.cs (C# file-based app)
XML — Add Microsoft.Extensions.Configuration.EnvironmentVariables package reference
```xml
```
* PackageReference (\*.csproj)
Read the values from `process.env`. Node.js doesn’t map the `:` and `__` separators, so use the exact environment variable name:
index.js
```javascript
// Prefer the connection properties for the resource
const redisUri = process.env.CACHE_URI;
// The full connection string is also available:
const redisConnection = process.env.ConnectionStrings__cache;
```
For Redis, `ConnectionStrings__cache` uses the StackExchange.Redis connection string format rather than a URI, so pass `CACHE_URI` to Node.js clients such as ioredis. For the properties each resource provides, see that integration’s connect page, such as [Connect to Redis](/integrations/caching/redis/redis-connect/).
## See also
[Section titled “See also”](#see-also)
* [Service defaults](/get-started/csharp-service-defaults/)
* [Service discovery](/fundamentals/service-discovery/)
* [Resource naming conventions](/app-host/resource-naming/)
* [Executable resources](/app-host/executable-resources/)
* [JavaScript integration](/integrations/frameworks/javascript/)
* [.NET Worker Service](https://learn.microsoft.com/dotnet/core/extensions/workers)
# Container files
> Inject files and directories into Aspire container resources at development and publish time using WithContainerFiles, with options for source paths and permissions.
Aspire provides APIs to inject files and directories into containers, enabling you to configure containerized resources with custom configuration files, scripts, certificates, and other assets. There are two complementary APIs:
* [`WithContainerFiles`](/reference/api/csharp/aspire.hosting/containerresourcebuilderextensions/methods/#withcontainerfiles "Creates or updates files and/or folders at the destination path in the container.")[`withContainerFiles`](/reference/api/typescript/aspire.hosting/withcontainerfiles/ "Creates or updates files and folders in a container by copying them from a source path on the host."): Injects files into containers at development time when they start during `aspire run`.
* [`PublishWithContainerFiles`](/reference/api/csharp/aspire.hosting/resourcebuilderextensions/methods/#publishwithcontainerfiles "Configures the resource to copy container files from the specified source resource during publishing.")[`publishWithContainerFiles`](/reference/api/typescript/aspire.hosting/publishwithcontainerfiles/ "Configures the resource to copy container files from the specified source resource during publishing."): Copies files from one resource’s container into another resource’s container as build artifacts at publish time during `aspire publish`.
## Inject files at development time
[Section titled “Inject files at development time”](#inject-files-at-development-time)
The `WithContainerFiles` extension method creates or updates files and directories inside a container at a specified destination path. It supports three approaches depending on your needs: inline entries for declarative file definitions, a source path for copying from the host file system, and an async callback for dynamic file generation.
Caution
`WithContainerFiles` is primarily intended for development-time configuration and is not supported at publish time. To inject files into containers during publish, use [`PublishWithContainerFiles`](#inject-files-at-publish-time) instead.
### Inline entries
[Section titled “Inline entries”](#inline-entries)
Use the inline entries overload to declaratively define files and directories using `ContainerFileSystemItem` objects. This is useful when file contents are known at build time or can be expressed as string literals.
* TypeScript
Note
The `withContainerFiles` API is not yet available in the TypeScript AppHost SDK.
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
builder.AddContainer("myapp", "myapp:latest")
.WithContainerFiles("/app/config", [
new ContainerFile
{
Name = "appsettings.json",
Contents = """
{
"Logging": {
"LogLevel": {
"Default": "Information"
}
}
}
"""
},
new ContainerDirectory
{
Name = "scripts",
Entries = [
new ContainerFile
{
Name = "init.sh",
Contents = "#!/bin/bash\necho 'Initializing...'",
Mode = UnixFileMode.UserRead
| UnixFileMode.UserWrite
| UnixFileMode.UserExecute
}
]
}
]);
builder.Build().Run();
```
In the preceding example:
* A JSON configuration file is created at `/app/config/appsettings.json` with the specified contents.
* A nested `scripts` directory is created at `/app/config/scripts/` containing an executable shell script.
### Source path
[Section titled “Source path”](#source-path)
Use the source path overload to copy files from a directory on the host machine into the container. This is useful when you have existing configuration files or assets on disk.
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const frontend = await builder.addViteApp("frontend", "../frontend");
await frontend.withContainerFilesSource("/app/dist");
const api = await builder.addProject("api", "../Api/Api.csproj");
await api.publishWithContainerFiles(frontend, "./wwwroot");
await builder.build().run();
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
builder.AddContainer("myapp", "myapp:latest")
.WithContainerFiles("/app/config", "./config-files");
builder.Build().Run();
```
Unless the source path is a rooted (absolute) path, it’s interpreted as relative to the AppHost project directory. All files in the source directory are copied to the destination path in the container at startup.
### Async callback
[Section titled “Async callback”](#async-callback)
Use the callback overload to generate files dynamically when the container starts. The callback receives a `ContainerFileSystemCallbackContext` that provides access to the `IServiceProvider` and the resource’s `IResource` model, enabling you to resolve services or inspect the app model.
* TypeScript
Note
The `withContainerFiles` API is not yet available in the TypeScript AppHost SDK.
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
builder.AddContainer("worker", "worker:latest")
.WithContainerFiles("/app/config",
async (context, cancellationToken) =>
{
var config = new
{
MachineName = Environment.MachineName,
Timestamp = DateTime.UtcNow
};
return
[
new ContainerFile
{
Name = "runtime-config.json",
Contents = JsonSerializer.Serialize(config)
}
];
});
builder.Build().Run();
```
The callback is invoked each time the container starts, so the generated files always reflect the current state.
## File types
[Section titled “File types”](#file-types)
The `WithContainerFiles` API uses a type hierarchy rooted at the abstract `ContainerFileSystemItem` class. Each type represents a different kind of file system entry.
### ContainerFile
[Section titled “ContainerFile”](#containerfile)
`ContainerFile` represents a standard file. Set either `Contents` (a string) or `SourcePath` (an absolute path on the host) to provide the file data — the two are mutually exclusive.
AppHost.cs
```csharp
// File with inline contents
var configYaml = new ContainerFile
{
Name = "config.yaml",
Contents = "key: value"
};
// File sourced from the host file system
var dataCsv = new ContainerFile
{
Name = "data.csv",
SourcePath = "/path/to/data.csv"
};
```
Set `ContinueOnError` to `true` to allow the container to start even if creating this particular file fails:
AppHost.cs
```csharp
var optionalJson = new ContainerFile
{
Name = "optional-config.json",
Contents = "{}",
ContinueOnError = true
};
```
### ContainerDirectory
[Section titled “ContainerDirectory”](#containerdirectory)
`ContainerDirectory` represents a directory that can contain nested `ContainerFileSystemItem` entries, allowing you to build arbitrary directory trees.
AppHost.cs
```csharp
var certsDir = new ContainerDirectory
{
Name = "certs",
Entries = [
new ContainerFile
{
Name = "ca.pem",
SourcePath = "/path/to/ca.pem"
},
new ContainerDirectory
{
Name = "private",
Entries = [
new ContainerFile
{
Name = "server.key",
SourcePath = "/path/to/server.key",
Mode = UnixFileMode.UserRead
}
]
}
]
};
```
You can also populate a `ContainerDirectory` from files on disk using the static `GetFileSystemItemsFromPath` method:
AppHost.cs
```csharp
var assetsDir = new ContainerDirectory
{
Name = "assets",
Entries = ContainerDirectory.GetFileSystemItemsFromPath(
"/path/to/assets",
searchOptions: SearchOption.AllDirectories)
};
```
### ContainerOpenSSLCertificateFile
[Section titled “ContainerOpenSSLCertificateFile”](#containeropensslcertificatefile)
`ContainerOpenSSLCertificateFile` represents a PEM-encoded public certificate. In addition to placing the certificate file in the container, Aspire automatically creates an OpenSSL-compatible symlink (`[subject hash].[n]`) in the same directory — equivalent to running `openssl rehash`. This enables containers that use OpenSSL for certificate validation to discover the certificate automatically.
* TypeScript
Note
The `withContainerFiles` API is not yet available in the TypeScript AppHost SDK.
* C#
AppHost.cs
```csharp
builder.AddContainer("myapp", "myapp:latest")
.WithContainerFiles("/certs", [
new ContainerOpenSSLCertificateFile
{
Name = "ca-cert.pem",
Contents = pemCertificateString
}
]);
```
## File permissions and ownership
[Section titled “File permissions and ownership”](#file-permissions-and-ownership)
All `WithContainerFiles` overloads accept optional parameters to control file ownership and permissions.
### Owner and group
[Section titled “Owner and group”](#owner-and-group)
The `defaultOwner` and `defaultGroup` parameters set the default UID and GID applied to all created files and directories. Both default to `0` (root) when not specified. You can override ownership on individual items using the `Owner` and `Group` properties on any `ContainerFileSystemItem`.
* TypeScript
Note
The `withContainerFiles` API is not yet available in the TypeScript AppHost SDK.
* C#
AppHost.cs
```csharp
builder.AddContainer("myapp", "myapp:latest")
.WithContainerFiles("/app/data",
[
new ContainerFile
{
Name = "shared.txt",
Contents = "shared data"
},
new ContainerFile
{
Name = "user-only.txt",
Contents = "private data",
Owner = 1000,
Group = 1000
}
],
defaultOwner: 33, // www-data
defaultGroup: 33);
```
In this example, `shared.txt` inherits the default owner/group of `33`, while `user-only.txt` overrides with UID/GID `1000`.
### Umask
[Section titled “Umask”](#umask)
The `umask` parameter controls default permissions by subtracting (masking) permission bits from the base defaults. Without an explicit `Mode` set on an item:
* **Directories** start with `0777` (read/write/execute for all) and have the umask subtracted
* **Files** start with `0666` (read/write for all) and have the umask subtracted
The default umask is `0022`, which results in:
* Directories: `0755` (owner: rwx, group: r-x, others: r-x)
* Files: `0644` (owner: rw-, group: r—, others: r—)
You can set `Mode` directly on individual items to override the umask-based default:
* TypeScript
Note
The `withContainerFiles` API is not yet available in the TypeScript AppHost SDK.
* C#
AppHost.cs
```csharp
builder.AddContainer("myapp", "myapp:latest")
.WithContainerFiles("/app/scripts",
[
new ContainerFile
{
Name = "run.sh",
Contents = "#!/bin/bash\necho 'Running'",
Mode = UnixFileMode.UserRead
| UnixFileMode.UserWrite
| UnixFileMode.UserExecute
}
],
umask: UnixFileMode.OtherRead
| UnixFileMode.OtherWrite
| UnixFileMode.OtherExecute);
```
### Persistent containers
[Section titled “Persistent containers”](#persistent-containers)
For containers with `ContainerLifetime.Persistent`, changing the contents of container file entries causes the container to be recreated. Ensure any data written through `WithContainerFiles` is idempotent for a given app model configuration to avoid unintended container restarts.
## Inject files at publish time
[Section titled “Inject files at publish time”](#inject-files-at-publish-time)
The `PublishWithContainerFiles` method copies files from one resource’s container into another resource’s container during `aspire publish`. This is the preferred approach for injecting files into containers at publish time.
A key use case is embedding single-page application (SPA) or static JavaScript frontends into a reverse proxy or web server container for production deployment. During development, frontend apps like Vite or React typically run as standalone dev servers. In production, however, the compiled static assets are often served by a backend API or a dedicated web server like Nginx. `PublishWithContainerFiles` bridges this gap by copying the built frontend output into the serving container as part of the publish process — no manual file copying or multi-stage Dockerfile required.
### Embed a frontend in a backend
[Section titled “Embed a frontend in a backend”](#embed-a-frontend-in-a-backend)
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const frontend = await builder.addViteApp("frontend", "../frontend");
const api = await builder.addProject("api", "./Api/Api.csproj")
.publishWithContainerFiles(frontend, "./wwwroot");
await builder.build().run();
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
var frontend = builder.AddViteApp("frontend", "../frontend");
var api = builder.AddProject("api")
.PublishWithContainerFiles(frontend, "./wwwroot");
builder.Build().Run();
```
In this example:
1. The `frontend` resource builds inside its container, producing compiled JavaScript, CSS, and HTML.
2. During publish, Aspire copies those files from the `frontend` container into the `api` container at `./wwwroot`.
3. The resulting `api` container includes both the API code and the frontend static assets, ready to serve the full application.
### Serve a frontend from YARP
[Section titled “Serve a frontend from YARP”](#serve-a-frontend-from-yarp)
You can also embed frontend assets into a dedicated reverse proxy container:
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const frontend = await builder.addViteApp("frontend", "../frontend");
const nginx = await builder.addYarp("gateway")
.publishWithStaticFiles(frontend);
await builder.build().run();
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
var frontend = builder.AddViteApp("frontend", "../frontend");
var nginx = builder.AddYarp("gateway")
.PublishWithStaticFiles(frontend);
builder.Build().Run();
```
This produces a self-contained Nginx container that serves the frontend application, with no external volume mounts or runtime file copying needed.
`PublishWithContainerFiles` only applies in publish mode — it has no effect during `aspire run`. The destination resource must implement `IContainerFilesDestinationResource` (such as `ProjectResource`), and the source resource must implement `IResourceWithContainerFiles`.
### Customize the source path
[Section titled “Customize the source path”](#customize-the-source-path)
By default, the source resource exports files from its container based on its configured output paths. Use [`WithContainerFilesSource`](/reference/api/csharp/aspire.hosting/resourcebuilderextensions/methods/#withcontainerfilessource "Adds a container files source annotation to the resource being built, specifying the path to the container files source.")[`withContainerFilesSource`](/reference/api/typescript/aspire.hosting/withcontainerfilessource/ "Adds a container files source annotation to the resource being built, specifying the path to the container files source.") to specify which path inside the source container to copy from:
* TypeScript
Note
The `withContainerFiles` API is not yet available in the TypeScript AppHost SDK.
* C#
AppHost.cs
```csharp
var frontend = builder.AddViteApp("frontend", "../frontend")
.WithContainerFilesSource("/app/dist");
var api = builder.AddProject("api")
.PublishWithContainerFiles(frontend, "./wwwroot");
```
Use [`ClearContainerFilesSources`](/reference/api/csharp/aspire.hosting/resourcebuilderextensions/methods/#clearcontainerfilessources "Removes any container files source annotation from the resource being built.")[`clearContainerFilesSources`](/reference/api/typescript/aspire.hosting/clearcontainerfilessources/ "Removes any container files source annotation from the resource being built.") to remove any previously configured source paths before adding new ones.
## See also
[Section titled “See also”](#see-also)
* [Certificate configuration](/app-host/certificate-configuration/)
* [Add Dockerfiles to your app model](/app-host/withdockerfile/)
* [Resource lifetimes](/app-host/resource-lifetimes/)
# Container registry configuration
> Configure container registries for Aspire — generic registries, Docker Hub, Azure Container Registry, GitHub Container Registry, and per-resource image tagging.
Aspire 13.1 introduced explicit container registry configuration, giving developers control over where and when container images are pushed during deployment. This article explains how to configure container registries for your Aspire applications.
## Container registry configuration
[Section titled “Container registry configuration”](#container-registry-configuration)
When deploying Aspire applications to production environments, your containerized services need to be pushed to a container registry. Prior to Aspire 13.1, registry configuration was often implicit, making it difficult to control and understand the deployment process. The new `ContainerRegistryResource` provides explicit configuration for:
* **Generic container registries** — DockerHub, GitHub Container Registry (GHCR), Harbor, or any Docker-compatible registry
* **Azure Container Registry** — First-class support with automatic credential management
* **Pipeline integration** — Control when images are built and pushed using `aspire do push`
* **Authentication** — Configure registry credentials securely
## Generic container registries
[Section titled “Generic container registries”](#generic-container-registries)
Caution
This API is experimental and may change in future releases. Use diagnostic code `ASPIRECOMPUTE003` to suppress the experimental warning. For more information, see [ASPIRECOMPUTE003](/diagnostics/aspirecompute003/).
Use the [`AddContainerRegistry`](/reference/api/csharp/aspire.hosting/containerregistryresourcebuilderextensions/methods/#addcontainerregistry "Adds a container registry resource to the application model with parameterized values.")[`addContainerRegistry`](/reference/api/typescript/aspire.hosting/addcontainerregistry/ "Adds a container registry resource") method to configure a generic container registry for your application. This works with any Docker-compatible registry including DockerHub, GitHub Container Registry, Harbor, and private registries.
### Basic usage
[Section titled “Basic usage”](#basic-usage)
The following example configures a container registry and associates it with a project resource:
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
// Add a container registry
const registry = await builder.addContainerRegistry("myregistry", "registry.example.com");
// Associate the registry with a project
const api = await builder.addProject("api", "../Api/Api.csproj");
await api.withContainerRegistry(registry);
await builder.build().run();
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
// Add a container registry
var registry = builder.AddContainerRegistry("myregistry", "registry.example.com");
// Associate the registry with a project
var api = builder.AddProject("api")
.WithContainerRegistry(registry);
builder.Build().Run();
```
The preceding code:
* Creates a container registry resource pointing to `registry.example.com`.
* Associates the registry with the `api` project.
* When deploying, the `api` project will be built as a container image and pushed to the specified registry.
### DockerHub example
[Section titled “DockerHub example”](#dockerhub-example)
To push images to DockerHub, specify `docker.io` as the registry endpoint:
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const registry = await builder.addContainerRegistry("dockerhub", "docker.io");
const api = await builder.addProject("api", "../Api/Api.csproj");
await api.withContainerRegistry(registry);
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
var registry = builder.AddContainerRegistry("dockerhub", "docker.io");
var api = builder.AddProject("api")
.WithContainerRegistry(registry);
```
### GitHub Container Registry example
[Section titled “GitHub Container Registry example”](#github-container-registry-example)
To push images to GitHub Container Registry (GHCR):
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const registry = await builder.addContainerRegistry("ghcr", "ghcr.io");
const api = await builder.addProject("api", "../Api/Api.csproj");
await api.withContainerRegistry(registry);
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
var registry = builder.AddContainerRegistry("ghcr", "ghcr.io");
var api = builder.AddProject("api")
.WithContainerRegistry(registry);
```
### Private registry example
[Section titled “Private registry example”](#private-registry-example)
For private registries, provide the full registry URL:
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const registry = await builder.addContainerRegistry(
"private-registry",
"registry.mycompany.com:5000");
const api = await builder.addProject("api", "../Api/Api.csproj");
await api.withContainerRegistry(registry);
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
var registry = builder.AddContainerRegistry(
"private-registry",
"registry.mycompany.com:5000");
var api = builder.AddProject("api")
.WithContainerRegistry(registry);
```
## Authentication and credentials
[Section titled “Authentication and credentials”](#authentication-and-credentials)
Container registries typically require authentication for pushing images. You can configure credentials using parameters and secrets.
### Using parameters for registry credentials
[Section titled “Using parameters for registry credentials”](#using-parameters-for-registry-credentials)
Parameters allow you to provide registry configuration dynamically:
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const registryEndpoint = await builder.addParameter("registry-endpoint");
const registryRepository = await builder.addParameter("registry-repository");
const registry = await builder.addContainerRegistry(
"myregistry",
registryEndpoint,
registryRepository);
const api = await builder.addProject("api", "../Api/Api.csproj");
await api.withContainerRegistry(registry);
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
var registryEndpoint = builder.AddParameter("registry-endpoint");
var registryRepository = builder.AddParameter("registry-repository");
var registry = builder.AddContainerRegistry(
"myregistry",
registryEndpoint,
registryRepository);
var api = builder.AddProject("api")
.WithContainerRegistry(registry);
```
For more information about parameters, see [External parameters](/fundamentals/external-parameters/).
### Configuring credentials
[Section titled “Configuring credentials”](#configuring-credentials)
Registry credentials should be configured through your deployment environment:
#### Docker login
[Section titled “Docker login”](#docker-login)
Before pushing images, ensure you’re authenticated with the registry:
Docker login
```bash
docker login registry.example.com
```
For DockerHub:
DockerHub login
```bash
docker login docker.io -u username
```
For GitHub Container Registry:
GHCR login
```bash
echo $GITHUB_TOKEN | docker login ghcr.io -u USERNAME --password-stdin
```
#### CI/CD configuration
[Section titled “CI/CD configuration”](#cicd-configuration)
In CI/CD environments (GitHub Actions, Azure Pipelines, and so on), configure credentials using secrets:
GitHub Actions example
```yaml
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Push images
run: aspire do push
```
## Azure Container Registry
[Section titled “Azure Container Registry”](#azure-container-registry)
Azure Container Registry (ACR) provides first-class integration with Aspire, with automatic credential management and parallel provisioning.
### Explicit ACR configuration
[Section titled “Explicit ACR configuration”](#explicit-acr-configuration)
Aspire 13.1 introduces explicit container registry configuration for Azure Container Apps environments:
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const environment = await builder.addAzureContainerAppEnvironment("myenv");
const api = await builder.addProject("api", "../Api/Api.csproj");
await api.withContainerRegistry(environment);
await builder.build().run();
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
var environment = builder.AddAzureContainerAppEnvironment("myenv");
var api = builder.AddProject("api")
.WithContainerRegistry(environment);
builder.Build().Run();
```
In the preceding example:
* The code creates an Azure Container Apps environment with an associated ACR.
* The ACR is provisioned in parallel with that environment.
* Images are pushed as soon as the registry is available.
* Credentials are automatically managed through Azure authentication.
Note
Prior to Aspire 13.1, ACR was provisioned implicitly as part of the Container Apps environment. The explicit configuration provides better control and visibility into the deployment process.
For more information, see [Azure Container Registry integration](/integrations/cloud/azure/azure-container-registry/azure-container-registry-get-started/).
### Using an existing ACR
[Section titled “Using an existing ACR”](#using-an-existing-acr)
To use an existing Azure Container Registry, call the [`PublishAsExisting`](/reference/api/csharp/aspire.hosting.azure/existingazureresourceextensions/methods/#publishasexisting "Marks the resource as an existing resource when the application is deployed.")[`publishAsExisting`](/reference/api/typescript/aspire.hosting.azure/publishasexisting/ "Marks the resource as an existing resource when the application is deployed.") method when you add the ACR:
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const registryName = await builder.addParameter("registryName");
const rgName = await builder.addParameter("rgName");
const acr = await builder.addAzureContainerRegistry("my-acr");
await acr.publishAsExisting(registryName, rgName);
const environment = await builder.addAzureContainerAppEnvironment("env");
await environment.withAzureContainerRegistry(acr);
const api = await builder.addProject("api", "../Api/Api.csproj");
await api.withContainerRegistry(acr);
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
var registryName = builder.AddParameter("registryName");
var rgName = builder.AddParameter("rgName");
var acr = builder.AddAzureContainerRegistry("my-acr")
.PublishAsExisting(registryName, rgName);
builder.AddAzureContainerAppEnvironment("env")
.WithAzureContainerRegistry(acr);
var api = builder.AddProject("api")
.WithContainerRegistry(acr);
```
## Pipeline integration
[Section titled “Pipeline integration”](#pipeline-integration)
Aspire’s deployment pipeline includes a dedicated `push` step for pushing container images to registries.
### Using aspire do push
[Section titled “Using aspire do push”](#using-aspire-do-push)
The `aspire do push` command builds container images and pushes them to configured registries:
Aspire CLI — Push images
```bash
aspire do push
```
This command:
1. Builds all container images for compute resources
2. Tags images with the appropriate registry and repository names
3. Pushes images to their configured registries
Example output:
Output
```plaintext
16:03:38 (pipeline-execution) → Starting pipeline-execution...
16:03:38 (build-api) → Starting build-api...
16:03:43 (push-api) → Starting push-api...
16:03:43 (push-api) → Pushing api to container-registry
16:03:44 (push-api) i [INF] Docker tag for api -> docker.io/username/api:latest succeeded.
16:04:05 (push-api) i [INF] Docker push for docker.io/username/api:latest succeeded.
16:04:05 (push-api) ✓ Successfully pushed api to docker.io/username/api:latest (21.3s)
16:04:05 (push-api) ✓ push-api completed successfully
```
For more information about pipeline commands, see [`aspire do` command](/reference/cli/commands/aspire-do/).
### Pipeline step dependencies
[Section titled “Pipeline step dependencies”](#pipeline-step-dependencies)
The `push` step automatically handles dependencies:
* **`build-prereq`** — Ensures prerequisites are met before building
* **`build-`** — Builds container images for each resource
* **`push-`** — Pushes images to registries
You can execute individual steps or the entire pipeline:
Aspire CLI — Build only
```bash
aspire do build
```
Aspire CLI — Full deployment
```bash
aspire do deploy
```
The `deploy` step includes building, pushing, and deploying all resources.
## Complete examples
[Section titled “Complete examples”](#complete-examples)
### Multi-registry deployment
[Section titled “Multi-registry deployment”](#multi-registry-deployment)
You can configure different registries for different resources:
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const publicRegistry = await builder.addContainerRegistry("dockerhub", "docker.io");
const privateRegistry = await builder.addContainerRegistry(
"private",
"registry.company.com");
const publicApi = await builder.addProject("public-api", "../PublicApi/PublicApi.csproj");
await publicApi.withContainerRegistry(publicRegistry);
const internalApi = await builder.addProject("internal-api", "../InternalApi/InternalApi.csproj");
await internalApi.withContainerRegistry(privateRegistry);
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
var publicRegistry = builder.AddContainerRegistry("dockerhub", "docker.io");
var privateRegistry = builder.AddContainerRegistry(
"private",
"registry.company.com");
var publicApi = builder.AddProject("public-api")
.WithContainerRegistry(publicRegistry);
var internalApi = builder.AddProject("internal-api")
.WithContainerRegistry(privateRegistry);
```
### Parameterized registry configuration
[Section titled “Parameterized registry configuration”](#parameterized-registry-configuration)
You can use parameters when you need flexible deployment across environments:
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const registryEndpoint = await builder.addParameter("registry-endpoint");
const registryRepository = await builder.addParameter("registry-repository");
const registry = await builder.addContainerRegistry(
"container-registry",
registryEndpoint,
registryRepository);
const api = await builder.addProject("api", "../Api/Api.csproj");
await api.withContainerRegistry(registry);
const worker = await builder.addProject("worker", "../Worker/Worker.csproj");
await worker.withContainerRegistry(registry);
await builder.build().run();
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
var registryEndpoint = builder.AddParameter("registry-endpoint");
var registryRepository = builder.AddParameter("registry-repository");
var registry = builder.AddContainerRegistry(
"container-registry",
registryEndpoint,
registryRepository);
var api = builder.AddProject("api")
.WithContainerRegistry(registry);
var worker = builder.AddProject("worker")
.WithContainerRegistry(registry);
builder.Build().Run();
```
Configure the parameters in your AppHost configuration:
appsettings.json
```json
{
"Parameters": {
"registry-endpoint": "ghcr.io",
"registry-repository": "myorg"
}
}
```
Alternatively, use environment variables to configure them:
Environment variables
```bash
export Parameters__registry_endpoint="ghcr.io"
export Parameters__registry_repository="myorg"
```
### Azure deployment with ACR
[Section titled “Azure deployment with ACR”](#azure-deployment-with-acr)
The following code constitutes a complete AppHost example with Azure Container Apps and ACR:
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
// Create Azure Container Apps environment with ACR
const environment = await builder.addAzureContainerAppEnvironment("production");
// Add Redis cache
const cache = await builder.addRedis("cache");
// Add API with registry configuration
const api = await builder.addProject("api", "../Api/Api.csproj");
await api.withReference(cache);
await api.withContainerRegistry(environment);
// Add frontend with registry configuration
const web = await builder.addProject("web", "../Web/Web.csproj");
await web.withReference(api);
await web.withContainerRegistry(environment);
await builder.build().run();
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
// Create Azure Container Apps environment with ACR
var acaEnv = builder.AddAzureContainerAppEnvironment("production");
// Add Redis cache
var cache = builder.AddRedis("cache");
// Add API with registry configuration
var api = builder.AddProject("api")
.WithReference(cache)
.WithContainerRegistry(acaEnv);
// Add frontend with registry configuration
var web = builder.AddProject("web")
.WithReference(api)
.WithContainerRegistry(acaEnv);
builder.Build().Run();
```
## Benefits of explicit configuration
[Section titled “Benefits of explicit configuration”](#benefits-of-explicit-configuration)
The explicit container registry configuration introduced in Aspire 13.1 provides several benefits:
* **Visibility** — Clear understanding of where images are pushed
* **Control** — Explicit configuration of registry endpoints and credentials
* **Parallelization** — Registry provisioning happens in parallel with other resources
* **Early feedback** — Faster deployments with images pushing as soon as registries are ready
* **Flexibility** — Support for any Docker-compatible registry
For a deeper dive into container registry improvements, see [Safia Abdalla’s blog post on fixing Aspire’s image problem](https://blog.safia.rocks/2025/12/15/aspire-image-push/).
## See also
[Section titled “See also”](#see-also)
* [Azure Container Registry integration](/integrations/cloud/azure/azure-container-registry/azure-container-registry-get-started/)
* [Configure Azure Container Apps environments](/integrations/cloud/azure/configure-container-apps/)
* [`aspire do` command](/reference/cli/commands/aspire-do/)
* [External parameters](/fundamentals/external-parameters/)
* [Pipelines and app topology](/deployment/pipelines/)
# CORS configuration
> Learn when and how to configure Cross-Origin Resource Sharing (CORS) in Aspire applications, covering development and production scenarios.
Cross-Origin Resource Sharing (CORS) is a browser security feature that restricts how web pages can request resources from a different domain. This article explains when CORS is relevant in Aspire applications, and how to configure it correctly for development and production.
## When does CORS matter with Aspire?
[Section titled “When does CORS matter with Aspire?”](#when-does-cors-matter-with-aspire)
CORS is only enforced by **browsers**. It applies when a web frontend, running in a user’s browser, makes HTTP requests to an API on a different origin. That origin could be a different host, port, or scheme. Aspire doesn’t add or remove any browser CORS restrictions.
You need to configure CORS in your API when:
* Your Blazor WebAssembly, React, Angular, or other browser-based frontend calls an API on a different port or host.
* Your API is hosted separately from your frontend during development or production.
* Your browser shows errors like `Access to fetch at 'http://localhost:5000' from origin 'http://localhost:3000' has been blocked by CORS policy`.
You **don’t** need to configure CORS when:
* The API request is made on the server, for example by a Blazor Server, Razor Pages, or MVC application. Browser-side requests from those applications still require CORS if they cross origins.
* Your API and frontend share the same origin, consisting of the same scheme, host, and port.
* Communication is server-to-server. That is, backend services calling other backend services.
Note
Aspire’s inner-loop proxy handles port routing between services, but it doesn’t change browser CORS enforcement. The browser still sees the original scheme, host, and port of the API endpoint.
## Configuring CORS in development
[Section titled “Configuring CORS in development”](#configuring-cors-in-development)
During development, the recommended approach is to allow your frontend’s development server origin. Configure CORS in your API’s startup code.
The examples on this page use three sets of tabs, which you choose independently:
* **Aspire CLI**, **apphost.cs (C# file-based app)**, and **PackageReference(\*.csproj)** tabs select an installation method for Aspire integrations. If you’re using a TypeScript AppHost, only the **Aspire CLI** tab is applicable.
* **.NET** and **Node.js** tabs select the language of the API that the AppHost orchestrates.
* **C#** and **TypeScript** tabs select the language of the AppHost itself.
- .NET
Configure CORS with `AddCors` and `UseCors` in your API’s `Program.cs`. The complete API examples assume an existing ASP.NET Core project with a reference to your shared [service defaults](/get-started/csharp-service-defaults/) project, which supplies `AddServiceDefaults` and `MapDefaultEndpoints`.
Program.cs
```csharp
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
// Configure CORS for development
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(policy =>
{
policy.WithOrigins("http://localhost:3000") // Your frontend origin
.AllowAnyHeader()
.AllowAnyMethod();
});
});
var app = builder.Build();
app.UseRouting();
app.UseCors();
app.MapDefaultEndpoints();
app.MapGet("/", () => Results.Ok());
app.Run();
```
- Node.js
Configure CORS with the [`cors`](https://github.com/expressjs/cors) middleware in your API’s entry script. The complete API examples assume an existing [Express](https://expressjs.com/) app in an *api* folder:
api/package.json
```json
{
"name": "api",
"private": true,
"type": "module",
"dependencies": {
"cors": "^2.8.5",
"express": "^5.1.0"
}
}
```
api/server.js
```javascript
import express from 'express';
import cors from 'cors';
const app = express();
// Configure CORS for development
app.use(
cors({
origin: 'http://localhost:3000', // Your frontend origin
})
);
app.get('/', (req, res) => res.sendStatus(200));
app.listen(process.env.PORT ?? 5000);
```
By default, the `cors` middleware allows the common HTTP methods and reflects the headers that the browser requests in a preflight.
Caution
Avoid allowing any origin in production, such as with `AllowAnyOrigin()` in .NET or `origin: '*'` in the Node.js `cors` middleware, as it disables CORS protection entirely. Always specify the exact origins that should be allowed.
### Using environment-based CORS configuration
[Section titled “Using environment-based CORS configuration”](#using-environment-based-cors-configuration)
A better pattern is to read allowed origins from configuration, so you can easily change them per environment:
* .NET
Program.cs
```csharp
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(policy =>
{
var allowedOrigins = builder.Configuration
.GetSection("Cors:AllowedOrigins")
.Get() ?? [];
policy.WithOrigins(allowedOrigins)
.AllowAnyHeader()
.AllowAnyMethod();
});
});
var app = builder.Build();
app.UseRouting();
app.UseCors();
app.MapDefaultEndpoints();
app.MapGet("/", () => Results.Ok());
app.Run();
```
Then configure the allowed origins per environment:
* Development
appsettings.Development.json
```json
{
"Cors": {
"AllowedOrigins": ["http://localhost:3000", "https://localhost:7000"]
}
}
```
* Production
appsettings.json
```json
{
"Cors": {
"AllowedOrigins": ["https://myapp.com", "https://www.myapp.com"]
}
}
```
* Node.js
appsettings.Development.json
```json
{
"Cors": {
"AllowedOrigins": ["http://localhost:3000", "https://localhost:7000"]
}
}
```
* Development
appsettings.json
```json
{
"Cors": {
"AllowedOrigins": ["https://myapp.com", "https://www.myapp.com"]
}
}
```
* Production
This example reads a comma-separated list of origins from the `CORS_ALLOWED_ORIGINS` environment variable. If the variable isn’t set, no cross-origin requests are allowed:
api/server.js
```javascript
import express from 'express';
import cors from 'cors';
const allowedOrigins = (process.env.CORS_ALLOWED_ORIGINS ?? '')
.split(',')
.map((origin) => origin.trim())
.filter(Boolean);
const app = express();
app.use(cors({ origin: allowedOrigins }));
app.get('/', (req, res) => res.sendStatus(200));
app.listen(process.env.PORT ?? 5000);
```
Then set the allowed origins in each environment where the API runs, for example in your hosting platform’s application settings:
* Development
Development environment
```bash
CORS_ALLOWED_ORIGINS=http://localhost:3000,https://localhost:7000
```
* Production
Production environment
```bash
CORS_ALLOWED_ORIGINS=https://myapp.com,https://www.myapp.com
```
* Development
Development environment
```bash
CORS_ALLOWED_ORIGINS=http://localhost:3000,https://localhost:7000
```
* Production
Production environment
```bash
CORS_ALLOWED_ORIGINS=https://myapp.com,https://www.myapp.com
```
## Using Aspire to inject the frontend URL locally
[Section titled “Using Aspire to inject the frontend URL locally”](#using-aspire-to-inject-the-frontend-url-locally)
During local orchestration, you can use Aspire’s environment variable injection to provide the frontend endpoint to your API. Scope this approach to development because the injected endpoint reflects the resource’s network context, which might not be the public origin used by a browser after deployment.
Install the JavaScript hosting integration in your AppHost:
* Aspire CLI
Aspire CLI — Add Aspire.Hosting.JavaScript package
```bash
aspire add javascript
```
The Aspire CLI is interactive, be sure to select the appropriate search result when prompted:
Aspire CLI — Example output prompt
```bash
Select an integration to add:
> javascript (Aspire.Hosting.JavaScript)
> Other results listed as selectable options...
```
Note
If there's only one result, the CLI selects it automatically. You'll still need to confirm the version.
* apphost.cs (C# file-based app)
C# — AppHost.cs
```csharp
#:package Aspire.Hosting.JavaScript@*
```
* PackageReference (\*.csproj)
XML — Add Aspire.Hosting.JavaScript package reference
```xml
```
These examples assume a JavaScript frontend in `../frontend` with a `dev` script in its *package.json*. That script must start an HTTP server listening on the injected `PORT` value. Adjust the paths to match your application; for framework-specific setup, see [JavaScript integration](/integrations/frameworks/javascript/).
* .NET
This example assumes an existing `Api` project. A C# AppHost also needs a project reference to `Api`. Add the frontend and the API to your AppHost:
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const frontend = await builder.addJavaScriptApp('frontend', '../frontend', {
runScriptName: 'dev',
});
await frontend.withHttpEndpoint({ port: 3000, env: 'PORT' });
const api = await builder.addProject('api', '../Api/Api.csproj');
await api.withReference(frontend); // Injects services__frontend__http__0 into the API
await api.withExternalHttpEndpoints();
await builder.build().run();
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
var frontend = builder.AddJavaScriptApp("frontend", "../frontend", "dev")
.WithHttpEndpoint(port: 3000, env: "PORT");
var api = builder.AddProject("api")
.WithReference(frontend) // Injects services__frontend__http__0 into the API
.WithExternalHttpEndpoints();
builder.Build().Run();
```
In the API project, read the injected frontend endpoint for CORS configuration:
Program.cs
```csharp
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(policy =>
{
// Use the Aspire-injected endpoint only during local development.
var frontendUrl = builder.Configuration["services:frontend:http:0"];
if (builder.Environment.IsDevelopment() &&
Uri.TryCreate(frontendUrl, UriKind.Absolute, out var frontendUri))
{
policy.WithOrigins(frontendUri.GetLeftPart(UriPartial.Authority))
.AllowAnyHeader()
.AllowAnyMethod();
}
else
{
// Production configuration must contain the browser-visible public origins.
var allowedOrigins = builder.Configuration
.GetSection("Cors:AllowedOrigins")
.Get() ?? [];
policy.WithOrigins(allowedOrigins)
.AllowAnyHeader()
.AllowAnyMethod();
}
});
});
var app = builder.Build();
app.UseRouting();
app.UseCors();
app.MapDefaultEndpoints();
app.MapGet("/", () => Results.Ok());
app.Run();
```
* Node.js
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const frontend = await builder.addJavaScriptApp('frontend', '../frontend', {
runScriptName: 'dev',
});
await frontend.withHttpEndpoint({ port: 3000, env: 'PORT' });
const api = await builder.addProject('api', '../Api/Api.csproj');
await api.withReference(frontend); // Injects services__frontend__http__0 into the API
await api.withExternalHttpEndpoints();
await builder.build().run();
```
* TypeScript
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
var frontend = builder.AddJavaScriptApp("frontend", "../frontend", "dev")
.WithHttpEndpoint(port: 3000, env: "PORT");
var api = builder.AddProject("api")
.WithReference(frontend) // Injects services__frontend__http__0 into the API
.WithExternalHttpEndpoints();
builder.Build().Run();
```
* C#
This example assumes the Express API from the previous examples in an *api* folder with a *server.js* entry script. Add the frontend and the API to your AppHost. The API listens on the `PORT` value that Aspire assigns:
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const frontend = await builder.addJavaScriptApp('frontend', '../frontend', {
runScriptName: 'dev',
});
await frontend.withHttpEndpoint({ port: 3000, env: 'PORT' });
const api = await builder.addNodeApp('api', '../api', 'server.js');
await api.withHttpEndpoint({ env: 'PORT' });
await api.withReference(frontend); // Injects FRONTEND_HTTP into the API
await api.withExternalHttpEndpoints();
await builder.build().run();
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
var frontend = builder.AddJavaScriptApp("frontend", "../frontend", "dev")
.WithHttpEndpoint(port: 3000, env: "PORT");
var api = builder.AddNodeApp("api", "../api", "server.js")
.WithHttpEndpoint(env: "PORT")
.WithReference(frontend) // Injects FRONTEND_HTTP into the API
.WithExternalHttpEndpoints();
builder.Build().Run();
```
In the API, read the injected frontend endpoint for CORS configuration. Aspire sets `NODE_ENV` to `development` for Node.js apps when the AppHost runs in the Development environment:
api/server.js
```javascript
import express from 'express';
import cors from 'cors';
// Use the Aspire-injected endpoint only during local development.
const frontendUrl = process.env.FRONTEND_HTTP;
let allowedOrigins;
if (process.env.NODE_ENV === 'development' && URL.canParse(frontendUrl)) {
allowedOrigins = [new URL(frontendUrl).origin];
} else {
// Production configuration must contain the browser-visible public origins.
allowedOrigins = (process.env.CORS_ALLOWED_ORIGINS ?? '')
.split(',')
.map((origin) => origin.trim())
.filter(Boolean);
}
const app = express();
app.use(cors({ origin: allowedOrigins }));
app.get('/', (req, res) => res.sendStatus(200));
app.listen(process.env.PORT ?? 5000);
```
Aspire also injects the same endpoint as `services__frontend__http__0`. For more information, see [Service discovery](/fundamentals/service-discovery/).
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const frontend = await builder.addJavaScriptApp('frontend', '../frontend', {
runScriptName: 'dev',
});
await frontend.withHttpEndpoint({ port: 3000, env: 'PORT' });
const api = await builder.addNodeApp('api', '../api', 'server.js');
await api.withHttpEndpoint({ env: 'PORT' });
await api.withReference(frontend); // Injects FRONTEND_HTTP into the API
await api.withExternalHttpEndpoints();
await builder.build().run();
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
var frontend = builder.AddJavaScriptApp("frontend", "../frontend", "dev")
.WithHttpEndpoint(port: 3000, env: "PORT");
var api = builder.AddNodeApp("api", "../api", "server.js")
.WithHttpEndpoint(env: "PORT")
.WithReference(frontend) // Injects FRONTEND_HTTP into the API
.WithExternalHttpEndpoints();
builder.Build().Run();
```
Caution
Don’t use the injected service discovery endpoint as a production CORS origin. Configure each browser-visible public origin explicitly for the deployed environment, in `Cors:AllowedOrigins` for .NET or `CORS_ALLOWED_ORIGINS` for the Node.js examples.
## Common CORS issues and solutions
[Section titled “Common CORS issues and solutions”](#common-cors-issues-and-solutions)
### Issue: CORS error despite configuring CORS middleware
[Section titled “Issue: CORS error despite configuring CORS middleware”](#issue-cors-error-despite-configuring-cors-middleware)
**Symptom**: Browser shows a CORS error even though you’ve added CORS middleware to the API.
* .NET
**Cause**: The order of middleware execution matters. CORS must run **after routing** and **before authorization and endpoint execution**. In minimal `WebApplication` apps, calls such as `MapGet` and `MapDefaultEndpoints` register endpoints; their position relative to `UseCors` doesn’t determine middleware execution order.
**Solution**: Place CORS between routing and endpoint execution. Minimal hosting adds routing automatically when needed; an explicit `UseRouting` makes that ordering visible:
Program.cs
```csharp
app.UseRouting();
app.UseCors();
app.MapGet("/", () => Results.Ok());
```
If you’re using authentication and authorization, place their middleware after CORS so preflight requests can be handled first. This fragment assumes the corresponding authentication and authorization services are already configured:
Program.cs
```csharp
app.UseRouting();
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
app.MapGet("/secure", () => Results.Ok()).RequireAuthorization();
```
* Node.js
**Cause**: The order of middleware registration matters. Express runs middleware and routes in the order you register them, so a route or middleware registered before `cors()` can respond without CORS headers.
**Solution**: Register the `cors` middleware before your routes:
api/server.js
```javascript
app.use(cors({ origin: allowedOrigins }));
app.get('/', (req, res) => res.sendStatus(200));
```
If you’re using authentication middleware, register it after CORS so preflight requests can be handled first. This fragment assumes existing `authenticate` and `requireAuth` middleware functions:
api/server.js
```javascript
app.use(cors({ origin: allowedOrigins }));
app.use(authenticate);
app.get('/secure', requireAuth, (req, res) => res.sendStatus(200));
```
### Issue: Preflight requests failing
[Section titled “Issue: Preflight requests failing”](#issue-preflight-requests-failing)
**Symptom**: The browser blocks a request after its preflight `OPTIONS` request fails.
* .NET
`OPTIONS` requests return 405 Method Not Allowed.
**Cause**: The API doesn’t handle `OPTIONS` requests, which browsers use for CORS preflight checks.
**Solution**: `UseCors()` handles recognized preflight requests automatically. Ensure an applicable policy allows the request’s origin, method, and headers, and that CORS runs after routing and before authorization and endpoint execution.
* Node.js
`OPTIONS` requests return 200 OK, but the response has no `Access-Control-Allow-*` headers.
**Cause**: The `cors` middleware doesn’t run for `OPTIONS` requests. Express answers an `OPTIONS` request for a route automatically, but that response doesn’t include CORS headers.
**Solution**: When you register it for all routes with `app.use(cors(...))`, the `cors` middleware responds to preflight requests automatically. If you apply `cors()` to individual routes instead, also register it for the matching `OPTIONS` requests with `app.options(...)`. Ensure the configuration allows the request’s origin, method, and headers, and that the middleware runs before authentication and your routes.
### Issue: Credentials not sent with cross-origin requests
[Section titled “Issue: Credentials not sent with cross-origin requests”](#issue-credentials-not-sent-with-cross-origin-requests)
**Symptom**: Cookies aren’t included in cross-origin requests.
**Cause**: By default, browser `fetch` requests don’t include cookies on cross-origin requests.
**Solution**: Enable credentials on both the server and the client. In the API’s existing CORS configuration, use an explicit origin allowlist:
* .NET
Program.cs
```csharp
options.AddDefaultPolicy(policy =>
{
policy.WithOrigins("https://myapp.com")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials(); // Enable credentials
});
```
* Node.js
api/server.js
```javascript
app.use(
cors({
origin: 'https://myapp.com',
credentials: true, // Enable credentials
})
);
```
In a browser JavaScript module (or inside an async function), opt into credentials. Replace the API URL with your application’s browser-visible URL:
frontend.js
```javascript
const response = await fetch('https://api.myapp.com/', {
credentials: 'include',
});
```
Cookie policies still apply: cross-site cookies generally need `SameSite=None` and `Secure`, and browser third-party-cookie restrictions can prevent their delivery.
An explicitly supplied bearer `Authorization` header is different: allow that header in the CORS policy and handle its preflight. It doesn’t inherently require `AllowCredentials()` (or `credentials: true` in the Node.js `cors` middleware); add the header explicitly in the client request.
Caution
When allowing credentials, you must specify exact origins. In .NET, using `AllowAnyOrigin()` together with `AllowCredentials()` causes an error. In Node.js, browsers reject credentialed responses when the `cors` middleware is configured with `origin: '*'`.
### Issue: CORS errors in production but not development
[Section titled “Issue: CORS errors in production but not development”](#issue-cors-errors-in-production-but-not-development)
**Symptom**: CORS works locally but fails in production.
**Cause**: The production origin (for example, `https://myapp.com`) is not in the allowed origins list, or the environment-specific configuration isn’t deployed.
**Solution**: Verify the allowed origins in your production configuration match the exact origin your frontend uses, including the scheme (`https://`) and any subdomain.
## Dashboard CORS configuration
[Section titled “Dashboard CORS configuration”](#dashboard-cors-configuration)
The Aspire dashboard collects OpenTelemetry data from browser apps (browser telemetry). When a browser app runs on a different origin than the dashboard — for example, on a custom localhost domain — the dashboard must allow that origin so the browser can post telemetry to it.
In the AppHost, set the `ASPIRE_DASHBOARD_CORS_ALLOWED_ORIGINS` environment variable to a comma-separated list of allowed origins:
```plaintext
ASPIRE_DASHBOARD_CORS_ALLOWED_ORIGINS=https://myapp.com,https://www.myapp.com
```
You can also use the `*` wildcard to allow any domain (development only):
```plaintext
ASPIRE_DASHBOARD_CORS_ALLOWED_ORIGINS=*
```
Note
`ASPIRE_DASHBOARD_CORS_ALLOWED_ORIGINS` is the AppHost-facing setting. When you run the dashboard as a standalone container, configure the equivalent `DASHBOARD__OTLP__CORS__ALLOWEDORIGINS` setting instead. For more information, see [Enable browser telemetry](/dashboard/enable-browser-telemetry/).
For more information, see [Dashboard configuration](/dashboard/configuration/).
## See also
[Section titled “See also”](#see-also)
* [Inner-loop networking overview](/fundamentals/networking-overview/)
* [Service defaults](/get-started/csharp-service-defaults/)
* [JavaScript integration](/integrations/frameworks/javascript/)
* [Dashboard configuration](/dashboard/configuration/)
* [Enable browser telemetry](/dashboard/enable-browser-telemetry/)
* [Cross-Origin Resource Sharing (CORS) in ASP.NET Core](https://learn.microsoft.com/aspnet/core/security/cors)
* [Express `cors` middleware](https://github.com/expressjs/cors)
# Docker Compose to Aspire AppHost reference
> Quick reference for converting Docker Compose YAML syntax to Aspire AppHost API calls — services, networks, volumes, environment variables, and health checks.
This reference provides systematic mappings from Docker Compose YAML syntax to equivalent Aspire AppHost API calls. Use these tables as a quick reference when converting your existing Docker Compose files to Aspire application host configurations.
## Service definitions
[Section titled “Service definitions”](#service-definitions)
| Docker Compose | Aspire | Notes |
| --------------- | ---------------------------------------------------------- | ------------------------------------------------------------------- |
| `services:` | `var builder = DistributedApplication.CreateBuilder(args)` | Root application builder used for adding and representing resources |
| `service_name:` | `builder.Add*("service_name")` | Service name becomes resource name |
Learn more about [Docker Compose services](https://docs.docker.com/compose/compose-file/05-services/).
## Images and builds
[Section titled “Images and builds”](#images-and-builds)
| Docker Compose | Aspire | Notes |
| ------------------------------------- | --------------------------------------------------------------------------- | ----------------------------------------- |
| `image: nginx:latest` | `builder.AddContainer("name", "nginx", "latest")` | Direct image reference |
| `build: .` | `builder.AddDockerfile("name", ".")` | Build from Dockerfile |
| `build: ./path` | `builder.AddDockerfile("name", "./path")` | Build from specific path |
| `build.context: ./app` | `builder.AddDockerfile("name", "./app")` | Build context |
| `build.dockerfile: Custom.dockerfile` | `builder.Add*("name").WithDockerfile("Custom.dockerfile")` | Custom Dockerfile name |
| Generated Dockerfile | `builder.AddDockerfileBuilder("name", "./app", callback, stage: "runtime")` | Generate the Dockerfile from AppHost code |
Learn more about [Docker Compose build reference](https://docs.docker.com/compose/compose-file/build/) and [WithDockerfile](/app-host/withdockerfile/).
## Pull policy
[Section titled “Pull policy”](#pull-policy)
| Docker Compose | Aspire | Notes |
| ---------------------- | ----------------------------------------------- | -------------------------------- |
| `pull_policy: always` | `.WithImagePullPolicy(ImagePullPolicy.Always)` | Always pull the image |
| `pull_policy: missing` | `.WithImagePullPolicy(ImagePullPolicy.Missing)` | Pull only if not present locally |
| `pull_policy: never` | `.WithImagePullPolicy(ImagePullPolicy.Never)` | Never pull from registry |
Learn more about [Docker Compose pull\_policy](https://docs.docker.com/reference/compose-file/services/#pull_policy) and [image pull policy](/integrations/compute/docker/#configure-image-pull-policy).
## .NET projects
[Section titled “.NET projects”](#net-projects)
| Docker Compose | Aspire | Notes |
| --------------------------- | --------------------------------------------- | ----------------------------- |
| `build: ./MyApi` (for .NET) | `builder.AddProject("myapi")` | Direct .NET project reference |
Learn more about [adding .NET projects](/get-started/app-host/).
## Port mappings
[Section titled “Port mappings”](#port-mappings)
| Docker Compose | Aspire | Notes |
| -------------------- | ------------------------------------------------ | ----------------------------------------------------------------------------- |
| `ports: ["8080:80"]` | `.WithHttpEndpoint(port: 8080, targetPort: 80)` | HTTP endpoint mapping. Ports are optional; dynamic ports are used if omitted |
| `ports: ["443:443"]` | `.WithHttpsEndpoint(port: 443, targetPort: 443)` | HTTPS endpoint mapping. Ports are optional; dynamic ports are used if omitted |
| `expose: ["8080"]` | `.WithEndpoint(port: 8080)` | Internal port exposure. Ports are optional; dynamic ports are used if omitted |
Learn more about [Docker Compose ports](https://docs.docker.com/compose/compose-file/05-services/#ports) and [endpoint configuration](/fundamentals/networking-overview/).
## Environment variables
[Section titled “Environment variables”](#environment-variables)
| Docker Compose | Aspire / Notes |
| ------------------------------ | --------------------------------------------------------------------------------------------------------------------------- |
| `environment: KEY=value` | `.WithEnvironment("KEY", "value")` Static environment variable |
| `environment: KEY=${HOST_VAR}` | `.WithEnvironment(context => context.EnvironmentVariables["KEY"] = hostVar)` Environment variable with callback context |
| `environment: KEY=${PARAM}` | `.AsEnvironmentPlaceholder(resource)` inside `.PublishAsDockerComposeService(...)` Compose environment variable placeholder |
| `env_file: .env` | `.ConfigureEnvFile(env => { ... })` Environment file customization (available in 13.1+) |
Learn more about [Docker Compose environment](https://docs.docker.com/compose/compose-file/05-services/#environment) and [external parameters](/fundamentals/external-parameters/).
## Volumes and storage
[Section titled “Volumes and storage”](#volumes-and-storage)
| Docker Compose | Aspire | Notes |
| -------------------------------- | ------------------------------------------------------ | -------------------- |
| `volumes: ["data:/app/data"]` | `.WithVolume("data", "/app/data")` | Named volume |
| `volumes: ["./host:/container"]` | `.WithBindMount("./host", "/container")` | Bind mount |
| `volumes: ["./config:/app:ro"]` | `.WithBindMount("./config", "/app", isReadOnly: true)` | Read-only bind mount |
Learn more about [Docker Compose volumes](https://docs.docker.com/compose/compose-file/05-services/#volumes) and [persist container data](/fundamentals/persist-data-volumes/).
## Dependencies and ordering
[Section titled “Dependencies and ordering”](#dependencies-and-ordering)
| Docker Compose | Aspire | Notes |
| -------------------------------------------- | ------------------------ | --------------------------------------------------- |
| `depends_on: [db]` | `.WithReference(db)` | Service dependency with connection string injection |
| `depends_on: db: condition: service_started` | `.WaitFor(db)` | Wait for service start |
| `depends_on: db: condition: service_healthy` | `.WaitForCompletion(db)` | Wait for health check to pass |
Learn more about [Docker Compose depends\_on](https://docs.docker.com/compose/compose-file/05-services/#depends_on) and [launch profiles](/integrations/dotnet/launch-profiles/).
## Networks
[Section titled “Networks”](#networks)
| Docker Compose | Aspire | Notes |
| -------------------------------------- | ---------------------------------------- | --------------------------------------------------------------------------- |
| `networks: [backend]` | Automatic | Aspire handles networking automatically |
| Service host name on a Compose network | `.GetHostAddressExpression(endpoint)` | Produces the generated Docker Compose service host name for an endpoint |
| Custom networks | `.ConfigureComposeFile(file => { ... })` | Customize the generated Compose file when automatic networking isn’t enough |
Learn more about [Docker Compose networks](https://docs.docker.com/compose/compose-file/05-services/#networks) and [service discovery](/fundamentals/service-discovery/).
## Resource limits
[Section titled “Resource limits”](#resource-limits)
| Docker Compose | Aspire | Notes |
| -------------------------------------- | ------------- | ------------------------------------------ |
| `deploy.resources.limits.memory: 512m` | Not supported | Resource limits aren’t supported in Aspire |
| `deploy.resources.limits.cpus: 0.5` | Not supported | Resource limits aren’t supported in Aspire |
Learn more about [Docker Compose deploy reference](https://docs.docker.com/compose/compose-file/deploy/).
## Health checks
[Section titled “Health checks”](#health-checks)
| Docker Compose | Aspire | Notes |
| -------------------------------------------------------------- | --------------------------- | -------------------------------------------------- |
| `healthcheck.test: ["CMD", "curl", "http://localhost/health"]` | Built-in for integrations | Aspire integrations include health checks |
| `healthcheck.interval: 30s` | Configurable in integration | Health check configuration varies by resource type |
Learn more about [Docker Compose healthcheck](https://docs.docker.com/compose/compose-file/05-services/#healthcheck) and [health checks](/fundamentals/health-checks/).
## Restart policies
[Section titled “Restart policies”](#restart-policies)
| Docker Compose | Aspire | Notes |
| ------------------------- | ------------------------------------------------------------------------------------------- | ---------------------------------------------- |
| `restart: unless-stopped` | `.PublishAsDockerComposeService((resource, service) => service.Restart = "unless-stopped")` | Customize the generated Docker Compose service |
| `restart: always` | `.PublishAsDockerComposeService((resource, service) => service.Restart = "always")` | Customize the generated Docker Compose service |
| `restart: no` | Default | No restart policy |
Learn more about [Docker Compose restart](https://docs.docker.com/compose/compose-file/05-services/#restart).
## Logging
[Section titled “Logging”](#logging)
| Docker Compose | Aspire | Notes |
| ------------------------------- | ----------------------- | ---------------------------------- |
| `logging.driver: json-file` | Built-in | Aspire provides integrated logging |
| `logging.options.max-size: 10m` | Dashboard configuration | Managed through Aspire dashboard |
Learn more about [Docker Compose logging](https://docs.docker.com/compose/compose-file/05-services/#logging) and [telemetry](/fundamentals/telemetry/).
## Database services
[Section titled “Database services”](#database-services)
| Docker Compose | Aspire | Notes |
| --------------------- | ----------------------------- | --------------------------------------- |
| `image: postgres:15` | `builder.AddPostgres("name")` | PostgreSQL with automatic configuration |
| `image: mysql:8` | `builder.AddMySql("name")` | MySQL with automatic configuration |
| `image: redis:7` | `builder.AddRedis("name")` | Redis with automatic configuration |
| `image: mongo:latest` | `builder.AddMongoDB("name")` | MongoDB with automatic configuration |
Learn more about [Docker Compose services](https://docs.docker.com/compose/compose-file/05-services/) and [database integrations](/integrations/gallery/?search=database).
## See also
[Section titled “See also”](#see-also)
* [Migrate from Docker Compose to Aspire](/app-host/migrate-from-docker-compose/)
* [AppHost overview](/get-started/app-host/)
* [WithDockerfile](/app-host/withdockerfile/)
# AppHost eventing APIs
> Use the Aspire AppHost eventing APIs for lifecycle events, custom event publishing, and reactive integrations that respond to resource state transitions at runtime.
In Aspire, eventing allows you to publish and subscribe to events during various AppHost life cycles. Eventing is more flexible than life cycle events. Both let you run arbitrary code during event callbacks, but eventing offers finer control of event timing, publishing, and provides supports for custom events.
## AppHost eventing
[Section titled “AppHost eventing”](#apphost-eventing)
The following events are available in the AppHost and occur in the following order:
1. `BeforeStartEvent`: This event is raised before the AppHost starts.
2. `ResourceEndpointsAllocatedEvent`: This event is raised per resource after its endpoints are allocated.
3. `AfterResourcesCreatedEvent`: This event is raised after resources are created.
### Subscribe to AppHost events
[Section titled “Subscribe to AppHost events”](#subscribe-to-apphost-events)
To subscribe to built-in AppHost events, use the typed API available for each event. C# provides builder extension methods for selected events and the lower-level `Eventing.Subscribe()` API for others. TypeScript provides named subscription methods:
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
await builder.subscribeBeforeStart(async () => {
console.log('BeforeStartEvent');
});
await builder.subscribeAfterResourcesCreated(async () => {
console.log('AfterResourcesCreatedEvent');
});
await builder.build().run();
```
* C#
AppHost.cs
```csharp
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
var builder = DistributedApplication.CreateBuilder(args);
builder.OnBeforeStart(static (@event, cancellationToken) =>
{
var logger = @event.Services.GetRequiredService>();
logger.LogInformation("BeforeStartEvent");
return Task.CompletedTask;
});
builder.Eventing.Subscribe(
static (@event, cancellationToken) =>
{
var logger = @event.Services.GetRequiredService>();
logger.LogInformation("AfterResourcesCreatedEvent");
return Task.CompletedTask;
});
builder.Build().Run();
```
The following C# builder-level helper methods are available for AppHost events:
| Method | Event |
| ----------------- | ---------------------------------------------------------------- |
| `OnBeforeStart` | `BeforeStartEvent` — raised before the AppHost starts |
| `OnBeforePublish` | `BeforePublishEvent` — raised before manifest publishing begins |
| `OnAfterPublish` | `AfterPublishEvent` — raised after manifest publishing completes |
For the full API surface, see the [C# `DistributedApplicationEventingExtensions` API reference](/reference/api/csharp/aspire.hosting/distributedapplicationeventingextensions/) and the [TypeScript `Aspire.Hosting` API reference](/reference/api/typescript/aspire.hosting/).
If you need to subscribe via `IDistributedApplicationEventing` directly (for example, inside an `IDistributedApplicationEventingSubscriber`), you can use the lower-level `Eventing.Subscribe()` API:
AppHost.cs
```csharp
builder.Eventing.Subscribe(
static (@event, cancellationToken) =>
{
var logger = @event.Services.GetRequiredService>();
logger.LogInformation("BeforeStartEvent");
return Task.CompletedTask;
});
builder.Eventing.Subscribe(
static (@event, cancellationToken) =>
{
var logger = @event.Services.GetRequiredService>();
logger.LogInformation("AfterResourcesCreatedEvent");
return Task.CompletedTask;
});
```
Note
TypeScript AppHosts don’t expose the generic `builder.Eventing.Subscribe()` API. Use the named callback APIs generated for each supported event, such as `subscribeBeforeStart`, `subscribeAfterResourcesCreated`, `addEventingSubscriber`, and resource callbacks like [`OnResourceReady`](/reference/api/csharp/aspire.hosting/distributedapplicationeventingextensions/methods/#onresourceready "Subscribes a callback to the Aspire.Hosting.ApplicationModel.ResourceReadyEvent event for builder.")[`onResourceReady`](/reference/api/typescript/aspire.hosting/onresourceready/ "Subscribes to the ResourceReady event.").
Caution
`Eventing.Subscribe()` requires `T` to be a concrete event type. Subscribing to an interface or abstract class throws an `ArgumentException`. For `IDistributedApplicationEvent`, the exception message is `Cannot subscribe to interface or abstract type 'IDistributedApplicationEvent'. Subscribe to a concrete event type instead.` Use a concrete event type such as `BeforeStartEvent` or `AfterResourcesCreatedEvent`.
When the AppHost is run, by the time the Aspire dashboard is displayed, you should see the following log output in the console:
```plaintext
info: Program[0]
BeforeStartEvent
info: Aspire.Hosting.DistributedApplication[0]
Aspire version: 13.6.0
info: Aspire.Hosting.DistributedApplication[0]
Distributed application starting.
info: Aspire.Hosting.DistributedApplication[0]
Application host directory is: ../AspireApp/AspireApp.AppHost
info: Aspire.Hosting.DistributedApplication[0]
Now listening on: https://localhost:17178
info: Aspire.Hosting.DistributedApplication[0]
Login to the dashboard at https://localhost:17178/login?t=
info: Program[0]
AfterResourcesCreatedEvent
info: Aspire.Hosting.DistributedApplication[0]
Distributed application started. Press Ctrl+C to shut down.
```
The log output confirms that event handlers are executed in the order of the AppHost life cycle events. The subscription order doesn’t affect execution order. The `BeforeStartEvent` is triggered before the AppHost starts, and `AfterResourcesCreatedEvent` is triggered after resources are created.
## Resource eventing
[Section titled “Resource eventing”](#resource-eventing)
In addition to the AppHost events, you can also subscribe to resource events. Resource events are raised specific to an individual resource. Resource events are defined as implementations of the `IDistributedApplicationResourceEvent` interface. The following resource events are available in the listed order:
1. `InitializeResourceEvent`: Raised by orchestrators to signal to resources that they should initialize themselves.
2. `ResourceEndpointsAllocatedEvent`: Raised when the orchestrator allocates endpoints for a resource.
3. `ConnectionStringAvailableEvent`: Raised when a connection string becomes available for a resource.
4. `BeforeResourceStartedEvent`: Raised before the orchestrator starts a new resource.
5. `ResourceReadyEvent`: Raised when a resource initially transitions to a ready state.
### Subscribe to resource events
[Section titled “Subscribe to resource events”](#subscribe-to-resource-events)
To subscribe to resource events, use the convenience-based extension methods. After you have a distributed application builder instance, and a resource builder, walk up to the instance and chain a call to the desired event API:
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const cache = await builder.addRedis('cache');
await cache.onInitializeResource(async () => {
console.log('1. onInitializeResource');
});
await cache.onResourceEndpointsAllocated(async (event) => {
const resource = await event.resource();
console.log(`2. endpoints allocated for ${resource.getResourceName()}`);
});
await cache.onConnectionStringAvailable(async (event) => {
const resource = await event.resource();
console.log(
`3. connection string available for ${resource.getResourceName()}`
);
});
await cache.onBeforeResourceStarted(async () => {
console.log('4. onBeforeResourceStarted');
});
await cache.onResourceReady(async () => {
console.log('5. onResourceReady');
});
await builder.build().run();
```
* C#
AppHost.cs
```csharp
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
var builder = DistributedApplication.CreateBuilder(args);
var cache = builder.AddRedis("cache");
cache.OnResourceReady(static (resource, @event, cancellationToken) =>
{
var logger = @event.Services.GetRequiredService>();
logger.LogInformation("5. OnResourceReady");
return Task.CompletedTask;
});
cache.OnInitializeResource(
static (resource, @event, cancellationToken) =>
{
var logger = @event.Services.GetRequiredService>();
logger.LogInformation("1. OnInitializeResource");
return Task.CompletedTask;
});
cache.OnBeforeResourceStarted(
static (resource, @event, cancellationToken) =>
{
var logger = @event.Services.GetRequiredService>();
logger.LogInformation("4. OnBeforeResourceStarted");
return Task.CompletedTask;
});
cache.OnResourceEndpointsAllocated(
static (resource, @event, cancellationToken) =>
{
var logger = @event.Services.GetRequiredService>();
logger.LogInformation("2. OnResourceEndpointsAllocated");
return Task.CompletedTask;
});
cache.OnConnectionStringAvailable(
static (resource, @event, cancellationToken) =>
{
var logger = @event.Services.GetRequiredService>();
logger.LogInformation("3. OnConnectionStringAvailable");
return Task.CompletedTask;
});
var apiService = builder.AddProject("apiservice");
builder.AddProject("webfrontend")
.WithExternalHttpEndpoints()
.WithReference(cache)
.WaitFor(cache)
.WithReference(apiService)
.WaitFor(apiService);
builder.Build().Run();
```
The preceding code subscribes to the `InitializeResourceEvent`, `ResourceReadyEvent`, `ResourceEndpointsAllocatedEvent`, `ConnectionStringAvailableEvent`, and `BeforeResourceStartedEvent` events on the `cache` resource. Chain calls to the event methods to subscribe to multiple events on the same resource.
Note
TypeScript callbacks receive the event object. Use `await event.resource()` and `await event.services()` when you need the resource or service provider.
* [`OnInitializeResource`](/reference/api/csharp/aspire.hosting/distributedapplicationeventingextensions/methods/#oninitializeresource "Subscribes a callback to the Aspire.Hosting.ApplicationModel.InitializeResourceEvent event for builder.")[`onInitializeResource`](/reference/api/typescript/aspire.hosting/oninitializeresource/ "Subscribes to the InitializeResource event."): Subscribes to the `InitializeResourceEvent`.
* [`OnResourceEndpointsAllocated`](/reference/api/csharp/aspire.hosting/distributedapplicationeventingextensions/methods/#onresourceendpointsallocated "Subscribes a callback to the Aspire.Hosting.ApplicationModel.ResourceEndpointsAllocatedEvent event for builder.")[`onResourceEndpointsAllocated`](/reference/api/typescript/aspire.hosting/onresourceendpointsallocated/ "Subscribes to the ResourceEndpointsAllocated event."): Subscribes to the `ResourceEndpointsAllocatedEvent` event.
* [`OnConnectionStringAvailable`](/reference/api/csharp/aspire.hosting/distributedapplicationeventingextensions/methods/#onconnectionstringavailable "Subscribes a callback to the Aspire.Hosting.ApplicationModel.ConnectionStringAvailableEvent event for builder.")[`onConnectionStringAvailable`](/reference/api/typescript/aspire.hosting/onconnectionstringavailable/ "Subscribes to the ConnectionStringAvailable event."): Subscribes to the `ConnectionStringAvailableEvent` event.
* [`OnBeforeResourceStarted`](/reference/api/csharp/aspire.hosting/distributedapplicationeventingextensions/methods/#onbeforeresourcestarted "Subscribes a callback to the Aspire.Hosting.ApplicationModel.BeforeResourceStartedEvent event within the AppHost.")[`onBeforeResourceStarted`](/reference/api/typescript/aspire.hosting/onbeforeresourcestarted/ "Subscribes to the BeforeResourceStarted event."): Subscribes to the `BeforeResourceStartedEvent` event.
* `OnResourceReady` / `onResourceReady`: Subscribes to the `ResourceReadyEvent` event.
* [`OnResourceStopped`](/reference/api/csharp/aspire.hosting/distributedapplicationeventingextensions/methods/#onresourcestopped "Subscribes a callback to the Aspire.Hosting.ApplicationModel.ResourceStoppedEvent event for builder.")[`onResourceStopped`](/reference/api/typescript/aspire.hosting/onresourcestopped/ "Subscribes to the ResourceStopped event."): Subscribes to the `ResourceStoppedEvent` event.
When the AppHost is run, by the time the Aspire dashboard is displayed, you should see the following log output in the console:
```plaintext
info: Aspire.Hosting.DistributedApplication[0]
Aspire version: 13.6.0
info: Aspire.Hosting.DistributedApplication[0]
Distributed application starting.
info: Aspire.Hosting.DistributedApplication[0]
Application host directory is: ../AspireApp/AspireApp.AppHost
info: Program[0]
1. OnInitializeResource
info: Program[0]
2. OnResourceEndpointsAllocated
info: Program[0]
3. OnConnectionStringAvailable
info: Program[0]
4. OnBeforeResourceStarted
info: Aspire.Hosting.DistributedApplication[0]
Now listening on: https://localhost:17222
info: Aspire.Hosting.DistributedApplication[0]
Login to the dashboard at https://localhost:17222/login?t=
info: Program[0]
5. OnResourceReady
info: Aspire.Hosting.DistributedApplication[0]
Distributed application started. Press Ctrl+C to shut down.
```
Note
Some events block execution. For example, when the `BeforeResourceStartedEvent` is published, the resource startup blocks until all subscriptions for that event on a given resource finish executing. Whether an event blocks or not depends on how you publish it (see the following section).
## Publish events
[Section titled “Publish events”](#publish-events)
When subscribing to any of the built-in events, you don’t need to publish the event yourself as the AppHost orchestrator manages to publish built-in events on your behalf. However, you can publish custom events with the eventing API. To publish an event, you have to first define an event as an implementation of either the `IDistributedApplicationEvent` or `IDistributedApplicationResourceEvent` interface. You need to determine which interface to implement based on whether the event is a global AppHost event or a resource-specific event.
Then, you can subscribe and publish the event by calling the either of the following APIs:
* `PublishAsync(T, CancellationToken)`: Publishes an event to all subscribes of the specific event type.
* `PublishAsync(T, EventDispatchBehavior, CancellationToken)`: Publishes an event to all subscribes of the specific event type with a specified dispatch behavior.
### Provide an `EventDispatchBehavior`
[Section titled “Provide an EventDispatchBehavior”](#provide-an-eventdispatchbehavior)
When events are dispatched, you can control how the events are dispatched to subscribers. The event dispatch behavior is specified with the `EventDispatchBehavior` enum. The following behaviors are available:
* `EventDispatchBehavior.BlockingSequential`: Fires events sequentially and blocks until they’re all processed.
* `EventDispatchBehavior.BlockingConcurrent`: Fires events concurrently and blocks until they’re all processed.
* `EventDispatchBehavior.NonBlockingSequential`: Fires events sequentially but doesn’t block.
* `EventDispatchBehavior.NonBlockingConcurrent`: Fires events concurrently but doesn’t block.
The default behavior is `EventDispatchBehavior.BlockingSequential`. To override this behavior, when calling a publishing API such as `PublishAsync`, provide the desired behavior as an argument.
## Eventing subscribers
[Section titled “Eventing subscribers”](#eventing-subscribers)
In some cases, such as extension libraries, you may need to access lifecycle events from a service rather than directly from the Aspire application model. You can implement `IDistributedApplicationEventingSubscriber` and register the service with `AddEventingSubscriber` (or `TryAddEventingSubscriber` if you want to avoid duplicate registrations).
AppHost.cs
```csharp
using Aspire.Hosting.Eventing;
using Aspire.Hosting.Lifecycle;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
var builder = DistributedApplication.CreateBuilder(args);
builder.Services.AddEventingSubscriber();
builder.Build().Run();
internal sealed class LifecycleLoggerSubscriber(ILogger logger)
: IDistributedApplicationEventingSubscriber
{
public Task SubscribeAsync(
IDistributedApplicationEventing eventing,
DistributedApplicationExecutionContext executionContext,
CancellationToken cancellationToken)
{
eventing.Subscribe((@event, ct) =>
{
logger.LogInformation("1. BeforeStartEvent");
return Task.CompletedTask;
});
eventing.Subscribe((@event, ct) =>
{
logger.LogInformation("2. {Resource} ResourceEndpointsAllocatedEvent", @event.Resource.Name);
return Task.CompletedTask;
});
eventing.Subscribe((@event, ct) =>
{
logger.LogInformation("3. AfterResourcesCreatedEvent");
return Task.CompletedTask;
});
return Task.CompletedTask;
}
}
```
Note
C# eventing subscriber service classes are specific to the C# AppHost hosting model. TypeScript AppHosts can register callback-based subscribers with `addEventingSubscriber` or `tryAddEventingSubscriber`.
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
builder.addEventingSubscriber(async (events) => {
events.onBeforeStart(async () => {
console.log('1. BeforeStartEvent');
});
events.onAfterResourcesCreated(async () => {
console.log('3. AfterResourcesCreatedEvent');
});
});
await builder.build().run();
```
The subscriber approach keeps builder code minimal while still letting you respond to the same lifecycle moments as inline subscriptions:
* `AddEventingSubscriber()` (or `TryAddEventingSubscriber()`) ensures the subscriber participates whenever the AppHost starts.
* `SubscribeAsync` is called once per AppHost execution, giving you access to `IDistributedApplicationEventing` and the `DistributedApplicationExecutionContext` should you need model- or environment-specific data.
* You can register handlers for any built-in event (AppHost or resource) or for your own custom `IDistributedApplicationEvent` types.
Use this pattern whenever you previously relied on `IDistributedApplicationLifecycleHook`. The lifecycle hook APIs remain only for backward compatibility and will be removed in a future release.
### Migrating from lifecycle hooks
[Section titled “Migrating from lifecycle hooks”](#migrating-from-lifecycle-hooks)
If you’re migrating from the deprecated `IDistributedApplicationLifecycleHook` interface, use the following mapping:
| Old pattern (deprecated) | New pattern |
| -------------------------------- | ---------------------------------------------- |
| `BeforeStartAsync()` | Subscribe to `BeforeStartEvent` |
| `AfterEndpointsAllocatedAsync()` | Subscribe to `ResourceEndpointsAllocatedEvent` |
| `AfterResourcesCreatedAsync()` | Subscribe to `AfterResourcesCreatedEvent` |
| `TryAddLifecycleHook()` | `TryAddEventingSubscriber()` |
**Before (deprecated):**
OldLifecycleHook.cs
```csharp
public class MyHook : IDistributedApplicationLifecycleHook
{
public Task AfterResourcesCreatedAsync(
DistributedApplicationModel model,
CancellationToken cancellationToken)
{
// Handle event
return Task.CompletedTask;
}
}
// Registration
builder.Services.TryAddLifecycleHook();
```
**After (recommended):**
NewEventingSubscriber.cs
```csharp
public class MySubscriber : IDistributedApplicationEventingSubscriber
{
public Task SubscribeAsync(
IDistributedApplicationEventing eventing,
DistributedApplicationExecutionContext context,
CancellationToken cancellationToken)
{
eventing.Subscribe((@event, ct) =>
{
// Handle event using context.Model
return Task.CompletedTask;
});
return Task.CompletedTask;
}
}
// Registration
builder.Services.TryAddEventingSubscriber();
```
Caution
The `IDistributedApplicationLifecycleHook` interface is deprecated as of Aspire 9.0 and will be removed in a future release. Migrate to `IDistributedApplicationEventingSubscriber` for new code.
## Additional events
[Section titled “Additional events”](#additional-events)
Beyond the core lifecycle events, Aspire provides additional events for specific scenarios:
### Publishing events
[Section titled “Publishing events”](#publishing-events)
When publishing your application (generating deployment manifests), these events are raised:
| Event | When raised | Purpose |
| -------------------- | -------------------------- | ------------------------------------------------------- |
| `BeforePublishEvent` | Before publishing begins | Validate or modify resources before manifest generation |
| `AfterPublishEvent` | After publishing completes | Perform cleanup or post-publish actions |
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
await builder.subscribeBeforePublish(async (event) => {
const model = await event.model();
console.log(`Publishing ${model.getResources().length} resources`);
});
await builder.subscribeAfterPublish(async (event) => {
const services = await event.services();
console.log('Publish completed', services);
});
```
* C#
AppHost.cs
```csharp
builder.OnBeforePublish((@event, ct) =>
{
// Validate resources before publishing
return Task.CompletedTask;
});
builder.OnAfterPublish((@event, ct) =>
{
// Post-publish actions
return Task.CompletedTask;
});
```
For details on what happens during publishing, see [Publishing and deployment overview](/deployment/deploy-with-aspire/).
### Resource stopped event
[Section titled “Resource stopped event”](#resource-stopped-event)
The `ResourceStoppedEvent` is raised once each time a resource stops execution. If a handler restarts the resource, the restart doesn’t raise another event for the same stop. The event is raised again only when the resource subsequently stops:
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const cache = await builder.addRedis('cache');
await cache.onResourceStopped(async (event) => {
const resource = await event.resource();
console.log(`Resource ${resource.getResourceName()} stopped`);
});
```
* C#
AppHost.cs
```csharp
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
var builder = DistributedApplication.CreateBuilder(args);
var cache = builder.AddRedis("cache");
cache.OnResourceStopped(
static (resource, @event, ct) =>
{
var logger = @event.Services.GetRequiredService>();
logger.LogInformation("Resource {Name} stopped", resource.Name);
return Task.CompletedTask;
});
builder.Build().Run();
```
Note
Event publishing is **synchronous and blocking** — event handlers can delay further execution. Keep handlers lightweight and avoid long-running operations.
## See also
[Section titled “See also”](#see-also)
* [Custom resources](/extensibility/custom-resources/)
* [Resource annotations](/fundamentals/annotations-overview/)
# Host external executables in Aspire
> Host external executable applications in your Aspire AppHost using AddExecutable — model CLI tools, daemons, and language runtimes alongside containers and projects.
In Aspire, you can host external executable applications alongside your projects using the [`AddExecutable`](/reference/api/csharp/aspire.hosting/executableresourcebuilderextensions/methods/#addexecutable "Adds an executable resource to the application model.")[`addExecutable`](/reference/api/typescript/aspire.hosting/addexecutable/ "Adds an executable resource to the application model.") method. This capability is useful when you need to integrate executable applications or tools into your distributed application, such as Node.js applications, Python scripts, or specialized CLI tools.
## When to use executable resources
[Section titled “When to use executable resources”](#when-to-use-executable-resources)
Use executable resources when you need to:
* Run applications or tools directly on the host instead of in a container.
* Integrate command-line tools or utilities into your application.
* Run external processes that other resources depend on.
* Develop with tools that provide local development servers.
Common examples include:
* **Frontend development servers**: Tools like [Vercel CLI](https://vercel.com/docs/cli) or webpack dev server.
* **Language-specific applications**: Node.js apps, Python scripts, or Go applications.
* **Database tools**: Migration utilities or database seeders.
* **Build tools**: Asset processors or code generators.
## Basic usage
[Section titled “Basic usage”](#basic-usage)
The `AddExecutable` method requires a resource name, the executable path, and optionally command-line arguments and a working directory:
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
// Basic executable without arguments
const nodeApp = await builder.addExecutable("frontend", "node", ".", ["server.js"]);
// Executable with command-line arguments
const pythonApp = await builder.addExecutable("api", "python", ".", ["-m", "uvicorn", "main:app", "--reload", "--host", "0.0.0.0", "--port", "8000"]);
await builder.build().run();
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
// Basic executable without arguments
var nodeApp = builder.AddExecutable("frontend", "node", ".", "server.js");
// Executable with command-line arguments
var pythonApp = builder.AddExecutable(
"api", "python", ".", "-m", "uvicorn", "main:app", "--reload", "--host", "0.0.0.0", "--port", "8000");
builder.Build().Run();
```
This code demonstrates setting up a basic executable resource. The first example runs a Node.js server script, while the second starts a Python application using Uvicorn with specific configuration options passed as arguments directly to the `AddExecutable` method.
## Resource dependencies and environment configuration
[Section titled “Resource dependencies and environment configuration”](#resource-dependencies-and-environment-configuration)
You can provide command-line arguments directly in the `AddExecutable` call and configure environment variables for resource dependencies. Executable resources can reference other resources and access their connection information.
### Arguments in the AddExecutable call
[Section titled “Arguments in the AddExecutable call”](#arguments-in-the-addexecutable-call)
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
// Arguments provided directly in addExecutable
const app = await builder.addExecutable("vercel-dev", "vercel", ".", ["dev", "--listen", "3000"]);
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
// Arguments provided directly in AddExecutable
var app = builder.AddExecutable(
"vercel-dev", "vercel", ".", "dev", "--listen", "3000");
```
### Resource dependencies with environment variables
[Section titled “Resource dependencies with environment variables”](#resource-dependencies-with-environment-variables)
For arguments that depend on other resources, use environment variables:
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const redis = await builder.addRedis("cache");
const postgres = (await builder.addPostgres("postgres")).addDatabase("appdb");
const app = await builder.addExecutable("worker", "python", ".", ["worker.py"])
.withReference(redis) // Provides ConnectionStrings__cache
.withReference(postgres); // Provides ConnectionStrings__appdb
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
var redis = builder.AddRedis("cache");
var postgres = builder.AddPostgres("postgres").AddDatabase("appdb");
var app = builder.AddExecutable("worker", "python", ".", "worker.py")
.WithReference(redis) // Provides ConnectionStrings__cache
.WithReference(postgres); // Provides ConnectionStrings__appdb
```
When one resource depends on another, [`WithReference`](/reference/api/csharp/aspire.hosting/resourcebuilderextensions/methods/#withreference "Injects a connection string as an environment variable from the source resource into the destination resource, using the source resource's name as the...")[`withReference`](/reference/api/typescript/aspire.hosting/withreference/ "Adds a reference to another resource") passes along environment variables containing the dependent resource’s connection details. For example, the `worker` executable’s reference to `redis` and `postgres` provides it with the `ConnectionStrings__cache` and `ConnectionStrings__appdb` environment variables, which contain connection strings to these resources.
### Access specific endpoint information
[Section titled “Access specific endpoint information”](#access-specific-endpoint-information)
For more control over how connection information is passed to your executable:
* TypeScript
apphost.mts
```typescript
import { createBuilder, EndpointProperty } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const redis = await builder.addRedis("cache");
const redisEndpoint = await redis.getEndpoint("tcp");
const redisHost = await redisEndpoint.property(EndpointProperty.Host);
const redisPort = await redisEndpoint.property(EndpointProperty.Port);
const app = await builder.addExecutable("app", "node", ".", ["app.js"])
.withReference(redis)
.withEnvironment("REDIS_HOST", redisHost)
.withEnvironment("REDIS_PORT", redisPort);
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
var redis = builder.AddRedis("cache");
var app = builder.AddExecutable("app", "node", ".", "app.js")
.WithReference(redis)
.WithEnvironment(context =>
{
// Provide individual connection details
context.EnvironmentVariables["REDIS_HOST"] = redis.Resource.PrimaryEndpoint.Property(EndpointProperty.Host);
context.EnvironmentVariables["REDIS_PORT"] = redis.Resource.PrimaryEndpoint.Property(EndpointProperty.Port);
});
```
## Practical example: Vercel CLI
[Section titled “Practical example: Vercel CLI”](#practical-example-vercel-cli)
Here’s a complete example using the [Vercel CLI](https://vercel.com/docs/cli) to host a frontend application with a backend API:
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
// Backend API
const api = await builder.addProject("api", "./Api/Api.csproj")
.withExternalHttpEndpoints();
// Frontend with Vercel CLI
const frontend = await builder.addExecutable("vercel-dev", "vercel", ".", ["dev", "--listen", "3000"])
.withEnvironment("API_URL", api.getEndpoint("http"))
.withHttpEndpoint({ port: 3000, name: "http" });
await builder.build().run();
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
// Backend API
var api = builder.AddProject("api")
.WithExternalHttpEndpoints();
// Frontend with Vercel CLI
var frontend = builder.AddExecutable(
"vercel-dev", "vercel", ".", "dev", "--listen", "3000")
.WithEnvironment("API_URL", api.GetEndpoint("http"))
.WithHttpEndpoint(port: 3000, name: "http");
builder.Build().Run();
```
## Configure endpoints
[Section titled “Configure endpoints”](#configure-endpoints)
Executable resources can expose HTTP endpoints that other resources can reference:
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const frontend = await builder.addExecutable("webpack-dev", "npx", ".", ["webpack", "serve", "--port", "8080", "--host", "0.0.0.0"])
.withHttpEndpoint({ port: 8080, name: "http" });
// Another service can reference the frontend
const e2eTests = await builder.addExecutable("playwright", "npx", ".", ["playwright", "test"])
.withEnvironment("BASE_URL", frontend.getEndpoint("http"));
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
var frontend = builder.AddExecutable(
"webpack-dev", "npx", ".", "webpack", "serve", "--port", "8080", "--host", "0.0.0.0")
.WithHttpEndpoint(port: 8080, name: "http");
// Another service can reference the frontend
var e2eTests = builder.AddExecutable("playwright", "npx", ".", "playwright", "test")
.WithEnvironment("BASE_URL", frontend.GetEndpoint("http"));
```
## Environment configuration
[Section titled “Environment configuration”](#environment-configuration)
Configure environment variables for your executable:
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const app = await builder.addExecutable("api", "uvicorn", ".", ["main:app", "--reload", "--host", "0.0.0.0"])
.withEnvironment("DEBUG", "true")
.withEnvironment("LOG_LEVEL", "info")
.withEnvironment("START_TIME", new Date().toISOString());
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
var app = builder.AddExecutable(
"api", "uvicorn", ".", "main:app", "--reload", "--host", "0.0.0.0")
.WithEnvironment("DEBUG", "true")
.WithEnvironment("LOG_LEVEL", "info")
.WithEnvironment(context =>
{
// Dynamic environment variables
context.EnvironmentVariables["START_TIME"] = DateTimeOffset.UtcNow.ToString();
});
```
### `withEnvironment` API unification in Aspire 13.3
[Section titled “withEnvironment API unification in Aspire 13.3”](#withenvironment-api-unification-in-aspire-133)
Aspire 13.3 unified non-C# AppHost environment assignment behind a single `withEnvironment(name, value)` pattern. The public TypeScript API accepts plain strings, reference expressions, endpoint references, parameter resources, supported resources that expose connection strings, expression values, and awaitable forms of supported values.
When upgrading to Aspire 13.3, replace every earlier per-kind environment helper call with `withEnvironment(name, value)`. The 13.3 TypeScript SDK doesn’t generate compatibility aliases for those helpers.
## Publishing with PublishAsDockerFile
[Section titled “Publishing with PublishAsDockerFile”](#publishing-with-publishasdockerfile)
For production deployment, executable resources need to be containerized. Use the [`PublishAsDockerFile`](/reference/api/csharp/aspire.hosting/executableresourcebuilderextensions/methods/#publishasdockerfile "Adds annotation to Aspire.Hosting.ApplicationModel.ExecutableResource to support containerization during deployment.")[`publishAsDockerFile`](/reference/api/typescript/aspire.hosting/publishasdockerfile-aspire-hosting-applicationmodel-executableresource-obj-containerresource-promise-void/ "Publishes an executable as a Docker file") method to specify how the executable should be packaged:
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const app = await builder.addExecutable("frontend", "npm", ".", ["start", "--port", "3000"])
.publishAsDockerFile(async () => {});
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
var app = builder.AddExecutable(
"frontend", "npm", ".", "start", "--port", "3000")
.PublishAsDockerFile();
```
When you call `PublishAsDockerFile()`, Aspire generates a Dockerfile during the publish process. You can customize this by providing your own Dockerfile:
### Custom Dockerfile for publishing
[Section titled “Custom Dockerfile for publishing”](#custom-dockerfile-for-publishing)
Create a `Dockerfile` in your executable’s working directory:
Dockerfile
```dockerfile
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["npm", "start"]
```
Then reference it in your AppHost:
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const app = await builder.addExecutable("frontend", "npm", ".", ["start"])
.publishAsDockerFile(async () => {});
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
var app = builder.AddExecutable("frontend", "npm", ".", "start")
.PublishAsDockerFile([new DockerfileBuildArg("NODE_ENV", "production")]);
```
## Best practices
[Section titled “Best practices”](#best-practices)
When working with executable resources:
1. **Use explicit paths**: For better reliability, use full paths to executables when possible.
2. **Handle dependencies**: Use `WithReference` to establish proper dependency relationships.
3. **Configure explicit start**: Use [`WithExplicitStart`](/reference/api/csharp/aspire.hosting/resourcebuilderextensions/methods/#withexplicitstart "Adds a Aspire.Hosting.ApplicationModel.ExplicitStartupAnnotation annotation to the resource so it doesn't automatically start with the AppHost startup.")[`withExplicitStart`](/reference/api/typescript/aspire.hosting/withexplicitstart/ "Prevents resource from starting automatically") for executables that shouldn’t start automatically.
4. **Prepare for deployment**: Always use `PublishAsDockerFile()` for production scenarios.
5. **Environment isolation**: Use environment variables rather than command-line arguments for sensitive configuration.
6. **Resource naming**: Use descriptive names that clearly identify the executable’s purpose.
# Hot Reload and watch
> Learn how hot reload works in Aspire and how `aspire watch` rebuilds and restarts resources automatically when project files change during development.
Aspire has two levels of watch behavior:
1. **AppHost watch** - watches the AppHost itself so changes to the application model restart the AppHost-managed application.
2. **Resource watch and hot reload** - depend on the application or framework backing each resource.
Aspire’s CLI watch support is centered on the AppHost. Aspire supports two AppHost languages, C# and TypeScript, and `defaultWatchEnabled` applies to the AppHost-managed application regardless of which AppHost language you use.
When watch mode is enabled, Aspire owns the file-watching loop for the AppHost-managed application. File changes cause Aspire to restart the application topology so the updated AppHost model and resources are applied.
Aspire watch mode is the recommended CLI workflow when you want hot reload-like behavior for AppHost changes. It is restart-based: Aspire restarts the AppHost-managed application after changes instead of applying runtime-specific hot reload semantics inside every resource process.
## Default Aspire behavior
[Section titled “Default Aspire behavior”](#default-aspire-behavior)
By default, `aspire run` and `aspire start` start the Aspire application once. They don’t watch the AppHost or resource source files.
After you change AppHost code, restart the Aspire application manually:
* For `aspire run`, stop the process with ⌃+C`⌃+C`Control + C`CtrlC`Control + C`CtrlC`, and then run `aspire run` again.
* For `aspire start`, run `aspire start` again. The command stops the previous detached instance and starts a new one.
## Enable default watch mode
[Section titled “Enable default watch mode”](#enable-default-watch-mode)
Aspire includes an opt-in `defaultWatchEnabled` feature flag. When enabled, Aspire uses watch mode by default and automatically restarts the Aspire application after supported AppHost or resource file changes:
Aspire CLI
```bash
aspire config set features.defaultWatchEnabled true
```
To enable watch mode for every Aspire project on your machine, set the value globally:
Aspire CLI
```bash
aspire config set features.defaultWatchEnabled true --global
```
To see the current value and available feature flags, run:
Aspire CLI
```bash
aspire config list --all
```
Watch mode is useful when you want Aspire to restart the AppHost-managed application for you after AppHost changes. It supports both C# and TypeScript AppHosts and is a restart-based workflow, not the same experience as runtime-specific or IDE-specific hot reload.
## AppHost language guidance
[Section titled “AppHost language guidance”](#apphost-language-guidance)
* TypeScript
For a TypeScript AppHost, `defaultWatchEnabled` watches the AppHost. When AppHost code changes, Aspire restarts the AppHost-managed application so the updated model is applied.
TypeScript AppHost watch doesn’t automatically provide hot reload for every resource in the application. Use resource-specific watch, reload, restart, or rebuild workflows for changes inside individual resources.
Use this workflow when changes affect:
* The AppHost model in `apphost.mts`.
* Resource configuration, endpoints, parameters, or integration setup.
* Multiple services that need to be restarted together under Aspire orchestration.
Aspire CLI
```bash
aspire config set features.defaultWatchEnabled true
aspire run
```
* C#
For a C# AppHost, `defaultWatchEnabled` watches the AppHost project. When AppHost code changes, Aspire restarts the AppHost-managed application so the updated model is applied.
Today, C# project resources are also controlled by this setting. That means changes to C# project resources can trigger Aspire to restart the AppHost-managed application too.
Use this workflow when changes affect:
* The AppHost model in `AppHost.cs`.
* C# project resources that are part of the AppHost.
* Resource configuration, endpoints, parameters, or integration setup.
* Multiple services that need to be restarted together under Aspire orchestration.
Aspire CLI
```bash
aspire config set features.defaultWatchEnabled true
aspire run
```
## Hot reload for Aspire resources
[Section titled “Hot reload for Aspire resources”](#hot-reload-for-aspire-resources)
AppHost watch and resource hot reload are separate concerns. The AppHost describes and starts the application topology, but each resource is backed by a framework or runtime with its own development loop.
In general, keep the AppHost running while you work on individual resources. Don’t stop and restart the AppHost just because one resource changed. If a resource needs to be restarted or rebuilt, do that for the individual resource from the Aspire CLI or the Aspire Dashboard.
Use the `aspire resource` command to control individual resources from the CLI:
Aspire CLI
```bash
aspire resource stop
aspire resource start
```
For C# project resources, rebuild the individual resource when the project needs to be rebuilt:
Aspire CLI
```bash
aspire resource rebuild
```
* C# projects
`dotnet watch` natively supports C# Aspire AppHosts and transitively watches .NET projects in the application:
* Changes to the AppHost restart the AppHost.
* Changes to an individual .NET project, or to one of its dependencies, restart that project.
* Rude edits restart the application.
Run `dotnet watch` against the AppHost project when you want the .NET SDK’s watch loop for the AppHost and its C# projects:
.NET CLI
```bash
dotnet watch --project './src/MyApp.AppHost/MyApp.AppHost.csproj'
```
Important
This experience has some quirks today. Some changes are applied but can still require an explicit restart, and it isn’t always easy to tell when that happened. If you don’t observe an expected change, restart the resource with `aspire resource stop` and `aspire resource start`, or rebuild a C# project resource with `aspire resource rebuild`.
When you use Aspire watch mode instead, C# project resources are special today: `defaultWatchEnabled` controls both the C# AppHost and C# project resources.
* Vite resources
Vite resources use Vite’s development server behavior. Vite can provide browser refresh and Hot Module Replacement for the frontend application, but that behavior is separate from AppHost watch.
Use Aspire watch when changes affect the AppHost model. Use the Vite development loop when changes affect the frontend application and you want Vite’s browser refresh or Hot Module Replacement behavior.
If a Vite resource needs a restart, restart the Vite resource from the Aspire CLI or dashboard instead of restarting the AppHost.
* Other resources
Other Aspire resources follow the watch, reload, or restart behavior of the runtime or framework that backs the resource. For example, a container resource, executable resource, or framework-specific resource might not support hot reload at all, or it might require its own watch command.
Use Aspire watch when the AppHost model should be re-evaluated. Use the resource’s native development command when you want the tightest inner loop for that resource. If the resource needs to be restarted or rebuilt, restart or rebuild that individual resource from the Aspire CLI or dashboard.
## Recommended workflow
[Section titled “Recommended workflow”](#recommended-workflow)
Use these workflows based on what you’re editing:
| Goal | Recommended command |
| ------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| Run the whole distributed app once | `aspire run` |
| Run the whole distributed app in the background | `aspire start` |
| Watch a C# or TypeScript AppHost from the CLI | `aspire config set features.defaultWatchEnabled true`, then `aspire run` or `aspire start` |
| Watch C# project resources through Aspire | `aspire config set features.defaultWatchEnabled true`, then `aspire run` or `aspire start` |
| Restart one resource | `aspire resource stop`, then `aspire resource start` |
| Rebuild one C# project resource | `aspire resource rebuild` |
| Use a runtime-specific hot reload loop for one resource | The resource’s native watch, reload, or development-server command |
Aspire CLI doesn’t currently provide a single hot reload command that applies every runtime’s hot reload semantics across an AppHost-managed distributed application. Aspire default watch supports both AppHost languages by restarting the AppHost-managed application after AppHost changes. C# project resources are also controlled by this setting today. For other resources, keep the AppHost running and use the resource’s framework, runtime, CLI action, or dashboard action for resource-specific reloads, restarts, and rebuilds.
## IDE hot reload and debugging
[Section titled “IDE hot reload and debugging”](#ide-hot-reload-and-debugging)
Visual Studio, Visual Studio Code, and JetBrains Rider provide their own hot reload and debugging experiences. When you run the AppHost under a debugger in one of these IDEs, Aspire delegates debugging and IDE-managed hot reload behavior to that IDE. This doesn’t integrate with or overlap Aspire’s CLI restart, rebuild, or watch behavior.
Important
Use an IDE workflow when you want the IDE to manage debugging or hot reload for supported resources. Use Aspire CLI and dashboard actions when you want Aspire to restart, rebuild, or watch the AppHost-managed application and its resources.
### Visual Studio Code
[Section titled “Visual Studio Code”](#visual-studio-code)
Use the Aspire extension for Visual Studio Code when you want VS Code to start the AppHost, attach debuggers, and manage supported resource debugging experiences. VS Code hot reload or framework-specific refresh behavior still belongs to the debugger or framework backing the resource, not to Aspire restart, rebuild, or watch behavior.
### Visual Studio
[Section titled “Visual Studio”](#visual-studio)
Use Visual Studio when you want its built-in debugging and hot reload experience for supported resources. Visual Studio can run and debug Aspire apps, but IDE hot reload is still separate from Aspire restart, rebuild, and watch behavior.
### JetBrains Rider
[Section titled “JetBrains Rider”](#jetbrains-rider)
Use JetBrains Rider when you want Rider’s debugging and hot reload experience for supported resources. Rider’s IDE-managed hot reload behavior is separate from Aspire restart, rebuild, and watch behavior.
## See also
[Section titled “See also”](#see-also)
* [`aspire run`](/reference/cli/commands/aspire-run/)
* [`aspire start`](/reference/cli/commands/aspire-start/)
* [`aspire config set`](/reference/cli/commands/aspire-config-set/)
* [Aspire VS Code extension](/get-started/aspire-vscode-extension/)
# Migrate from Docker Compose to Aspire
> Compare Aspire and Docker Compose for local development, service discovery, and observability. Map Compose services and dependencies to TypeScript or C# AppHosts.
Docker Compose and Aspire both describe and run multi-service applications. This guide compares their local development workflows and maps Compose services, dependencies, and configuration to an Aspire AppHost written in TypeScript or C#.
## Understand the differences
[Section titled “Understand the differences”](#understand-the-differences)
Keep Docker Compose when a container-focused YAML workflow meets your needs. Consider Aspire when you want to compose containers with processes running directly on the host, manage service references in code, and inspect application telemetry alongside resource health.
### Docker Compose vs Aspire
[Section titled “Docker Compose vs Aspire”](#docker-compose-vs-aspire)
| Feature | Docker Compose | Aspire |
| ----------------------- | ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| **Primary purpose** | Define and run multi-container applications | Compose application resources for development and deployment |
| **Scope** | Containers | Containers, Python and Node.js apps, .NET projects, executables, and cloud resources |
| **Configuration** | Declarative YAML | Strongly typed TypeScript or C# AppHost |
| **Target environment** | Docker environments | Local development and deployment through publishing integrations, including Docker Compose |
| **Service discovery** | Service names and DNS on Compose networks | Service references and connection information passed to resources |
| **Local observability** | Container logs and health checks; add OpenTelemetry tooling for application telemetry | Integrated resource health, console logs, and an OpenTelemetry dashboard; application instrumentation is still required |
### Key conceptual shifts
[Section titled “Key conceptual shifts”](#key-conceptual-shifts)
When migrating from Docker Compose to Aspire, consider these conceptual differences:
* **From YAML to an AppHost** — Express configuration in strongly typed TypeScript or C# code
* **From containers to resources** — Compose containers with local application processes, parameters, and cloud resources
* **From container DNS to resource references** — Pass service endpoints and connection information to dependent resources
* **From separate tools to a shared dashboard** — Inspect resource health and instrumented application logs, traces, and metrics together
* **Startup orchestration differs** — Compose `depends_on` supports startup order and health conditions; Aspire references supply connection information, while wait relationships control startup dependencies
You don’t need to migrate orchestration just to view OpenTelemetry. Point instrumented Compose services at the [standalone Aspire dashboard](/dashboard/standalone/) to inspect logs, traces, and metrics, including through [coding-agent CLI or MCP workflows](/dashboard/ai-coding-agents/#standalone-mode). Standalone mode doesn’t add AppHost resource controls to Compose.
For detailed API mappings, see [Docker Compose to Aspire AppHost API reference](/app-host/docker-compose-to-apphost-reference/).
## Common migration patterns
[Section titled “Common migration patterns”](#common-migration-patterns)
This section demonstrates practical migration scenarios you’ll likely encounter when moving from Docker Compose to Aspire. Each pattern shows a complete Docker Compose example alongside its accurate Aspire equivalent.
### Multi-service web application
[Section titled “Multi-service web application”](#multi-service-web-application)
This example shows a typical three-tier application with a frontend, API, and database.
**Docker Compose example:**
compose.yaml
```yaml
version: '3.8'
services:
frontend:
build: ./frontend
ports:
- "3000:3000"
depends_on:
api:
condition: service_healthy
environment:
- API_URL=http://api:5000
api:
build: ./api
ports:
- "5000:5000"
depends_on:
database:
condition: service_healthy
environment:
- ConnectionStrings__DefaultConnection=Host=database;Database=myapp;Username=postgres;Password=secret
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
interval: 10s
timeout: 3s
retries: 3
database:
image: postgres:15
environment:
- POSTGRES_DB=myapp
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=secret
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 3s
retries: 3
volumes:
postgres_data:
```
**Aspire equivalent:**
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
// Add PostgreSQL with explicit version and persistent storage
const database = (await builder.addPostgres("postgres")
.withImageTag("15")
.withDataVolume())
.addDatabase("myapp");
// Add the API project with proper dependencies
const api = await builder.addProject("api", "./MyApp.Api/MyApp.Api.csproj")
.withHttpEndpoint({ port: 5000 })
.withHttpHealthCheck("/health")
.withReference(database, "DefaultConnection")
.waitFor(database);
// Add the frontend project with dependencies
const frontend = await builder.addProject("frontend", "./MyApp.Frontend/MyApp.Frontend.csproj")
.withHttpEndpoint({ port: 3000 })
.withReference(api)
.withEnvironment("API_URL", api.getEndpoint("http"))
.waitFor(api);
await builder.build().run();
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
// Add PostgreSQL with explicit version and persistent storage
var database = builder.AddPostgres("postgres")
.WithImageTag("15")
.WithDataVolume()
.AddDatabase("myapp");
// Add the API project with proper dependencies
var api = builder.AddProject("api")
.WithHttpEndpoint(port: 5000)
.WithHttpHealthCheck("/health")
.WithReference(database, "DefaultConnection")
.WaitFor(database);
// Add the frontend project with dependencies
var frontend = builder.AddProject("frontend")
.WithHttpEndpoint(port: 3000)
.WithReference(api)
.WithEnvironment("API_URL", api.GetEndpoint("http"))
.WaitFor(api);
builder.Build().Run();
```
build: services become project or Dockerfile resources
In Docker Compose, the `build:` directive creates container images from Dockerfiles. In Aspire, .NET services are added directly as project references with [`AddProject`](/reference/api/csharp/aspire.hosting/projectresourcebuilderextensions/methods/#addproject "Adds a .NET project to the application model.")[`addProject`](/reference/api/typescript/aspire.hosting/addproject/ "Adds a .NET project resource"), providing better debugging, hot reload, and telemetry integration. For services that still build from Dockerfiles, use [`AddDockerfile`](/reference/api/csharp/aspire.hosting/containerresourcebuilderextensions/methods/#adddockerfile "Adds a Dockerfile to the application model that can be treated like a container resource.")[`addDockerfile`](/reference/api/typescript/aspire.hosting/adddockerfile/ "Adds a Dockerfile to the application model that can be treated like a container resource.") for an existing Dockerfile or [`AddDockerfileBuilder`](/reference/api/csharp/aspire.hosting/containerresourcebuilderextensions/methods/#adddockerfilebuilder "Adds a Dockerfile to the application model that can be treated like a container resource, with the Dockerfile generated programmatically using the...")[`addDockerfileBuilder`](/reference/api/typescript/aspire.hosting/adddockerfilebuilder/ "Adds a container resource built from a programmatically generated Dockerfile") when the AppHost should generate the Dockerfile programmatically.
**Key differences explained:**
* **Build vs. project** — Docker Compose `build:` services become `AddProject()` for .NET apps, which runs them directly instead of in containers
* **Ports** — Both examples explicitly map ports (3000 and 5000)
* **Startup order** — Docker Compose uses `depends_on` with health conditions; Aspire uses [`WaitFor`](/reference/api/csharp/aspire.hosting/resourcebuilderextensions/methods/#waitfor "Waits for the dependency resource to enter the Running state before starting the resource.")[`waitFor`](/reference/api/typescript/aspire.hosting/waitfor/ "Waits for another resource to be ready") for startup ordering
* **Service discovery** — [`WithReference`](/reference/api/csharp/aspire.hosting/resourcebuilderextensions/methods/#withreference "Injects a connection string as an environment variable from the source resource into the destination resource, using the source resource's name as the...")[`withReference`](/reference/api/typescript/aspire.hosting/withreference/ "Adds a reference to another resource") only configures service discovery and connection strings; it doesn’t control startup order
* **Connection strings** — By default, `WithReference(database)` provides `ConnectionStrings__myapp` using the resource name from [`AddDatabase`](/reference/api/csharp/aspire.hosting.postgresql/postgresbuilderextensions/methods/#adddatabase "Adds a PostgreSQL database to the application model.")[`addDatabase`](/reference/api/typescript/aspire.hosting.postgresql/adddatabase/ "Adds a PostgreSQL database to the application model."). To match a different name like `DefaultConnection`, use a named reference: `.WithReference(database, "DefaultConnection")`
* **Volumes** — [`WithDataVolume`](/reference/api/csharp/aspire.hosting.postgresql/postgresbuilderextensions/methods/#withdatavolume "Adds a named volume for the data folder to a PostgreSQL container resource.")[`withDataVolume`](/reference/api/typescript/aspire.hosting.postgresql/withdatavolume/ "Adds a named volume for the data folder to a PostgreSQL container resource.") must be called explicitly to add persistent storage; it’s not automatic
* **Image versions** — `WithImageTag("15")` pins PostgreSQL to version 15
### Container-based services
[Section titled “Container-based services”](#container-based-services)
This example shows a mix of existing container images and a Dockerfile-built service being orchestrated.
**Docker Compose example:**
compose.yaml
```yaml
version: '3.8'
services:
web:
build: .
ports:
- "8080:8080"
depends_on:
redis:
condition: service_started
postgres:
condition: service_healthy
environment:
- REDIS_URL=redis://redis:6379
- DATABASE_URL=postgresql://postgres:secret@postgres:5432/main
redis:
image: redis:7
ports:
- "6379:6379"
postgres:
image: postgres:15
environment:
POSTGRES_PASSWORD: secret
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready"]
interval: 10s
volumes:
postgres_data:
```
**Aspire equivalent:**
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
// Add backing services with explicit versions
const redis = await builder.addRedis("redis")
.withImageTag("7")
.withHostPort(6379);
const postgres = (await builder.addPostgres("postgres")
.withImageTag("15")
.withDataVolume())
.addDatabase("main");
// Build the web app from a Dockerfile (matches Docker Compose "build: .")
const web = await builder.addDockerfile("web", ".")
.withHttpEndpoint({ port: 8080, targetPort: 8080 })
.withReference(redis)
.withReference(postgres)
.waitFor(redis)
.waitFor(postgres);
await builder.build().run();
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
// Add backing services with explicit versions
var redis = builder.AddRedis("redis")
.WithImageTag("7")
.WithHostPort(6379);
var postgres = builder.AddPostgres("postgres")
.WithImageTag("15")
.WithDataVolume()
.AddDatabase("main");
// Build the web app from a Dockerfile (matches Docker Compose "build: .")
var web = builder.AddDockerfile("web", ".")
.WithHttpEndpoint(port: 8080, targetPort: 8080)
.WithReference(redis)
.WithReference(postgres)
.WaitFor(redis)
.WaitFor(postgres);
builder.Build().Run();
```
Connection string format differences
Aspire generates .NET-format connection strings, which differ from Docker Compose URL formats:
* Docker Compose: `REDIS_URL=redis://redis:6379`
* Aspire: `ConnectionStrings__redis=localhost:54321`
* Docker Compose: `DATABASE_URL=postgresql://postgres:secret@postgres:5432/main`
* Aspire: `ConnectionStrings__main=Host=localhost;Port=12345;Username=postgres;Password=;Database=main`
If your application expects URL-format environment variables, construct them manually with [`WithEnvironment`](/reference/api/csharp/aspire.hosting/resourcebuilderextensions/methods/#withenvironment "Adds an environment variable to the resource.")[`withEnvironment`](/reference/api/typescript/aspire.hosting/withenvironment/ "Sets an environment variable"). See [Environment variables and configuration](#environment-variables-and-configuration) for details.
**Key differences explained:**
* **Image versions** — Explicitly specified with [`WithImageTag`](/reference/api/csharp/aspire.hosting/containerresourcebuilderextensions/methods/#withimagetag "Allows overriding the image tag on a container.")[`withImageTag`](/reference/api/typescript/aspire.hosting/withimagetag/ "Allows overriding the image tag on a container.") to match Docker Compose
* **Dockerfile builds** — Docker Compose `build: .` maps to `AddDockerfile("web", ".")`, which builds a container image from a Dockerfile. Use [`AddContainer`](/reference/api/csharp/aspire.hosting/containerresourcebuilderextensions/methods/#addcontainer "Adds a container resource to the application. Uses the \"latest\" tag.")[`addContainer`](/reference/api/typescript/aspire.hosting/addcontainer/ "Adds a container resource to the application.") for pre-built images that use `image:` in Docker Compose
* **Ports** — `WithHostPort()` maps to a static host port; without it, Aspire assigns a random port
* **Volumes** — `WithDataVolume()` must be called explicitly to add persistent storage
* **Startup ordering** — `WaitFor()` controls startup order, similar to Docker Compose `depends_on` with conditions
* **Connection strings** — `WithReference()` provides Aspire-format connection strings (`ConnectionStrings__*`), not URL-format variables
### Environment variables and configuration
[Section titled “Environment variables and configuration”](#environment-variables-and-configuration)
This example shows different approaches to configuration management.
**Docker Compose approach:**
compose.yaml
```yaml
services:
app:
image: myapp:latest
environment:
- DATABASE_URL=postgresql://user:pass@db:5432/myapp
- REDIS_URL=redis://cache:6379
- API_KEY=${API_KEY}
- LOG_LEVEL=info
```
**Aspire approach:**
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
// Add external parameter for secrets
const apiKey = builder.addParameter("apiKey", { secret: true });
const database = (await builder.addPostgres("db"))
.addDatabase("myapp");
const cache = await builder.addRedis("cache");
const app = await builder.addContainer("app", { image: "myapp", tag: "latest" })
.withReference(database)
.withReference(cache)
.withEnvironment("API_KEY", apiKey)
.withEnvironment("LOG_LEVEL", "info");
await builder.build().run();
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
// Add external parameter for secrets
var apiKey = builder.AddParameter("apiKey", secret: true);
var database = builder.AddPostgres("db")
.AddDatabase("myapp");
var cache = builder.AddRedis("cache");
var app = builder.AddContainer("app", "myapp", "latest")
.WithReference(database)
.WithReference(cache)
.WithEnvironment("API_KEY", apiKey)
.WithEnvironment("LOG_LEVEL", "info");
builder.Build().Run();
```
Aspire connection strings differ from Docker Compose URLs
`WithReference()` provides connection strings in .NET format, not URL format:
* `ConnectionStrings__myapp=Host=localhost;Port=12345;Username=postgres;Password=;Database=myapp`
* `ConnectionStrings__cache=localhost:54321`
If your application expects URL-format variables like `DATABASE_URL` or `REDIS_URL`, construct them manually using the `WithEnvironment` callback:
* TypeScript
apphost.mts
```typescript
const dbPassword = builder.addParameter("dbPassword", { secret: true });
const db = (await builder.addPostgres("db", { password: dbPassword }))
.addDatabase("myapp");
const app = await builder.addContainer("app", "myapp:latest")
.withReference(db)
.withEnvironment("DATABASE_URL",
builder.createReferenceExpression`postgresql://postgres:${dbPassword}@db:5432/myapp`)
.withEnvironment("REDIS_URL", "redis://cache:6379");
```
* C#
AppHost.cs
```csharp
var dbPassword = builder.AddParameter("dbPassword", secret: true);
var db = builder.AddPostgres("db", password: dbPassword)
.AddDatabase("myapp");
var app = builder.AddContainer("app", "myapp", "latest")
.WithReference(db)
.WithEnvironment(context =>
{
context.EnvironmentVariables["DATABASE_URL"] =
ReferenceExpression.Create(
$"postgresql://postgres:{dbPassword}@db:5432/myapp");
context.EnvironmentVariables["REDIS_URL"] = "redis://cache:6379";
});
```
### Custom volumes and bind mounts
[Section titled “Custom volumes and bind mounts”](#custom-volumes-and-bind-mounts)
**Docker Compose example:**
compose.yaml
```yaml
version: '3.8'
services:
app:
image: myapp:latest
volumes:
- app_data:/data
- ./config:/app/config:ro
worker:
image: myworker:latest
volumes:
- app_data:/shared
volumes:
app_data:
```
**Aspire equivalent:**
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const app = await builder.addContainer("app", { image: "myapp", tag: "latest" })
.withVolume("/data", { name: "app-data", isReadOnly: true })
.withBindMount("./config", "/app/config", { isReadOnly: true });
const worker = await builder.addContainer("worker", { image: "myworker", tag: "latest" })
.withVolume("/shared", { name: "app-data" });
await builder.build().run();
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
// Create a named volume for sharing data
var appData = builder.AddVolume("app-data");
var app = builder.AddContainer("app", "myapp", "latest")
.WithVolume(appData, "/data")
.WithBindMount("./config", "/app/config", isReadOnly: true);
var worker = builder.AddContainer("worker", "myworker", "latest")
.WithVolume(appData, "/shared");
builder.Build().Run();
```
**Key differences:**
* **Named volumes** — Created with `AddVolume()` and shared between containers
* **Bind mounts** — Use [`WithBindMount`](/reference/api/csharp/aspire.hosting/containerresourcebuilderextensions/methods/#withbindmount "Adds a bind mount to a container resource.")[`withBindMount`](/reference/api/typescript/aspire.hosting/withbindmount/ "Adds a bind mount to a container resource.") for host directory access
### Networking
[Section titled “Networking”](#networking)
Docker Compose supports custom networks to isolate groups of services from each other:
compose.yaml
```yaml
services:
proxy:
build: ./proxy
networks:
- frontend
app:
build: ./app
networks:
- frontend
- backend
db:
image: postgres
networks:
- backend
networks:
frontend:
backend:
```
Aspire doesn’t have an equivalent for custom network isolation. Instead, Aspire automatically creates a shared container network for all container resources and uses service discovery to manage inter-service communication. All containers in an Aspire AppHost can reach each other by resource name. .NET projects and executables run on the host and access containers through injected host/port endpoints.
Note
If your Docker Compose setup relies on network isolation (for example, preventing a frontend service from directly accessing the database), Aspire doesn’t provide a direct equivalent. Consider using application-level access controls or firewall rules in your deployment environment instead.
## Migration strategy
[Section titled “Migration strategy”](#migration-strategy)
Successfully migrating from Docker Compose to Aspire requires a systematic approach.
1. ### Assess your current setup
[Section titled “Assess your current setup”](#assess-your-current-setup)
Before migrating, inventory your Docker Compose setup:
* **Services** — Identify all services including databases, caches, APIs, and web applications
* **Dependencies** — Map out service dependencies from `depends_on` declarations
* **Data persistence** — Catalog all volumes and bind mounts used for data storage
* **Environment variables** — List all configuration variables and secrets
* **Health checks** — Document any custom health check commands
* **Image versions** — Note specific versions used in production
2. ### Create the Aspire AppHost
[Section titled “Create the Aspire AppHost”](#create-the-aspire-apphost)
Start by creating a new Aspire project:
```bash
aspire new aspire-starter -o MyApp
```
3. ### Migrate services incrementally
[Section titled “Migrate services incrementally”](#migrate-services-incrementally)
Migrate services one by one, starting with backing services:
* **Add backing services** like PostgreSQL, Redis with specific versions using `WithImageTag()`
* **Add persistent storage** using `WithDataVolume()` where needed
* **Convert .NET applications** to project references with `AddProject()` for better integration
* **Convert Dockerfile-built containers** using `AddDockerfile()` to match `build:` directives
* **Convert pre-built images** using `AddContainer()` to match `image:` directives
* **Configure dependencies** with `WithReference()` for service discovery
* **Add startup ordering** with `WaitFor()` to match `depends_on` behavior
* **Set up environment variables** — Note that connection string formats will differ
* **Migrate health checks** — Use [`WithHttpHealthCheck`](/reference/api/csharp/aspire.hosting/resourcebuilderextensions/methods/#withhttphealthcheck "Adds a health check to the resource which is mapped to a specific endpoint.")[`withHttpHealthCheck`](/reference/api/typescript/aspire.hosting/withhttphealthcheck-aspire-hosting-applicationmodel-containerresource-string-number-string/ "Adds a health check to the resource which is mapped to a specific endpoint.") or [`WithHealthCheck`](/reference/api/csharp/aspire.hosting/resourcebuilderextensions/methods/#withhealthcheck "Adds a Aspire.Hosting.ApplicationModel.HealthCheckAnnotation to the resource annotations to associate a resource with a named health check managed by the...")[`withHealthCheck`](/reference/api/typescript/aspire.hosting/withhealthcheck/ "Adds a health check by key") for custom checks
4. ### Handle data migration
[Section titled “Handle data migration”](#handle-data-migration)
For persistent data:
* Use `WithDataVolume()` for automatic volume management with integrations
* Use [`WithVolume`](/reference/api/csharp/aspire.hosting/containerresourcebuilderextensions/methods/#withvolume "Adds a volume to a container resource.")[`withVolume`](/reference/api/typescript/aspire.hosting/withvolume-aspire-hosting-applicationmodel-containerresource-string-string-boolean/ "Adds a volume to a container resource.") for named volumes that need to persist data
* Use `WithBindMount()` for host directory mounts when you need direct access to host files
5. ### Test and validate
[Section titled “Test and validate”](#test-and-validate)
* Start the Aspire AppHost and verify all services start correctly
* Check the dashboard to confirm service health and connectivity status
* Validate that inter-service communication works as expected
* **Verify connection strings** — If your app expects specific URL formats, you may need to adjust environment variables
## Migration troubleshooting
[Section titled “Migration troubleshooting”](#migration-troubleshooting)
### Common issues and solutions
[Section titled “Common issues and solutions”](#common-issues-and-solutions)
#### Connection string format mismatch
[Section titled “Connection string format mismatch”](#connection-string-format-mismatch)
Aspire generates .NET-style connection strings (`ConnectionStrings__*`) rather than URL formats like `postgresql://` or `redis://`.
**Solution**: If your application expects specific URL formats, construct them manually using `WithEnvironment()`:
* TypeScript
apphost.mts
```typescript
const dbPassword = builder.addParameter("dbPassword", { secret: true });
const postgres = (await builder.addPostgres("db", { password: dbPassword }))
.addDatabase("myapp");
const app = await builder.addContainer("app", "myapp:latest")
.withReference(postgres)
.withEnvironment("DATABASE_URL",
builder.createReferenceExpression`postgresql://postgres:${dbPassword}@db:5432/myapp`);
```
* C#
AppHost.cs
```csharp
var dbPassword = builder.AddParameter("dbPassword", secret: true);
var postgres = builder.AddPostgres("db", password: dbPassword)
.AddDatabase("myapp");
var app = builder.AddContainer("app", "myapp", "latest")
.WithReference(postgres)
.WithEnvironment(context =>
{
context.EnvironmentVariables["DATABASE_URL"] =
ReferenceExpression.Create(
$"postgresql://postgres:{dbPassword}@db:5432/myapp");
});
```
#### Service startup order issues
[Section titled “Service startup order issues”](#service-startup-order-issues)
`WithReference()` only configures service discovery, not startup ordering.
**Solution**: Use `WaitFor()` to ensure dependencies are ready:
* TypeScript
apphost.mts
```typescript
const api = await builder.addProject("api", "./Api/Api.csproj", "https")
.withReference(database) // Service discovery
.waitFor(database); // Startup ordering
```
* C#
AppHost.cs
```csharp
var api = builder.AddProject("api")
.WithReference(database) // Service discovery
.WaitFor(database); // Startup ordering
```
#### Volume mounting issues
[Section titled “Volume mounting issues”](#volume-mounting-issues)
* Use absolute paths for bind mounts to avoid path resolution issues
* Ensure the host directory exists and has proper permissions
* Use `WithDataVolume()` for database integrations — this must be called explicitly
#### Port conflicts
[Section titled “Port conflicts”](#port-conflicts)
Aspire automatically assigns random ports by default.
**Solution**: Use `WithHostPort()` or `WithHttpEndpoint(port:)` for static port mapping:
* TypeScript
apphost.mts
```typescript
const redis = await builder.addRedis("cache")
.withHostPort(6379);
```
* C#
AppHost.cs
```csharp
var redis = builder.AddRedis("cache")
.WithHostPort(6379);
```
#### Health check migration
[Section titled “Health check migration”](#health-check-migration)
Docker Compose health checks use shell commands. Aspire integrations (like PostgreSQL and Redis) include built-in health checks automatically. For custom health checks, Aspire offers different approaches depending on the resource type.
**Solution**: For resources with HTTP endpoints, use `WithHttpHealthCheck()`:
* TypeScript
apphost.mts
```typescript
const api = await builder.addProject("api", "./Api/Api.csproj", "https")
.withHttpHealthCheck("/health");
```
* C#
AppHost.cs
```csharp
var api = builder.AddProject("api")
.WithHttpHealthCheck("/health");
```
For custom container health checks that need shell commands (like RabbitMQ), register a custom health check and associate it with the resource:
* TypeScript
apphost.mts
```typescript
const rabbit = await builder.addContainer("rabbitmq", "rabbitmq", "4.1.4-management-alpine")
.withHealthCheck("rabbitmq-health");
// WaitFor uses the registered health check to determine readiness
const app = await builder.addProject("app", "./App/App.csproj", "https")
.waitFor(rabbit);
```
* C#
AppHost.cs
```csharp
builder.Services.AddHealthChecks()
.AddCheck("rabbitmq-health", () =>
{
// Implement your custom health check logic here,
// for example, attempting a TCP connection to the service
return HealthCheckResult.Healthy();
});
var rabbit = builder.AddContainer("rabbitmq", "rabbitmq", "4.1.4-management-alpine")
.WithHealthCheck("rabbitmq-health");
// WaitFor uses the registered health check to determine readiness
var app = builder.AddProject("app")
.WaitFor(rabbit);
```
Note
Aspire integration packages (like `Aspire.Hosting.PostgreSQL` or `Aspire.Hosting.Redis`) include built-in health checks. You don’t need to define custom health checks for these services — `WaitFor()` automatically waits for the built-in health check to pass.
## Next steps
[Section titled “Next steps”](#next-steps)
After migrating to Aspire:
* Explore [Aspire integrations](/integrations/overview/) to replace custom container configurations
* Set up [health checks](/fundamentals/health-checks/) for better monitoring
* Learn about [deployment options](/deployment/deploy-with-aspire/) for production environments
* Consider [testing](/testing/overview/) your distributed application
* Review [telemetry configuration](/fundamentals/telemetry/) for observability
# Configure resource lifetimes in Aspire
> Learn how session, persistent, resource-scoped, and parent-process lifetimes, plus explicit start, control Aspire containers, executables, and projects.
Aspire resources support a number of different lifetime modes. For example, the default **session lifetime** starts a resource when the AppHost starts and shuts it down when the AppHost exits. A **persistent lifetime** leaves a resource running when the AppHost exits and can reuse the same instance on the next run.
Resource lifetimes apply to containers, executables, and projects. Persistent executable and project lifetimes are experimental in Aspire 13.4. You can use different lifetimes for resources that take time to initialize, need stable local endpoints, should remain available while you restart or rebuild the AppHost, or need to match another resource’s lifetime.
Experimental shared lifetime APIs
The shared lifetime APIs are experimental and emit the [`ASPIREPERSISTENCE001`](/diagnostics/aspirepersistence001/) diagnostic. The existing container-specific `WithLifetime(ContainerLifetime.Persistent)` and `WithLifetime(ContainerLifetime.Session)` APIs remain supported for container resources.
## Lifetime modes
[Section titled “Lifetime modes”](#lifetime-modes)
Use the shared lifetime APIs for new code. They support container, executable, and project resources.
### Session lifetime
[Section titled “Session lifetime”](#session-lifetime)
A session lifetime creates the resource when the AppHost starts and disposes of it when the AppHost stops. This is the default lifetime for resources, so you usually don’t need to configure it explicitly.
Use session lifetime for resources that should only exist while the AppHost is running, such as local test dependencies, temporary containers, or processes that don’t need stable state across runs. Session resources also default to proxied endpoints, which are available while the AppHost is running.
If you previously configured another lifetime and want to return a resource to the default behavior, call [`WithSessionLifetime`](/reference/api/csharp/aspire.hosting/resourcebuilderextensions/methods/#withsessionlifetime "Configures a resource to use a session lifetime.")[`withSessionLifetime`](/reference/api/typescript/aspire.hosting/withsessionlifetime/ "Configures a resource to use a session lifetime."). For container resources, `WithLifetime(ContainerLifetime.Session)` is still supported.
### Persistent lifetime
[Section titled “Persistent lifetime”](#persistent-lifetime)
A persistent lifetime reuses a previously created resource when possible and doesn’t dispose of it when the AppHost stops. Configure this behavior with [`WithPersistentLifetime`](/reference/api/csharp/aspire.hosting/resourcebuilderextensions/methods/#withpersistentlifetime "Configures a resource to use a persistent lifetime.")[`withPersistentLifetime`](/reference/api/typescript/aspire.hosting/withpersistentlifetime/ "Configures a resource to use a persistent lifetime."), or with the existing container-specific `WithLifetime(ContainerLifetime.Persistent)` API for container resources.
Use persistent lifetime for resources that are expensive to initialize, need stable local endpoints, or should remain available while you restart or rebuild the AppHost. Common examples include databases, message brokers, emulators, long-running executables, and project resources that should continue running after the AppHost exits.
Configuration changes can recreate persistent resources
Persistent resources are automatically recreated when the AppHost detects meaningful configuration changes. If the configuration differs, the resource is recreated with the new settings.
Persistent containers use proxied endpoints by default, just like session containers. The proxy runs only while the AppHost is running, so the proxy address isn’t reachable after the AppHost stops. Persistent executables and projects default to proxyless endpoints so their direct addresses stay stable and reachable even after the AppHost stops.
You can still configure endpoint proxy behavior explicitly on any persistent resource. Set `isProxied: false` on an individual endpoint, or call `WithEndpointProxySupport(false)` to make every endpoint on a resource proxyless. When a proxyless endpoint doesn’t specify a public `port`, Aspire allocates one before the resource is created. For persistent resources, Aspire stores the allocated port in user secrets when user secrets are available and reuses it on later AppHost runs.
For more information, see [Allocate ports for dynamic proxyless endpoints](/fundamentals/networking-overview/#allocate-ports-for-dynamic-proxyless-endpoints).
Persistent container ≠ persistent data
Persistent container lifetime doesn’t guarantee data durability. For details, see [Container lifetime vs. data durability](#container-lifetime-vs-data-durability).
Persistent resources and replicas
Persistent resources don’t support replicas because they depend on a single unique resource identifier to be resolved across AppHost runs.
Persistent resources aren’t compatible with Aspire IDE debugging sessions. If you need to debug a persistent executable or project, use your debugger’s attach mode if one is available.
### Parent-process lifetime
[Section titled “Parent-process lifetime”](#parent-process-lifetime)
A parent-process lifetime keeps a resource available across AppHost restarts, but scopes cleanup to a parent process. Configure this behavior with `WithParentProcessLifetime(processId)`.
Use parent-process lifetime for resources that should outlive an individual AppHost run but still be cleaned up when a broader development tool, IDE, or other owning process exits.
Parent-process lifetime resources share persistent resource behavior across AppHost runs. If Aspire detects meaningful configuration changes on a subsequent run, the resource is recreated with the new settings.
The parent process ID must be the valid ID of a running process. Aspire records both the process ID and the process identity timestamp so cleanup follows the specific process instance instead of accidentally matching a reused process ID.
### Resource-scoped lifetime
[Section titled “Resource-scoped lifetime”](#resource-scoped-lifetime)
A resource-scoped lifetime configures one resource to use another resource’s effective lifetime. Configure this behavior with `WithLifetimeOf(resource)`.
Use resource-scoped lifetime when a companion resource should follow the lifetime choice of another resource. This is useful for sidecars, helper executables, or child resources that should become persistent only when the resource they support is persistent.
Aspire evaluates the source resource’s lifetime when it prepares the application model, so later lifetime changes to the source resource are reflected by the dependent resource. The source and dependent resources must both support lifetime configuration.
## Defer resource start with explicit start
[Section titled “Defer resource start with explicit start”](#defer-resource-start-with-explicit-start)
Use [`WithExplicitStart`](/reference/api/csharp/aspire.hosting/resourcebuilderextensions/methods/#withexplicitstart "Adds a Aspire.Hosting.ApplicationModel.ExplicitStartupAnnotation annotation to the resource so it doesn't automatically start with the AppHost startup.")[`withExplicitStart`](/reference/api/typescript/aspire.hosting/withexplicitstart/ "Prevents resource from starting automatically") to prevent a resource from starting automatically with the rest of the AppHost. The resource appears in the dashboard but remains stopped until you start it manually.
Starting in Aspire 13.5, `WithExplicitStart()` also affects when execution configuration callbacks (for example, [`WithEnvironment`](/reference/api/csharp/aspire.hosting/resourcebuilderextensions/methods/#withenvironment "Adds an environment variable to the resource.")[`withEnvironment`](/reference/api/typescript/aspire.hosting/withenvironment/ "Sets an environment variable") and [`WithArgs`](/reference/api/csharp/aspire.hosting/resourcebuilderextensions/methods/#withargs "Adds arguments to be passed to a resource that supports arguments when it is launched.")[`withArgs`](/reference/api/typescript/aspire.hosting/withargs/ "Adds arguments to be passed to a resource that supports arguments when it is launched.")) are evaluated, depending on whether the resource is session-scoped or persistent.
### Session-scoped explicit-start resources
[Section titled “Session-scoped explicit-start resources”](#session-scoped-explicit-start-resources)
For session-scoped resources (the default lifetime), Aspire defers Developer Control Plane (DCP) registration until you manually start the resource from the dashboard. This means execution configuration callbacks — such as `WithEnvironment(context => ...)` — are **not** evaluated during AppHost startup. They run only when the resource is manually started.
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
// The callback below is NOT evaluated during AppHost startup.
// It runs only when the resource is manually started from the dashboard.
var job = builder.AddExecutable("batch-job", "dotnet", ".", "run", "--project", "BatchJob")
.WithExplicitStart()
.WithEnvironment(async context =>
{
// Prompt or compute dynamic configuration at start time
context.EnvironmentVariables["API_KEY"] = await GetApiKeyAsync();
});
builder.Build().Run();
```
Placeholder API used for demonstration
`GetApiKeyAsync` is a placeholder used to demonstrate deferred, callback-based configuration. Its implementation isn’t shown because the relevant behavior is retrieving a value when the callback runs and assigning that value to an environment variable on the target resource.
### Persistent explicit-start resources
[Section titled “Persistent explicit-start resources”](#persistent-explicit-start-resources)
For persistent resources, Aspire must register the resource with the Developer Control Plane (DCP) immediately at startup so it can discover any existing running instance. However, when you manually start a persistent explicit-start resource, Aspire patches the existing DCP resource to start it rather than deleting and recreating it. This means the execution configuration callbacks run once during startup registration and are **not** re-evaluated when you manually start the resource.
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
// Persistent explicit-start resources are registered at startup to detect existing instances.
// The callback runs during startup registration — not again when manually started.
#pragma warning disable ASPIREPERSISTENCE001
var cache = builder.AddContainer("long-lived-cache", "my-cache-image")
.WithPersistentLifetime()
.WithExplicitStart()
.WithEnvironment(context =>
{
context.EnvironmentVariables["CACHE_SIZE"] = "512mb";
});
#pragma warning restore ASPIREPERSISTENCE001
builder.Build().Run();
```
## Configure a persistent container
[Section titled “Configure a persistent container”](#configure-a-persistent-container)
For new code, configure a persistent container with `WithPersistentLifetime()`:
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const postgres = await builder.addPostgres('postgres');
await postgres.withPersistentLifetime();
await postgres.withDataVolume();
const db = postgres.addDatabase('inventorydb');
const inventory = await builder.addProject(
'inventory',
'./InventoryService/InventoryService.csproj'
);
await inventory.withReference(db);
await builder.build().run();
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
var postgres = builder.AddPostgres("postgres")
.WithPersistentLifetime()
.WithDataVolume();
var db = postgres.AddDatabase("inventorydb");
builder.AddProject("inventory")
.WithReference(db);
builder.Build().Run();
```
In the preceding example, the PostgreSQL container persists between AppHost runs, and `WithDataVolume()` stores database data in a named volume that survives container recreation. The `inventory` project references the database as normal.
## Configure a persistent executable
[Section titled “Configure a persistent executable”](#configure-a-persistent-executable)
Executable resources can also use persistent lifetimes. Persistent executables are useful for local services that have expensive startup, need stable process identity, or should remain reachable while the AppHost restarts.
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const worker = await builder.addExecutable('worker', 'node', '../worker', [
'server.js',
]);
await worker.withHttpEndpoint({ port: 5050, targetPort: 5050 });
await worker.withPersistentLifetime();
await builder.build().run();
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
var worker = builder.AddExecutable("worker", "node", "../worker", "server.js")
.WithHttpEndpoint(port: 5050, targetPort: 5050)
.WithPersistentLifetime();
builder.Build().Run();
```
The preceding example configures a concrete `port` so the endpoint address is explicit. If you omit the public `port`, Aspire allocates one before the executable starts and reuses it from user secrets on later AppHost runs when user secrets are available.
## Configure a persistent project
[Section titled “Configure a persistent project”](#configure-a-persistent-project)
Project resources can use persistent lifetimes when you want the project process to continue running after the AppHost exits.
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const api = await builder.addProject('api', '../ApiService/ApiService.csproj');
await api.withPersistentLifetime();
await builder.build().run();
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
var api = builder.AddProject("api")
.WithPersistentLifetime();
builder.Build().Run();
```
Persistent project and executable resources are run by Aspire’s orchestrator so it can manage their lifecycle consistently. Persistent project and executable resources don’t support replicas.
## Match another resource’s lifetime
[Section titled “Match another resource’s lifetime”](#match-another-resources-lifetime)
Use [`WithLifetimeOf`](/reference/api/csharp/aspire.hosting/resourcebuilderextensions/methods/#withlifetimeof "Configures a resource to match the lifetime of another resource.")[`withLifetimeOf`](/reference/api/typescript/aspire.hosting/withlifetimeof/ "Configures a resource to match the lifetime of another resource.") when a companion resource should follow another resource’s lifetime. This is useful when a sidecar, helper process, or supporting service should become persistent only when its source resource is persistent.
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const database = await builder.addPostgres('postgres');
await database.withPersistentLifetime();
await database.withDataVolume();
const companion = await builder.addExecutable(
'companion',
'dotnet',
'../Companion',
['Companion.dll']
);
await companion.withLifetimeOf(database);
await builder.build().run();
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
var database = builder.AddPostgres("postgres")
.WithPersistentLifetime()
.WithDataVolume();
var companion = builder.AddExecutable("companion", "dotnet", "../Companion", "Companion.dll")
.WithLifetimeOf(database);
builder.Build().Run();
```
The dependent resource’s lifetime is evaluated when Aspire prepares the application model, so later changes to the source resource’s lifetime are reflected by the dependent resource.
## Scope cleanup to a parent process
[Section titled “Scope cleanup to a parent process”](#scope-cleanup-to-a-parent-process)
Use [`WithParentProcessLifetime`](/reference/api/csharp/aspire.hosting/resourcebuilderextensions/methods/#withparentprocesslifetime "Configures a resource to use a persistent lifetime that ends when a parent process exits.")[`withParentProcessLifetime`](/reference/api/typescript/aspire.hosting/withparentprocesslifetime/ "Configures a resource to use a persistent lifetime that ends when a parent process exits.") when a resource should survive AppHost restarts but be cleaned up when another process exits. Aspire records the parent process identity instead of retaining a live process handle, so the cleanup scope follows the specific process instance instead of a reused process ID.
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const parentProcessId = 1234;
const worker = await builder.addExecutable(
'scoped-worker',
'node',
'../worker',
['server.js']
);
await worker.withParentProcessLifetime(parentProcessId);
await builder.build().run();
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
var parentProcessId = int.Parse(builder.Configuration["RESOURCE_PARENT_PROCESS_ID"]!);
var worker = builder.AddExecutable("scoped-worker", "node", "../worker", "server.js")
.WithParentProcessLifetime(parentProcessId);
builder.Build().Run();
```
The parent process ID must be greater than zero and identify a running process.
## Use the container-specific lifetime API
[Section titled “Use the container-specific lifetime API”](#use-the-container-specific-lifetime-api)
The older container-specific lifetime API is still supported. Use `WithLifetime(ContainerLifetime.Persistent)` to keep a container running across AppHost restarts, or `WithLifetime(ContainerLifetime.Session)` to explicitly use the default session behavior.
For new code, prefer the shared `WithPersistentLifetime()` and `WithSessionLifetime()` APIs because they work consistently across containers, executables, and projects.
* TypeScript
apphost.mts
```typescript
import { ContainerLifetime, createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const postgres = await builder.addPostgres('postgres');
await postgres.withLifetime(ContainerLifetime.Persistent);
await postgres.withDataVolume();
await builder.build().run();
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
var postgres = builder.AddPostgres("postgres")
.WithLifetime(ContainerLifetime.Persistent)
.WithDataVolume();
builder.Build().Run();
```
## Dashboard visualization
[Section titled “Dashboard visualization”](#dashboard-visualization)
The Aspire dashboard shows persistent resources with a distinctive pin icon to help you identify them:

After the AppHost stops, persistent containers continue running and can be seen in your container runtime (such as Docker Desktop):

## Container naming and uniqueness
[Section titled “Container naming and uniqueness”](#container-naming-and-uniqueness)
By default, persistent containers use a naming pattern that combines:
* The service name you specify in your AppHost.
* A postfix based on a hash of the AppHost project path.
This naming scheme ensures that persistent containers are unique to each AppHost project, preventing conflicts when multiple Aspire projects use the same service names.
For example, if you have a service named `"postgres"` in an AppHost project located at `/path/to/MyApp.AppHost`, the container name might be `postgres-abc123def` where `abc123def` is derived from the project path hash.
### Custom container names
[Section titled “Custom container names”](#custom-container-names)
For advanced scenarios, you can set a custom container name using the [`WithContainerName`](/reference/api/csharp/aspire.hosting/containerresourcebuilderextensions/methods/#withcontainername "Overrides the default container name for this resource. By default Aspire generates a unique container name based on the resource name and a random postfix...")[`withContainerName`](/reference/api/typescript/aspire.hosting/withcontainername/ "Overrides the default container name for this resource. By default Aspire generates a unique container name based on the resource name and a random postfix...") method:
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const postgres = await builder.addPostgres('postgres');
await postgres.withPersistentLifetime();
await postgres.withContainerName('my-shared-postgres');
await builder.build().run();
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
var postgres = builder.AddPostgres("postgres")
.WithPersistentLifetime()
.WithContainerName("my-shared-postgres");
builder.Build().Run();
```
When you specify a custom container name, Aspire first checks if a container with that name already exists. If a container with that name exists and was previously created by Aspire, it follows the normal persistent container behavior and can be automatically recreated if the configuration changes. If a container with that name exists but wasn’t created by Aspire, it won’t be managed or recreated by the AppHost. If no container with the custom name exists, Aspire creates a new one.
## Executable and project naming and uniqueness
[Section titled “Executable and project naming and uniqueness”](#executable-and-project-naming-and-uniqueness)
Persistent executable and project resources are scoped to a specific AppHost instance and uniquely identified by their resource name within that scope. Two executable or project resources with the same name in different AppHosts don’t collide with each other; they result in separate process instances.
## Manual cleanup
[Section titled “Manual cleanup”](#manual-cleanup)
Caution
Persistent resources aren’t automatically removed when you stop the AppHost. To delete them, stop and remove the underlying container or process with the resource’s runtime or operating system tools.
For persistent containers, use Docker CLI commands, Docker Desktop, or your preferred container management tool to stop and remove the container:
Stop and remove a persistent container
```bash
# Stop the container
docker stop my-container-name
# Remove the container
docker rm my-container-name
```
For persistent executable and project resources, stop the running process with your operating system process manager or terminal tools. You can also stop a persistent resource from the Aspire dashboard if the runtime-specific cleanup option isn’t straightforward.
## Container lifetime vs. data durability
[Section titled “Container lifetime vs. data durability”](#container-lifetime-vs-data-durability)
`WithPersistentLifetime()` and `WithDataVolume()` serve different purposes and are often used together. The following table summarizes the behavior of each combination for container resources:
| Configuration | Container behavior | Data behavior |
| -------------------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| Neither (default) | Created on start, destroyed on stop | Lost every time the AppHost stops |
| `WithPersistentLifetime()` only | Stays running between AppHost runs | Survives AppHost restarts, but **lost if the container is recreated** (config change, pruning, image update) |
| `WithDataVolume()` only | Created on start, destroyed on stop | Persists in a named volume—**survives container recreation** |
| Both (recommended for databases) | Stays running between AppHost runs | Persists in a named volume—survives container recreation |
For **databases and other stateful services**, use both APIs together so you get fast startup (the container stays running) *and* data safety (a volume protects data even if the container is recreated):
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const postgres = await builder.addPostgres('postgres');
await postgres.withPersistentLifetime();
await postgres.withDataVolume();
```
* C#
AppHost.cs
```csharp
var postgres = builder.AddPostgres("postgres")
.WithPersistentLifetime()
.WithDataVolume();
```
For **caches or other ephemeral state**, `WithPersistentLifetime()` alone may be sufficient because losing data on container recreation is acceptable.
Tip
For more details on volumes and bind mounts, see [Persist data using volumes](/fundamentals/persist-data-volumes/).
# Resource naming conventions
> Learn when Aspire preserves resource names in configuration keys and when it encodes names for endpoint and connection-property environment variables.
When you define resources in an Aspire AppHost, their names determine how configuration is surfaced to consuming services. This article explains which generated configuration keys preserve resource names, which environment variables encode them, and how to choose valid names.
The AppHost examples assume the named application projects already exist. A C# AppHost needs project references for the `Projects.*` types; a TypeScript AppHost needs the corresponding project paths. Install the hosting integrations used by each example: [Redis](/integrations/caching/redis/redis-host/), [PostgreSQL](/integrations/databases/postgres/postgres-host/), and [RabbitMQ](/integrations/messaging/rabbitmq/rabbitmq-host/). Running those resources requires a supported [container runtime](/get-started/prerequisites/).
## How resource names map to environment variables
[Section titled “How resource names map to environment variables”](#how-resource-names-map-to-environment-variables)
When a resource is referenced by another resource using `WithReference`, Aspire injects configuration into the consuming resource. Connection strings use the `ConnectionStrings__` pattern, and service discovery endpoints use `services______`.
Note
The connection string and service discovery formats preserve the resource name exactly, including dashes. Other injected forms, such as uppercase endpoint and connection-property variables, encode dashes as underscores. Aspire also provides a portable connection-string alias with dashes replaced by underscores. For more information, see [When Aspire encodes resource names](#when-aspire-encodes-resource-names).
### Connection string environment variables
[Section titled “Connection string environment variables”](#connection-string-environment-variables)
For connection strings, Aspire uses the following pattern:
```plaintext
ConnectionStrings__
```
For example, if you define a Redis resource named `cache`:
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const cache = await builder.addRedis('cache');
await builder.addProject('api', '../MyApi/MyApi.csproj').withReference(cache);
await builder.build().run();
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
var cache = builder.AddRedis("cache");
var api = builder.AddProject("api")
.WithReference(cache);
builder.Build().Run();
```
The `api` project receives a `ConnectionStrings__cache` environment variable containing the Redis connection string.
### Service discovery environment variables
[Section titled “Service discovery environment variables”](#service-discovery-environment-variables)
For service endpoints, Aspire injects variables following this pattern:
```plaintext
services______0
```
For example, a project named `catalog` with an `http` endpoint provides:
```plaintext
services__catalog__http__0=http://localhost:5123
```
Indices start at `0` separately for each endpoint name or scheme. An `http` endpoint and an `https` endpoint can therefore both use index `0`:
```plaintext
services__catalog__http__0=http://localhost:5123
services__catalog__https__0=https://localhost:7234
```
For endpoints named `http` or `https`, the key uses the endpoint’s current URI scheme. Other endpoint names use their configured name.
For more information, see [Service discovery](/fundamentals/service-discovery/).
## When Aspire encodes resource names
[Section titled “When Aspire encodes resource names”](#when-aspire-encodes-resource-names)
Aspire preserves the original resource name in connection string and service discovery keys. For the separately injected uppercase endpoint and connection-property variables, Aspire creates an environment-variable-safe prefix by replacing dashes with underscores.
The following AppHost defines resources whose names contain dashes:
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const postgres = await builder.addPostgres('postgres');
const db = await postgres.addDatabase('app-db');
const catalog = await builder.addProject(
'catalog-api',
'../CatalogApi/CatalogApi.csproj'
);
const worker = await builder.addProject('worker', '../Worker/Worker.csproj');
await worker.withReference(db);
await worker.withReference(catalog);
await builder.build().run();
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
var db = builder.AddPostgres("postgres").AddDatabase("app-db");
var catalog = builder.AddProject("catalog-api");
builder.AddProject("worker")
.WithReference(db)
.WithReference(catalog);
builder.Build().Run();
```
The `worker` resource receives different name forms according to the configuration type:
| Configuration type | Example environment variable | Resource name handling |
| -------------------------------- | -------------------------------- | ---------------------- |
| Connection string | `ConnectionStrings__app-db` | Preserved |
| Portable connection-string alias | `ConnectionStrings__app_db` | Encoded |
| Service discovery | `services__catalog-api__http__0` | Preserved |
| Uppercase endpoint | `CATALOG_API_HTTP` | Encoded |
Note
Connection-property variables use the same encoded uppercase prefix. For example, a `HOST` property exported for `app-db` uses the name `APP_DB_HOST`.
### Accessing the connection string in consuming services
[Section titled “Accessing the connection string in consuming services”](#accessing-the-connection-string-in-consuming-services)
In your .NET application, the configuration system maps double underscores (`__`) to the colon (`:`) separator used by `IConfiguration`. This means the environment variable `ConnectionStrings__app-db` is transformed to the configuration key `ConnectionStrings:app-db`:
Program.cs
```csharp
// This resolves ConnectionStrings__app-db (injected by Aspire)
var connectionString = builder.Configuration.GetConnectionString("app-db");
```
Tip
When using Aspire integration client APIs (for example, `AddNpgsqlDataSource("app-db")`), pass the original resource name with dashes — Aspire integrations resolve the correct environment variable internally.
For Python and JavaScript services, the double-underscore format is used directly:
app.py
```python
import os
connection_string = os.environ.get("ConnectionStrings__app-db")
```
app.js
```javascript
const connectionString = process.env['ConnectionStrings__app-db'];
```
## Naming best practices
[Section titled “Naming best practices”](#naming-best-practices)
Follow these recommendations when naming resources in your AppHost:
* **Prefer lowercase names**: Uppercase letters are allowed, but lowercase names match the conventions used by DNS names and container networking, so they read more consistently across the dashboard, environment variables, and logs.
* **Use dashes as separators**: Dashes (`-`) are the conventional separator for multi-word resource names in Aspire. Underscores aren’t valid in resource names.
* **Keep names short and descriptive**: Resource names appear in the Aspire dashboard, in environment variables, and in logs. Short names reduce visual noise.
* **Be consistent with related names**: If you name a server resource `postgres`, name related databases `postgres-appdb` rather than something unrelated.
The following example shows a set of clear, conventional resource names:
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const postgres = await builder.addPostgres('postgres');
const appDb = await postgres.addDatabase('app-db');
const cache = await builder.addRedis('cache');
const messageBus = await builder.addRabbitMQ('message-bus');
const api = await builder.addProject('api', '../Api/Api.csproj');
await api.withReference(appDb);
await api.withReference(cache);
await api.withReference(messageBus);
await builder.build().run();
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
var postgres = builder.AddPostgres("postgres");
var appDb = postgres.AddDatabase("app-db");
var cache = builder.AddRedis("cache");
var messageBus = builder.AddRabbitMQ("message-bus");
var api = builder.AddProject("api")
.WithReference(appDb)
.WithReference(cache)
.WithReference(messageBus);
builder.Build().Run();
```
Names to avoid, and why:
* `my_service` — underscores aren’t allowed in resource names.
* `web--api` — consecutive dashes aren’t allowed.
* `api-` — a name can’t end with a dash.
* `1api` — a name must start with a letter, not a digit.
## Resource name validation
[Section titled “Resource name validation”](#resource-name-validation)
Aspire validates resource names at startup. A valid resource name:
* Is between 1 and 64 characters long.
* Starts with an ASCII letter (`a`–`z` or `A`–`Z`).
* Contains only ASCII letters, digits (`0`–`9`), and dashes (`-`).
* Doesn’t contain consecutive dashes (`--`).
* Doesn’t end with a dash.
Note
Uppercase letters are permitted by validation, but lowercase names are the recommended convention. Underscores (`_`) aren’t valid in resource names.
In a C# AppHost, invalid names that the analyzer can determine from literal strings produce the [ASPIRE006](/diagnostics/aspire006/) build-time diagnostic. Aspire also validates names when constructing the application model and throws an `ArgumentException` for invalid names. This runtime validation applies to dynamically computed names and TypeScript AppHosts, which don’t receive the C# analyzer diagnostic.
## See also
[Section titled “See also”](#see-also)
* [Service discovery](/fundamentals/service-discovery/)
* [Inner-loop networking overview](/fundamentals/networking-overview/)
* [External parameters](/fundamentals/external-parameters/)
# TypeScript AppHost project structure
> Learn the files and configuration that make up a TypeScript AppHost project — entry point, package manifest, dependencies, and how the AppHost runs Aspire resources.
When you create a TypeScript AppHost with `aspire new`, the CLI scaffolds a project with the following structure:
* my-apphost/
* .aspire/modules/ Generated TypeScript SDK (do not edit)
* aspire.mts
* base.mts
* transport.mts
* apphost.mts Your AppHost entry point
* aspire.config.json Aspire configuration
* package.json
* tsconfig.apphost.json
When you run `aspire init --language typescript` in an existing JavaScript or TypeScript app that already has a root `package.json`, Aspire creates the AppHost in a nested `aspire-apphost/` package. The root `aspire.config.json` points to `aspire-apphost/apphost.mts`, and the root `package.json` gets Aspire delegate scripts so the existing app package keeps its own module and toolchain settings:
* my-existing-app/
* aspire-apphost/
* .aspire/modules/ Generated TypeScript SDK (do not edit)
* aspire.mts
* base.mts
* transport.mts
* apphost.mts Your AppHost entry point
* package.json
* tsconfig.apphost.json
* aspire.config.json Aspire configuration
* package.json Existing app package with Aspire delegate scripts
## aspire.config.json
[Section titled “aspire.config.json”](#aspireconfigjson)
The `aspire.config.json` file is the central configuration for your AppHost. It replaces the older `.aspire/settings.json` and `apphost.run.json` files.
aspire.config.json
```json
{
"appHost": {
"path": "apphost.mts",
"language": "typescript/nodejs"
},
"packages": {
"Aspire.Hosting.JavaScript": "13.6.0"
},
"profiles": {
"https": {
"applicationUrl": "https://localhost:17127;http://localhost:15118",
"environmentVariables": {
"ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21169",
"ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22260"
}
}
}
}
```
### Key sections
[Section titled “Key sections”](#key-sections)
| Section | Description |
| ------------------ | ------------------------------------------------------------------------------------- |
| `appHost.path` | Path to your AppHost entry point (`apphost.mts`) |
| `appHost.language` | Language runtime (`typescript/nodejs`) |
| `packages` | Hosting integration packages and their versions. Added automatically by `aspire add`. |
| `profiles` | Launch profiles with dashboard URLs and environment variables |
### Add and restore integrations
[Section titled “Add and restore integrations”](#add-and-restore-integrations)
Use `aspire add` from the AppHost root to add hosting integrations. The CLI adds the package to the `packages` section, restores AppHost dependencies, and regenerates the TypeScript SDK in `.aspire/modules/`:
Add an integration
```bash
aspire add redis
```
This updates `aspire.config.json` so the package is restored the next time the AppHost runs:
aspire.config.json
```diff
{
"packages": {
"Aspire.Hosting.JavaScript": "13.6.0",
"Aspire.Hosting.Redis": "13.6.0"
}
}
```
Run `aspire restore` when you want to regenerate `.aspire/modules/` without starting the AppHost, such as after switching branches, updating package versions, or preparing a CI job:
Restore a TypeScript AppHost
```bash
aspire restore
```
### Project references for local development
[Section titled “Project references for local development”](#project-references-for-local-development)
You can reference a local hosting integration project by using a `.csproj` path instead of a version:
aspire.config.json
```json
{
"packages": {
"MyIntegration": "../src/MyIntegration/MyIntegration.csproj"
}
}
```
See [Multi-language integrations](/extensibility/multi-language-integration-authoring/) for details on building hosting integrations that work with TypeScript AppHosts.
## .aspire/modules/ directory
[Section titled “.aspire/modules/ directory”](#aspiremodules-directory)
The `.aspire/modules/` directory under the AppHost root contains the generated TypeScript SDK. It’s created and updated automatically by the Aspire CLI — **do not edit these files**.
| File | Purpose |
| --------------- | ------------------------------------------------------- |
| `aspire.mts` | Generated typed API for all your installed integrations |
| `base.mts` | Base types and handle infrastructure |
| `transport.mts` | JSON-RPC transport layer |
The SDK regenerates when:
* You run `aspire add` to add or update an integration
* You run `aspire run` or `aspire start` and the package list has changed
* You run `aspire restore` to manually regenerate
Your `apphost.mts` imports from this SDK:
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
```
Tip
Add `.aspire/` to your `.gitignore` — it contains generated artifacts that can be recreated from `aspire.config.json` at any time.
## apphost.mts
[Section titled “apphost.mts”](#apphostmts)
The entry point for your AppHost. This is where you define your application’s resources and their relationships:
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const cache = await builder.addRedis("cache");
const api = await builder
.addNodeApp("api", "./api", "src/index.ts")
.withHttpEndpoint({ env: "PORT" })
.withReference(cache);
await builder.build().run();
```
Note
Projects created with Aspire CLI versions earlier than 13.4 used `apphost.ts` and `./.modules/`. They continue to work on Aspire 13.4. For compatibility details and optional migration steps, see [Legacy `apphost.ts` projects in the Aspire 13.4 release notes](/whats-new/aspire-13-4/#legacy-apphostts-projects-pre-134).
## Configuration with appsettings.json
[Section titled “Configuration with appsettings.json”](#configuration-with-appsettingsjson)
TypeScript AppHosts read configuration from JSON files that live next to `apphost.mts`. Add an `appsettings.json` file to your AppHost and Aspire loads it into the AppHost’s configuration at startup. You read the effective values in `apphost.mts` through the builder’s configuration accessor.
* example-app/
* .aspire/modules/
* …
* apphost.mts
* aspire.config.json
* appsettings.json Base configuration
* appsettings.Development.json Environment-specific overrides
* appsettings.Production.json Environment-specific overrides
* package.json
Start with base settings as plain JSON. Settings can be flat, like `message`, or grouped in nested objects, like `deployment.region`. Keys are matched case-insensitively, so camelCase fits naturally in a TypeScript project:
appsettings.json
```json
{
"message": "From appsettings.json",
"deployment": {
"region": "westus"
}
}
```
With only `appsettings.json` present, read a flat value by its key. For a nested value, use `:` to separate each level of the JSON path. The `getConfigValue()` method returns `null` if the key isn’t found:
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const configuration = await builder.getConfiguration();
const message = await configuration.getConfigValue('message');
// message === 'From appsettings.json';
const region = await configuration.getConfigValue('deployment:region');
// region === 'westus';
```
### Environment-specific settings
[Section titled “Environment-specific settings”](#environment-specific-settings)
Aspire always loads `appsettings.json` first, then layers `appsettings.{Environment}.json` on top. The value passed to `--environment` replaces `{Environment}` in the filename. For example, `--environment Development` loads `appsettings.Development.json`, while `--environment Production` loads `appsettings.Production.json`.
The environment-specific file overrides matching values from `appsettings.json`. The merged configuration is available through `getConfiguration()` when the AppHost starts.
#### Development
[Section titled “Development”](#development)
Add the Development overrides:
appsettings.Development.json
```json
{
"message": "From appsettings.Development.json",
"deployment": {
"region": "westus2"
}
}
```
Start the AppHost with the `Development` environment:
Start with Development settings
```bash
aspire start --environment Development
```
The lookups in `apphost.mts` now return the Development values:
apphost.mts — Development values
```typescript
const message = await configuration.getConfigValue('message');
// message === 'From appsettings.Development.json';
const region = await configuration.getConfigValue('deployment:region');
// region === 'westus2';
```
#### Production
[Section titled “Production”](#production)
Add the Production overrides:
appsettings.Production.json
```json
{
"message": "From appsettings.Production.json",
"deployment": {
"region": "eastus2"
}
}
```
Start the AppHost with the `Production` environment:
Start with Production settings
```bash
aspire start --environment Production
```
The same lookups now return the Production values:
apphost.mts — Production values
```typescript
const message = await configuration.getConfigValue('message');
// message === 'From appsettings.Production.json';
const region = await configuration.getConfigValue('deployment:region');
// region === 'eastus2';
```
For the full list of run options, see the [`aspire start` CLI reference](/reference/cli/commands/aspire-start/).
Note
Aspire looks for `appsettings.json` and `appsettings.{Environment}.json` in the same directory as `apphost.mts`. If more than one mechanism sets the active environment, `--environment` takes precedence over `DOTNET_ENVIRONMENT` and `ASPIRE_ENVIRONMENT`. Environment variables and command-line arguments take precedence over both JSON files, so you can override settings at run time without editing JSON.
## Package managers
[Section titled “Package managers”](#package-managers)
The Aspire CLI supports the following package managers at the **AppHost root** — the directory that contains your `apphost.mts` and `aspire.config.json`. The CLI selects between them by inspecting package manager signals, including the `packageManager` field in `package.json`, lock files, and package manager configuration in the AppHost root.
| Package manager | Detection signals | Version expectation |
| ----------------- | ------------------------------------------------------------------------- | ----------------------------------- |
| npm | `packageManager`, `package-lock.json`, or no other signal | npm 10 or later; npm is the default |
| pnpm | `packageManager` or `pnpm-lock.yaml` | pnpm 10 or later |
| Yarn | `packageManager`, `yarn.lock`, `.yarnrc.yml`, or `.yarn/` | Yarn 4 or later (Berry) |
| Bun | `packageManager`, `bun.lock`, or `bun.lockb` | Bun 1.2 or later |
| Deno | `packageManager`, `deno.lock`, `deno.json`, or `deno.jsonc` | Deno 2 or later |
| Yarn Classic (v1) | `yarn.lock` with `# yarn lockfile v1` or `packageManager` with `yarn@1.x` | Not supported |
This policy governs the **AppHost root only**. Apps the AppHost orchestrates — for example, a Node.js service added with [`AddNodeApp`](/reference/api/csharp/aspire.hosting.javascript/javascripthostingextensions/methods/#addnodeapp "Adds a node application to the application model. Node should be available on the PATH.")[`addNodeApp`](/reference/api/typescript/aspire.hosting.javascript/addnodeapp/ "Adds a node application to the application model. Node should be available on the PATH."), a Bun guest app, or a workspace package — can use any package manager their own tooling requires; they are independent of the AppHost-root toolchain.
Aspire end-to-end tests cover TypeScript AppHosts with representative `packageManager` pins such as `npm@10.0.0`, `pnpm@10.0.0`, `yarn@4.14.1`, and `bun@1.2.0`. These tested versions are representative points within the supported ranges, not the only versions you can use.
Caution
**Yarn Classic (v1) is not supported.** If the Aspire CLI detects a Yarn Classic lock file (`# yarn lockfile v1`) or a `packageManager` field such as `"yarn@1.x"` in `package.json`, it throws an error and stops. Upgrade to Yarn 4 or later, or switch to npm, pnpm, Bun, or Deno.
To upgrade to Yarn 4, run:
Upgrade to Yarn 4
```bash
corepack enable yarn
corepack use yarn@stable
```
## package.json
[Section titled “package.json”](#packagejson)
The scaffolded `package.json` includes convenience scripts and the required Node.js version:
package.json
```json
{
"name": "my-apphost",
"private": true,
"type": "module",
"scripts": {
"aspire:lint": "eslint apphost.mts",
"aspire:start": "aspire run",
"aspire:build": "tsc -p tsconfig.apphost.json",
"aspire:dev": "tsc --watch -p tsconfig.apphost.json",
"lint": "npm run aspire:lint",
"dev": "npm run aspire:start",
"build": "npm run aspire:build",
"watch": "npm run aspire:dev"
},
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
}
}
```
The `dev` script means you can also start your AppHost with `npm run dev` (or the equivalent for your toolchain, for example `bun run dev` or `pnpm run dev`).
### Supported Node.js engine
[Section titled “Supported Node.js engine”](#supported-nodejs-engine)
When using Node.js, TypeScript AppHosts target the engine range that `aspire init` writes into the scaffolded AppHost `package.json`. Deno and Bun run the AppHost with their own runtimes:
package.json — supported engines.node
```json
{
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
}
}
```
The supported ranges are:
* **Node.js 20.19+** — supported.
* **Node.js 22.13+** (22.x LTS) — supported.
* **Node.js 24.x and later** — supported by the scaffolded `engines.node` constraint.
Older Node.js versions are not supported. Package managers may enforce `engines.node` on install (for example, pnpm does by default, while npm only does when `engine-strict` is configured), so unsupported runtimes are best treated as blocked even when a given toolchain only warns.
Note
If you widen `engines.node` beyond the scaffolded constraint, you take on responsibility for compatibility. Aspire CLI behavior, TypeScript SDK generation, and the `tsc --noEmit` startup validation are tested against the ranges listed above.
## Package manager toolchain
[Section titled “Package manager toolchain”](#package-manager-toolchain)
The Aspire CLI automatically detects your TypeScript AppHost toolchain and adjusts install and run commands accordingly. The following toolchains are supported: **npm** (default), **Bun**, **Yarn**, **pnpm**, and **Deno**.
### Toolchain detection
[Section titled “Toolchain detection”](#toolchain-detection)
Detection follows the [supported AppHost-root package managers](#package-managers) policy. The CLI resolves the toolchain by inspecting the AppHost directory and its parent directories (up to eight levels). It checks, in order:
1. The `packageManager` field in `package.json` — for example, `"packageManager": "pnpm@10.0.0"`.
2. Toolchain markers: `bun.lock` or `bun.lockb` → Bun; `pnpm-lock.yaml` → pnpm; `yarn.lock`, `.yarnrc.yml`, or a `.yarn/` directory → Yarn; `deno.lock`, `deno.json`, or `deno.jsonc` → Deno.
3. If nothing is found, npm is used as the default.
The search walks up parent directories, so a workspace-level `packageManager` setting or lockfile is picked up automatically by nested AppHosts.
### Declaring your toolchain
[Section titled “Declaring your toolchain”](#declaring-your-toolchain)
The recommended way to pin the toolchain is with the `packageManager` field in `package.json`, which Aspire uses for toolchain detection. This field is also used by [Node.js Corepack](https://nodejs.org/api/corepack.html) for package managers such as Yarn and pnpm:
* npm (default)
No extra configuration is required — npm is the default. If you want to pin npm explicitly, set the `packageManager` field:
package.json
```diff
{
"packageManager": "npm@10.0.0",
"name": "my-apphost",
"private": true,
"type": "module"
}
```
* pnpm
package.json
```diff
{
"packageManager": "pnpm@10.0.0",
"name": "my-apphost",
"private": true,
"type": "module"
}
```
* Yarn
package.json
```diff
{
"packageManager": "yarn@4.14.1",
"name": "my-apphost",
"private": true,
"type": "module"
}
```
* Bun
package.json
```diff
{
"packageManager": "bun@1.2.0",
"name": "my-apphost",
"private": true,
"type": "module"
}
```
Alternatively, committing a toolchain-specific lockfile (`pnpm-lock.yaml`, `yarn.lock`, `bun.lock`, etc.) is sufficient for detection.
### Install and run commands by toolchain
[Section titled “Install and run commands by toolchain”](#install-and-run-commands-by-toolchain)
When a non-npm toolchain is detected, the CLI substitutes the matching commands:
| Toolchain | Install command | Execute command | Watch command |
| --------- | --------------- | ----------------------------------------------------- | --------------------------------------------------------------------- |
| npm | `npm install` | `npx tsx ...` | `npx nodemon ...` |
| pnpm | `pnpm install` | `pnpm exec tsx ...` | `pnpm exec nodemon ...` |
| Yarn | `yarn install` | `yarn exec tsx ...` | `yarn exec nodemon ...` |
| Bun | `bun install` | `bun run {appHostFile}` | `bun --watch run {appHostFile}` |
| Deno | `deno install` | `deno run -A --unstable-sloppy-imports {appHostFile}` | `deno run -A --unstable-sloppy-imports --check --watch {appHostFile}` |
Note
Bun has built-in TypeScript support, so it runs `apphost.mts` directly without `tsx`.
### Use Deno as the AppHost runtime
[Section titled “Use Deno as the AppHost runtime”](#use-deno-as-the-apphost-runtime)
Install Deno 2 or later and select it with a Deno configuration file, lockfile, or a `packageManager` entry such as `"deno@2.0.0"`. The generated AppHost module and resource APIs stay the same. This is distinct from [hosting a Deno application](/integrations/frameworks/deno/deno-host/): [`AddDenoApp`](/reference/api/csharp/aspire.hosting.javascript/javascripthostingextensions/methods/#adddenoapp "Adds a Deno application to the application model. Deno should be available on the PATH.")[`addDenoApp`](/reference/api/typescript/aspire.hosting.javascript/adddenoapp/ "Adds a Deno application to the application model. Deno should be available on the PATH.") models a guest resource and doesn’t choose the AppHost runtime.
Aspire restores dependencies with `deno install` and validates the AppHost with `deno check`. Deno uses its own configuration rather than `tsconfig.apphost.json`. Watch mode uses Deno’s native watcher and checks types on every restart, without `tsx` or `nodemon`. The CLI rejects Deno 1.x.
AppHost permissions
Aspire starts a Deno AppHost with `--allow-all` because the AppHost needs environment, filesystem, network, and process access to orchestrate resources. This isn’t a Deno sandbox: run only AppHost code and integrations you trust.
### aspire doctor checks
[Section titled “aspire doctor checks”](#aspire-doctor-checks)
The `aspire doctor` command checks that the selected toolchain executable is available. If Bun, Yarn, pnpm, or Deno is detected but not installed, the command reports an error with install guidance. For Deno, it also checks the Deno 2 minimum version.
Aspire CLI
```bash
aspire doctor
```
## Async chaining
[Section titled “Async chaining”](#async-chaining)
TypeScript AppHosts support fluent chaining for builder methods — for example, `builder.addContainer(...).withReference(...)` — so you can build resource graphs in a compact, readable style. Starting with Aspire 13.4, the generated SDK extends this to **all** generated async methods that return a chainable wrapper type: environment helpers, execution-context queries, and endpoint property accessors.
Previously, using these methods required splitting the chain or using a double `await`:
apphost.ts (before)
```typescript
// Two separate awaits were needed when chaining through async wrapper-returning methods
const envContext = await builder.environment();
const isDevelopment = await envContext.isDevelopment();
```
Now you can chain through them with a **single `await`**:
apphost.ts (after)
```typescript
const isDevelopment = await builder.environment().isDevelopment();
const isRunMode = await context.executionContext().isRunMode();
const endpointHost = await container.getEndpoint("http").property(EndpointProperty.Host);
```
This works because the code generator now emits a thenable wrapper for every generated async method whose return type is itself a chainable wrapper.
Note
The thenable wrappers are generated automatically — you do not need to change `aspire.config.json` or run any extra commands. Re-running `aspire run` or `aspire restore` after upgrading your `Aspire.Hosting.JavaScript` package version regenerates the `.aspire/modules/` SDK with the updated types.
## TypeScript validation before startup
[Section titled “TypeScript validation before startup”](#typescript-validation-before-startup)
Before starting a TypeScript AppHost, the Aspire CLI runs `tsc --noEmit`, or `deno check` for Deno, to check for type errors to prevent the dashboard and resources from starting in a partially broken state. If your AppHost has TypeScript compile errors, `aspire run` and `aspire publish` stop before the AppHost launches and display the diagnostic output:
```text
apphost.mts(22,7): error TS2322: Type 'string' is not assignable to type 'number'.
```
### Watch mode behavior
[Section titled “Watch mode behavior”](#watch-mode-behavior)
When you use `aspire run` in watch mode with a nodemon-based toolchain, TypeScript validation is embedded in the restart command. The watcher **can still start** even if there are initial type errors — it will recover automatically as you edit and save files that fix the errors. Deno instead uses its native `--watch --check` flow.
## HTTPS development certificates
[Section titled “HTTPS development certificates”](#https-development-certificates)
When you run a TypeScript AppHost with an HTTPS launch profile, the Aspire CLI needs a trusted HTTPS development certificate to be present on your machine. Unlike .NET AppHost users who typically have the .NET SDK on their `PATH`, TypeScript AppHost users may not have `dotnet` available, so the Aspire CLI provides its own certificate management commands.
If you see an error similar to the following when running `aspire run`:
```text
Unable to configure HTTPS endpoint. No server certificate was specified, and the default
developer certificate could not be found or is out of date. To generate and trust a
developer certificate run 'aspire certs trust'. For more information on configuring
HTTPS see https://aspire.dev/docs/.
```
Run the following command to create and trust the development certificate:
Aspire CLI
```bash
aspire certs trust
```
Tip
If you continue to see certificate errors after running `aspire certs trust`, try cleaning existing certificates first:
Aspire CLI
```bash
aspire certs clean
aspire certs trust
```
### Trusting the certificate for outbound TLS connections
[Section titled “Trusting the certificate for outbound TLS connections”](#trusting-the-certificate-for-outbound-tls-connections)
When your AppHost code opens TLS connections to Aspire-managed resources at runtime (for example, connecting directly to the dashboard’s OTLP endpoint from custom AppHost logic), the Aspire CLI ensures Node.js trusts the same development certificate that the Developer Control Plane (DCP) uses. The CLI exports the trusted development certificate into a content-addressed PEM cache under the Aspire home directory (`ASPIRE_HOME`), and Node.js is configured to trust that bundle through the `NODE_EXTRA_CA_CERTS` environment variable.
If you’ve already set `NODE_EXTRA_CA_CERTS` for your own certificates, the CLI preserves your value by generating a combined bundle that includes both your certificates and the Aspire development certificate, rather than overwriting your setting.
For a Deno AppHost, Aspire uses `DENO_CERT` for the trusted PEM bundle, preserving an existing bundle by combining it with Aspire’s development certificate. This works across Deno 2 releases; `NODE_EXTRA_CA_CERTS` support in Deno starts only with Deno 2.8.
See [Certificate configuration](/app-host/certificate-configuration/) for details on HTTPS certificate management in Aspire, including Linux-specific setup.
## Troubleshooting codegen failures
[Section titled “Troubleshooting codegen failures”](#troubleshooting-codegen-failures)
When the Aspire CLI runs a TypeScript (Node.js) AppHost, it contacts a managed server to generate the TypeScript SDK. That server loads NuGet-restored packages (`Aspire.Hosting.JavaScript`, `Aspire.TypeSystem`, and related assemblies). If the CLI version and the SDK version in `aspire.config.json` differ in major, minor, patch, or prerelease identifiers (excluding build metadata), code generation can fail.
### CLI/SDK version mismatch warning
[Section titled “CLI/SDK version mismatch warning”](#clisdk-version-mismatch-warning)
Before starting the AppHost, the CLI compares its built-in SDK version against the SDK version used for code generation (the `sdk.version` value in `aspire.config.json`, or the target version during `aspire update`). When the CLI is older than the SDK, the CLI prints a warning:
```text
⚠ The installed Aspire CLI version () differs from the Aspire SDK
version () used for code generation. If you run into errors,
update the Aspire CLI to match or exceed the SDK version.
```
A newer CLI running an AppHost pinned to an older SDK doesn’t produce the warning, and build metadata is ignored. The AppHost still starts after the warning. If codegen succeeds, no further action is required. If codegen fails, update the CLI with `aspire update --self`.
### Codegen failure output
[Section titled “Codegen failure output”](#codegen-failure-output)
When code generation fails, the CLI exits immediately and prints:
```text
❌ TypeScript (Node.js) SDK code generation failed because the installed Aspire CLI appears
to be incompatible with the configured Aspire SDK. Run 'aspire update' to align the CLI
and SDK and try again.
ℹ Run 'aspire update' to align the installed Aspire CLI with the configured SDK version,
then retry.
ℹ Run with '--debug' for full diagnostic details.
```
Tip
Prior to this improvement, a CLI/SDK version mismatch produced an empty `System.TypeLoadException` message followed by a 60-second timeout. If you saw that behavior, update the CLI with `aspire update`.
### Getting full diagnostic details
[Section titled “Getting full diagnostic details”](#getting-full-diagnostic-details)
Pass `--debug` to `aspire run` for detailed diagnostic output:
Aspire CLI
```bash
aspire run --debug
```
In addition to the standard failure message, the CLI prints a diagnostic block:
```text
🔬 Diagnostic details:
Exception: System.TypeLoadException
Type: Aspire.Hosting.SomeType
Runtime Aspire.Hosting: +
• Aspire.Hosting.CodeGeneration.TypeScript +
• Aspire.TypeSystem +
```
The same information is always written to the CLI log file at `~/.aspire/logs/cli_*.log`, even when `--debug` is not passed.
### Resolving the mismatch
[Section titled “Resolving the mismatch”](#resolving-the-mismatch)
Run `aspire update` to update the packages in `aspire.config.json` to match the installed CLI version, or reinstall the CLI to match the SDK version already in use:
Aspire CLI
```bash
aspire update
```
## See also
[Section titled “See also”](#see-also)
* [Build your first app](/get-started/first-app/?lang=typescript)
* [AppHost overview](/get-started/app-host/)
* [Multi-language architecture](/architecture/multi-language-architecture/)
* [aspire doctor command](/reference/cli/commands/aspire-doctor/)
* [aspire update command](/reference/cli/commands/aspire-update/)
* [aspire certs trust command](/reference/cli/commands/aspire-certs-trust/)
* [Certificate configuration](/app-host/certificate-configuration/)
# Test TUI and shell apps using WithTerminal
> Configure interactive resource terminals with WithTerminal, choose initial dimensions, and attach from the Aspire dashboard, CLI, or VS Code.
If you have a terminal user interface (TUI) application or a shell-based experience that you want to exercise while it runs under Aspire, add [`WithTerminal`](/reference/api/csharp/aspire.hosting/terminalresourcebuilderextensions/methods/#withterminal "Configures a resource to expose an interactive terminal session.")[`withTerminal`](/reference/api/typescript/aspire.hosting/withterminal/ "Adds an interactive terminal session to a resource using the default terminal options.") to the resource. Aspire then exposes an interactive terminal session that you can attach to from the [Aspire dashboard](/dashboard/overview/) or from the [`aspire terminal`](/reference/cli/commands/aspire-terminal/) CLI command.
For an introduction to the resource Terminal view, terminal dock, and detached windows, start with [Dashboard terminals](/dashboard/terminals/).
Aspire 13.6 preview
This page describes resource-terminal behavior and defaults for Aspire 13.6 preview. Use matching AppHost, dashboard, and CLI builds.
## Prerequisites
[Section titled “Prerequisites”](#prerequisites)
Start with a [TypeScript or C# AppHost](/get-started/app-host/) and an installed interactive program. Resource terminals support executable and project processes on Windows, macOS, and Linux, as well as container processes through the container runtime. Containers require a [supported container runtime](/get-started/prerequisites/).
The following example runs Python’s interactive REPL. Install Python and ensure `python3` is on the AppHost machine’s `PATH`; use `python` instead if that is your installation’s command, for example on Windows. It doesn’t require a Python source file.
## Add an interactive resource
[Section titled “Add an interactive resource”](#add-an-interactive-resource)
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
await builder
.addExecutable('repl', 'python3', '.', ['-i', '-q'])
.withTerminal();
await builder.build().run();
```
* C#
AppHost.cs
```csharp
#pragma warning disable ASPIRETERMINAL001
var builder = DistributedApplication.CreateBuilder(args);
builder.AddExecutable("repl", "python3", ".", "-i", "-q")
.WithTerminal();
builder.Build().Run();
```
Start the app with `aspire run`, select `repl` on the dashboard’s **Console logs** page, and switch to **Terminal** if needed. At the Python prompt, enter `print("Hello from Aspire")`. You can also run `aspire terminal attach repl` after [enabling the CLI commands](#work-with-terminals-from-the-cli).
Experimental
`WithTerminal` is an experimental API. Calling it in C# produces the `ASPIRETERMINAL001` diagnostic, which you must acknowledge for your AppHost to build. Suppress it inline with `#pragma warning disable ASPIRETERMINAL001` (as shown in the C# example above), or add `ASPIRETERMINAL001` to `` in your project file. The shape of the API and its options may change in a future release.
## When to use WithTerminal
[Section titled “When to use WithTerminal”](#when-to-use-withterminal)
Reach for `WithTerminal` when a resource is interactive rather than a plain background service:
* A **TUI application**—for example, an agent, a diagnostics console, or a curses-style tool—that draws a full-screen interface you want to see and drive.
* A **shell-based experience** where you want an interactive prompt inside a container or executable while it runs as part of your app model.
* An interactive development tool that you want to use without leaving the Aspire dashboard or CLI.
`WithTerminal` attaches to the resource’s configured process; it doesn’t open an additional shell inside an existing service. For that scenario, use an [AppHost-owned container exec terminal](/dashboard/ephemeral-terminals/).
### Explore a complete application
[Section titled “Explore a complete application”](#explore-a-complete-application)
The [TypeScript terminal basics sample](https://github.com/microsoft/aspire-samples/tree/main/samples/terminals/basics-typescript) and [C# terminal basics sample](https://github.com/microsoft/aspire-samples/tree/main/samples/terminals/basics-csharp) demonstrate this with a containerized REST client, an API, and Redis.
The [TypeScript AppHost](https://github.com/microsoft/aspire-samples/blob/main/samples/terminals/basics-typescript/apphost.mts) and [C# AppHost](https://github.com/microsoft/aspire-samples/blob/main/samples/terminals/basics-csharp/apphost.cs) build the client’s image with [`AddDockerfile`](/reference/api/csharp/aspire.hosting/containerresourcebuilderextensions/methods/#adddockerfile "Adds a Dockerfile to the application model that can be treated like a container resource.")[`addDockerfile`](/reference/api/typescript/aspire.hosting/adddockerfile/ "Adds a Dockerfile to the application model that can be treated like a container resource."), pass the API endpoint through `BASE_URL`, wait for the API, and enable the terminal with `WithTerminal()`. The client runs as the resource’s main process; there is no separate client session to create.
Follow either sample’s README to send a request from the resource terminal, change the stored data through Redis’s separate REPL command, and read it back through the API. This illustrates the distinction between an interactive resource and an [ephemeral terminal](/dashboard/ephemeral-terminals/) without requiring host installations of the client tools.
## The debugger is not attached automatically
[Section titled “The debugger is not attached automatically”](#the-debugger-is-not-attached-automatically)
When you apply `WithTerminal`, Aspire runs the resource as a plain process and **does not automatically attach the debugger**. If you need to debug the resource, attach the debugger manually to the running process from your IDE.
Note
The orchestrator (DCP) cannot currently run a process under the debugger and a pseudo-terminal (PTY) at the same time, so Aspire favors interactive terminal execution over automatic IDE execution.
## Attach from multiple places at once
[Section titled “Attach from multiple places at once”](#attach-from-multiple-places-at-once)
Terminal sessions support multiple simultaneous viewers. You can open **two browser tabs pointing at the same terminal**, or a browser tab and the CLI together. They share the same process and screen, not separate shells. Coordinate input when another person or automation is attached.
One peer holds the **primary** role and drives the terminal’s dimensions, while the others attach as secondary viewers. From the CLI you can attach without taking sizing control with `aspire terminal attach --viewer`, and take control later with the `Ctrl+B T` hotkey. A secondary viewer isn’t an exclusive-input or read-only boundary.
## Configure the terminal
[Section titled “Configure the terminal”](#configure-the-terminal)
To query a running database or cache, use the integration’s [`WithRepl()` command](/dashboard/ephemeral-terminals/#open-a-database-or-cache-repl) instead. It opens an additional client in the terminal dock without changing the server’s main process. `WithTerminal()` is for a resource whose main process needs an interactive terminal.
The terminal session is described by a set of options with sensible defaults:
| Option | Default | Description |
| ------------------ | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Columns` | `120` | The initial number of columns for the terminal grid. |
| `Rows` | `30` | The initial number of rows for the terminal grid. |
| `ShowTerminalHost` | `false` | Whether the hidden per-replica terminal host resources appear in the dashboard and CLI resource lists **and** export their diagnostic logs, metrics, and traces. Set to `true` to diagnose terminal-host startup or connectivity issues. |
Tip
`Columns` and `Rows` must each be `1` or greater. Configuring either with zero or a negative value in the `WithTerminal(...)` callback throws an `ArgumentOutOfRangeException` immediately instead of failing later during terminal-host startup.
Terminal host telemetry is opt-in
By default, the hidden terminal host helper resources don’t export logs, metrics, or traces, even when an OTLP endpoint is configured for the app—so they never show up in the dashboard’s telemetry resource list (including the **Traces** filter). Setting `ShowTerminalHost = true` both reveals the helper resources and turns on their diagnostic telemetry when an OTLP endpoint is available. This is independent of the parent resource’s own telemetry, which is never affected by `WithTerminal`.
Note
The resource being run is always the terminal program: for executables that’s the process itself, and for containers it’s the container’s own process. Configure the executable or container command to select the program; `TerminalOptions` doesn’t have a shell-selection option. Initial dimensions can later change when a viewer takes sizing control.
* TypeScript
TypeScript’s `withTerminal()` currently applies the default options. Configuring initial dimensions through a TypeScript options overload isn’t supported.
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
await builder
.addExecutable('repl', 'python3', '.', ['-i', '-q'])
.withTerminal();
await builder.build().run();
```
* C#
In C#, pass a callback to override any of these options:
AppHost.cs
```csharp
#pragma warning disable ASPIRETERMINAL001
var builder = DistributedApplication.CreateBuilder(args);
builder.AddExecutable("repl", "python3", ".", "-i", "-q")
.WithTerminal(options =>
{
options.Columns = 200;
options.Rows = 50;
});
builder.Build().Run();
```
## Terminals and replicas
[Section titled “Terminals and replicas”](#terminals-and-replicas)
Each replica of a resource gets its own independent terminal session. Aspire creates one terminal host per parent replica, so requesting three replicas yields three separate terminals. The order of [`WithReplicas`](/reference/api/csharp/aspire.hosting/projectresourcebuilderextensions/methods/#withreplicas "Configures how many replicas of the project should be created for the project.")[`withReplicas`](/reference/api/typescript/aspire.hosting/withreplicas/ "Configures how many replicas of the project should be created for the project.") and `WithTerminal` does not matter. For a project-based interactive app in `../Agent/Agent.csproj`:
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
await builder
.addProject('agent', '../Agent/Agent.csproj')
.withReplicas(3)
.withTerminal();
await builder.build().run();
```
* C#
Add a project reference to `Agent.csproj` in your AppHost before using its generated `Projects.Agent` type.
AppHost.cs
```csharp
#pragma warning disable ASPIRETERMINAL001
var builder = DistributedApplication.CreateBuilder(args);
builder.AddProject("agent")
.WithReplicas(3)
.WithTerminal();
builder.Build().Run();
```
When a resource has more than one replica, choose which one to attach to with `aspire terminal attach --replica ` (indices are 0-based), or pick interactively when prompted.
Note
Add a terminal to a resource only once. Calling `WithTerminal()` more than once on the same resource throws an exception.
## View terminals in the dashboard
[Section titled “View terminals in the dashboard”](#view-terminals-in-the-dashboard)
When a resource has `WithTerminal` applied, its **Console logs** page gains a live terminal session alongside the usual console log stream. Select a replica to interact with its independent session.

The page picks a default view based on the resource’s state at the moment you navigate to it:
* **Running** (the PTY is live) → the page defaults to the **Terminal** view, so the interactive session is the first thing you see.
* **Waiting**/**Starting** or already **Exited**/**Finished**/**FailedToStart** → the page defaults to the **Console logs** view, so hosting messages—such as “Waiting for resource X to become healthy…” or a startup failure—and post-exit output remain visible immediately.
Select **Settings** in the page toolbar and choose **Terminal** or **Console logs** to switch views manually at any time. Both views stay live while you’re on the page: switching between them never tears down the terminal session or loses console log scrollback, and a later state transition (for example, `Waiting` → `Running`) doesn’t auto-switch the view once you’ve navigated to the page. Selecting a different resource re-evaluates the default for that resource.
Note
The view you pick only affects what’s currently displayed—it doesn’t change what’s captured. Console logs keep streaming, and the terminal session keeps running, regardless of which view is active.
For detached windows, font and sizing controls, keyboard focus navigation, and lifecycle differences, see [Dashboard terminals](/dashboard/terminals/).
Container startup output
The container runtime can start a container before attaching its terminal. One-time startup output and terminal capability queries can be missed. For tools that depend on those queries, start an interactive shell as the container process and launch the tool after attaching.
Firefox renderer
Desktop and Android Firefox always render dashboard terminals using WebGL2, even when WebGPU is available, to avoid a [known Firefox WebGPU performance issue](https://bugzilla.mozilla.org/show_bug.cgi?id=1870699). This applies to every terminal mount—including automatic retries and explicit reconnects—and requires no configuration. Firefox on iOS (`FxiOS/`) uses WebKit rather than Gecko, so it isn’t affected and keeps automatic renderer selection like other browsers.
Because a terminal-backed executable bridges its standard output and error through the pseudo-terminal (PTY) instead of separate log streams, the **Console logs** view and [`aspire logs`](/reference/cli/commands/aspire-logs/) only show that resource’s startup and system logs. For example the view shows lifecycle and DCP diagnostics but not the interleaved `stdout` and `stderr` text you see in the **Terminal** view. Use the **Terminal** view or `aspire terminal attach ` to see the process’s own output.
### Open a terminal in a separate window
[Section titled “Open a terminal in a separate window”](#open-a-terminal-in-a-separate-window)
Use **Open in new window** to keep a terminal visible while you inspect other dashboard pages. AppHost-owned terminal dock tabs can also open in a separate window, as shown below. Their detached view contains the terminal and its controls rather than the dashboard navigation.

Resource terminals and AppHost-owned dock terminals have different ownership and lifetimes. Closing a viewer doesn’t replace disposing an AppHost-owned terminal. For more about docked terminals, see [Explore the Aspire dashboard](/dashboard/explore/#interactive-terminals).
## Access terminals from VS Code
[Section titled “Access terminals from VS Code”](#access-terminals-from-vs-code)
In the [Aspire VS Code extension](/get-started/aspire-vscode-extension/), any resource configured with `WithTerminal()` gains an **Open terminal** entry in its right-click context menu. Selecting **Open terminal** runs `aspire terminal attach ` and opens the session as an editor-style terminal tab directly in VS Code—no separate shell window needed.
For multi-replica resources, the extension automatically passes `--replica ` so the correct instance is attached. The `--apphost` flag is included when the current AppHost connection information is available, so the command connects without requiring you to specify the AppHost separately.
Note
The **Open terminal** context-menu item is only visible for resources that have `terminal.enabled` set, which is the case for any resource where `WithTerminal()` was called in the AppHost. Resources without `WithTerminal()` do not show this menu entry.
## Work with terminals from the CLI
[Section titled “Work with terminals from the CLI”](#work-with-terminals-from-the-cli)
The `aspire terminal` command group lets you list and attach to terminal sessions from your shell without any configuration step:
* [`aspire terminal ps`](/reference/cli/commands/aspire-terminal-ps/) lists every terminal-enabled resource in the running AppHost, with grid size, attached-peer count, and per-replica health.
* [`aspire terminal attach`](/reference/cli/commands/aspire-terminal-attach/) attaches your local terminal to a resource’s interactive PTY session.
* [`aspire terminal tape play`](/reference/cli/commands/aspire-terminal-tape-play/) drives an existing resource terminal with scripted input and output waits. See [terminal automation](/dashboard/terminal-tape-playback/) for complete examples and tape file syntax.
## See also
[Section titled “See also”](#see-also)
* [aspire terminal command](/reference/cli/commands/aspire-terminal/)
* [Executable resources](/app-host/executable-resources/)
* [Dashboard terminal experiences](/dashboard/terminals/)
* [Create AppHost-owned terminals](/dashboard/ephemeral-terminals/)
# Add Dockerfiles to your app model
> Add Dockerfiles to your Aspire app model with WithDockerfile — build local container images, layer arguments, and pin context paths for project and executable resources.
With Aspire it’s possible to specify a *Dockerfile* to build when the [AppHost](/get-started/app-host/) is started using either the [`AddDockerfile`](/reference/api/csharp/aspire.hosting/containerresourcebuilderextensions/methods/#adddockerfile "Adds a Dockerfile to the application model that can be treated like a container resource.")[`addDockerfile`](/reference/api/typescript/aspire.hosting/adddockerfile/ "Adds a Dockerfile to the application model that can be treated like a container resource.") or [`WithDockerfile`](/reference/api/csharp/aspire.hosting/containerresourcebuilderextensions/methods/#withdockerfile "Causes Aspire to build the specified container image from a Dockerfile.")[`withDockerfile`](/reference/api/typescript/aspire.hosting/withdockerfile/ "Causes Aspire to build the specified container image from a Dockerfile.") extension methods.
These two methods serve different purposes:
* **`AddDockerfile`**: Creates a new container resource from an existing Dockerfile. Use this when you want to add a custom containerized service to your app model.
* **`WithDockerfile`**: Customizes an existing container resource (like a database or cache) to use a different Dockerfile. Use this when you want to modify the default container image for an Aspire component.
Both methods expect an existing Dockerfile in the specified context path—neither method creates a Dockerfile for you. To generate a Dockerfile from AppHost code instead, use the [Dockerfile builder APIs](#generate-a-dockerfile-programmatically) or the [Dockerfile factory APIs](#generate-a-dockerfile-with-a-factory-function).
## When to use AddDockerfile vs WithDockerfile
[Section titled “When to use AddDockerfile vs WithDockerfile”](#when-to-use-adddockerfile-vs-withdockerfile)
Choose the appropriate method based on your scenario:
**Use `AddDockerfile` when:**
* You want to add a custom containerized service to your app model.
* You have an existing Dockerfile for a custom application or service.
* You need to create a new container resource that isn’t provided by Aspire components.
**Use `WithDockerfile` when:**
* You want to customize an existing Aspire component (like PostgreSQL, Redis, etc.).
* You need to replace the default container image with a custom one.
* You want to maintain the strongly typed resource builder and its extension methods.
* You have specific requirements that the default container image doesn’t meet.
## Add a Dockerfile to the app model
[Section titled “Add a Dockerfile to the app model”](#add-a-dockerfile-to-the-app-model)
In the following example the `AddDockerfile` extension method is used to specify a container by referencing the context path for the container build.
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const container = await builder.addDockerfile(
"mycontainer", "relative/context/path");
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
var container = builder.AddDockerfile(
"mycontainer", "relative/context/path");
```
Unless the context path argument is a rooted path the context path is interpreted as being relative to the AppHost project directory.
By default the name of the *Dockerfile* which is used is `Dockerfile` and is expected to be within the context path directory. It’s possible to explicitly specify the *Dockerfile* name either as an absolute path or a relative path to the context path.
This is useful if you wish to modify the specific *Dockerfile* being used when running locally or when the AppHost is deploying.
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const container = (await builder.executionContext.isRunMode())
? await builder.addDockerfile(
"mycontainer", "relative/context/path", "Dockerfile.debug")
: await builder.addDockerfile(
"mycontainer", "relative/context/path", "Dockerfile.release");
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
var container = builder.ExecutionContext.IsRunMode
? builder.AddDockerfile(
"mycontainer", "relative/context/path", "Dockerfile.debug")
: builder.AddDockerfile(
"mycontainer", "relative/context/path", "Dockerfile.release");
```
## Customize existing container resources
[Section titled “Customize existing container resources”](#customize-existing-container-resources)
When using `AddDockerfile` the return value is an `IResourceBuilder`. Aspire includes many custom resource types that are derived from `ContainerResource`.
Using the `WithDockerfile` extension method it’s possible to take an existing Aspire component (like PostgreSQL, Redis, or SQL Server) and replace its default container image with a custom one built from your own Dockerfile. This allows you to continue using the strongly typed resource types and their specific extension methods while customizing the underlying container.
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const pgsql = await builder.addPostgres("pgsql");
// This replaces the default PostgreSQL container image with a custom one
// built from your Dockerfile, while keeping PostgreSQL-specific functionality.
await pgsql.withDockerfile("path/to/context");
await pgsql.withPgAdmin(); // Still works because it's still a PostgreSQL resource.
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
// This replaces the default PostgreSQL container image with a custom one
// built from your Dockerfile, while keeping PostgreSQL-specific functionality
var pgsql = builder.AddPostgres("pgsql")
.WithDockerfile("path/to/context")
.WithPgAdmin(); // Still works because it's still a PostgreSQL resource
```
## Generate a Dockerfile programmatically
[Section titled “Generate a Dockerfile programmatically”](#generate-a-dockerfile-programmatically)
Use [`AddDockerfileBuilder`](/reference/api/csharp/aspire.hosting/containerresourcebuilderextensions/methods/#adddockerfilebuilder "Adds a Dockerfile to the application model that can be treated like a container resource, with the Dockerfile generated programmatically using the...")[`addDockerfileBuilder`](/reference/api/typescript/aspire.hosting/adddockerfilebuilder/ "Adds a container resource built from a programmatically generated Dockerfile") or [`WithDockerfileBuilder`](/reference/api/csharp/aspire.hosting/containerresourcebuilderextensions/methods/#withdockerfilebuilder "Builds the specified container image from a Dockerfile generated by a callback using the Aspire.Hosting.ApplicationModel.Docker.DockerfileBuilder API.")[`withDockerfileBuilder`](/reference/api/typescript/aspire.hosting/withdockerfilebuilder/ "Configures the resource to use a programmatically generated Dockerfile") when you need Aspire to generate a Dockerfile from AppHost code. These APIs are useful when the Dockerfile depends on AppHost configuration, when you want to compose Dockerfile fragments, or when you want to keep multi-stage image build logic near the resource definition.
Caution
The Dockerfile builder APIs are experimental and may change in future releases. In C# AppHosts, suppress diagnostic [`ASPIREDOCKERFILEBUILDER001`](/diagnostics/aspiredockerfilebuilder001/) when you choose to use them.
`AddDockerfileBuilder` creates a new container resource and configures the generated Dockerfile in one step:
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
import type { DockerfileBuilderCallbackContext } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const configureDockerfile = async (context: DockerfileBuilderCallbackContext) => {
const dockerfile = await context.builder();
await dockerfile
.from("node:22-alpine", { stageName: "build" })
.workDir("/app")
.copy("package*.json", "./")
.run("npm ci")
.copy(".", ".")
.run("npm run build");
await dockerfile
.from("nginx:alpine", { stageName: "runtime" })
.copyFrom("build", "/app/dist", "/usr/share/nginx/html")
.expose(80);
};
await builder.addDockerfileBuilder("frontend", "../frontend", configureDockerfile, {
stage: "runtime",
});
await builder.build().run();
```
* C#
AppHost.cs
```csharp
using Aspire.Hosting.ApplicationModel.Docker;
var builder = DistributedApplication.CreateBuilder(args);
#pragma warning disable ASPIREDOCKERFILEBUILDER001
builder.AddDockerfileBuilder("frontend", "../frontend", context =>
{
var build = context.Builder.From("node:22-alpine", "build");
build.WorkDir("/app")
.Copy("package*.json", "./")
.Run("npm ci")
.Copy(".", ".")
.Run("npm run build");
var runtime = context.Builder.From("nginx:alpine", "runtime");
runtime.CopyFrom("build", "/app/dist", "/usr/share/nginx/html")
.Expose(80);
return Task.CompletedTask;
}, stage: "runtime");
#pragma warning restore ASPIREDOCKERFILEBUILDER001
builder.Build().Run();
```
`WithDockerfileBuilder` applies a generated Dockerfile to an existing container resource. The image name provided when the resource is created is replaced by the generated Dockerfile build during publish:
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
import type { DockerfileBuilderCallbackContext } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const configureDockerfile = async (context: DockerfileBuilderCallbackContext) => {
const dockerfile = await context.builder();
await dockerfile
.from("nginx:alpine", { stageName: "runtime" })
.copy(".", "/usr/share/nginx/html")
.expose(80);
};
await builder
.addContainer("frontend", "nginx:alpine")
.withDockerfileBuilder("../frontend", configureDockerfile, {
stage: "runtime",
});
await builder.build().run();
```
* C#
AppHost.cs
```csharp
using Aspire.Hosting.ApplicationModel.Docker;
var builder = DistributedApplication.CreateBuilder(args);
#pragma warning disable ASPIREDOCKERFILEBUILDER001
builder.AddContainer("frontend", "nginx:alpine")
.WithDockerfileBuilder("../frontend", context =>
{
var stage = context.Builder.From("nginx:alpine", "runtime");
stage.Copy(".", "/usr/share/nginx/html")
.Expose(80);
return Task.CompletedTask;
}, stage: "runtime");
#pragma warning restore ASPIREDOCKERFILEBUILDER001
builder.Build().Run();
```
## Generate a Dockerfile with a factory function
[Section titled “Generate a Dockerfile with a factory function”](#generate-a-dockerfile-with-a-factory-function)
Use [`AddDockerfileFactory`](/reference/api/csharp/aspire.hosting/containerresourcebuilderextensions/methods/#adddockerfilefactory "Adds a Dockerfile to the application model that can be treated like a container resource, with the Dockerfile content generated by a synchronous factory...")[`addDockerfileFactory`](/reference/api/typescript/aspire.hosting/adddockerfilefactory/ "Adds a Dockerfile to the application model that can be treated like a container resource, with the Dockerfile content generated by an asynchronous factory...") or [`WithDockerfileFactory`](/reference/api/csharp/aspire.hosting/containerresourcebuilderextensions/methods/#withdockerfilefactory "Builds the specified container image from a Dockerfile generated by a synchronous factory function.")[`withDockerfileFactory`](/reference/api/typescript/aspire.hosting/withdockerfilefactory/ "Builds the specified container image from a Dockerfile generated by an asynchronous factory function.") when you need to generate a Dockerfile as a string from AppHost code. Unlike the [Dockerfile builder APIs](#generate-a-dockerfile-programmatically) that use a fluent API to compose Dockerfile instructions, the factory APIs let you return Dockerfile content directly as a string — useful when you already have string-based Dockerfile generation logic or want to construct content conditionally.
The factory callback receives a `DockerfileFactoryContext` parameter that provides access to the resource and DI services when needed.
`AddDockerfileFactory` creates a new container resource and configures the generated Dockerfile in one step:
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const container = await builder.addDockerfileFactory("myapp", "../myapp", async () => `
FROM node:22-alpine
WORKDIR /app
COPY . .
RUN npm ci
EXPOSE 3000
CMD ["node", "server.js"]
`);
await builder.build().run();
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
var container = builder.AddDockerfileFactory("myapp", "../myapp", async context =>
{
// Return Dockerfile content as a string.
return """
FROM node:22-alpine
WORKDIR /app
COPY . .
RUN npm ci
EXPOSE 3000
CMD ["node", "server.js"]
""";
});
builder.Build().Run();
```
`WithDockerfileFactory` applies a factory-generated Dockerfile to an existing container resource:
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
await builder.addContainer("myapp", { image: "placeholder", tag: "latest" })
.withDockerfileFactory("../myapp", async () => `
FROM nginx:alpine
COPY dist/ /usr/share/nginx/html
EXPOSE 80
`);
await builder.build().run();
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
builder.AddContainer("myapp", "placeholder")
.WithDockerfileFactory("../myapp", async context =>
{
return """
FROM nginx:alpine
COPY dist/ /usr/share/nginx/html
EXPOSE 80
""";
});
builder.Build().Run();
```
## Pass build arguments
[Section titled “Pass build arguments”](#pass-build-arguments)
The [`WithBuildArg`](/reference/api/csharp/aspire.hosting/containerresourcebuilderextensions/methods/#withbuildarg "Adds a build argument when the container is build from a Dockerfile.")[`withBuildArg`](/reference/api/typescript/aspire.hosting/withbuildarg/ "Adds a build argument when the container is built from a Dockerfile.") method can be used to pass arguments into the container image build.
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const container = await builder.addDockerfile("mygoapp", "relative/context/path");
await container.withBuildArg("GO_VERSION", "1.22");
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
var container = builder.AddDockerfile("mygoapp", "relative/context/path")
.WithBuildArg("GO_VERSION", "1.22");
```
The value parameter on the `WithBuildArg` method can be a literal value (`boolean`, `string`, `int`) or it can be a resource builder for a [parameter resource](/fundamentals/external-parameters/). The following code replaces the `GO_VERSION` with a parameter value that can be specified at deployment time.
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const goVersion = await builder.addParameter("goversion");
const container = await builder.addDockerfile("mygoapp", "relative/context/path");
await container.withBuildArg("GO_VERSION", goVersion);
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
var goVersion = builder.AddParameter("goversion");
var container = builder.AddDockerfile("mygoapp", "relative/context/path")
.WithBuildArg("GO_VERSION", goVersion);
```
Build arguments correspond to the [`ARG` command](https://docs.docker.com/build/guide/build-args/) in *Dockerfiles*. Expanding the preceding example, this is a multi-stage *Dockerfile* which specifies specific container image version to use as a parameter.
Dockerfile
```dockerfile
# Stage 1: Build the Go program
ARG GO_VERSION=1.22
FROM golang:${GO_VERSION} AS builder
WORKDIR /build
COPY . .
RUN go build mygoapp.go
# Stage 2: Run the Go program
FROM mcr.microsoft.com/cbl-mariner/base/core:2.0
WORKDIR /app
COPY --from=builder /build/mygoapp .
CMD ["./mygoapp"]
```
Note
Instead of hardcoding values into the container image, it’s recommended to use environment variables for values that frequently change. This avoids the need to rebuild the container image whenever a change is required.
## Pass build secrets
[Section titled “Pass build secrets”](#pass-build-secrets)
In addition to build arguments it’s possible to specify build secrets using [`WithBuildSecret`](/reference/api/csharp/aspire.hosting/containerresourcebuilderextensions/methods/#withbuildsecret "Adds a secret build argument when the container is built from a Dockerfile.")[`withBuildSecret`](/reference/api/typescript/aspire.hosting/withbuildsecret/ "Adds a secret build argument when the container is built from a Dockerfile.") which are made selectively available to individual commands in the *Dockerfile* using the `--mount=type=secret` syntax on `RUN` commands.
* TypeScript
apphost.mts
```typescript
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const accessToken = await builder.addParameter("accesstoken", { secret: true });
const container = await builder.addDockerfile("myapp", "relative/context/path");
await container.withBuildSecret("ACCESS_TOKEN", accessToken);
```
* C#
AppHost.cs
```csharp
var builder = DistributedApplication.CreateBuilder(args);
var accessToken = builder.AddParameter("accesstoken", secret: true);
var container = builder.AddDockerfile("myapp", "relative/context/path")
.WithBuildSecret("ACCESS_TOKEN", accessToken);
```
For example, consider the `RUN` command in a *Dockerfile* which exposes the specified secret to the specific command:
Dockerfile
```dockerfile
# The helloworld command can read the secret from /run/secrets/ACCESS_TOKEN
RUN --mount=type=secret,id=ACCESS_TOKEN helloworld
```
Caution
Caution should be exercised when passing secrets in build environments. This is often done when using a token to retrieve dependencies from private repositories or feeds before a build. It is important to ensure that the injected secrets are not copied into the final or intermediate images.
# Multi-language architecture
> Understand how Aspire uses a guest/host architecture, ATS, SDK generation, and local token-based auth to support TypeScript AppHosts.
Aspire supports writing AppHosts in multiple languages. While the orchestration engine is built on .NET, a guest/host architecture allows AppHosts written in TypeScript today, and additional languages in the future, to access Aspire integrations, service discovery, and the dashboard.
## Why a shared backend
[Section titled “Why a shared backend”](#why-a-shared-backend)
Aspire’s hosting integrations and deployment publishers are written in .NET and have already accumulated the runtime behavior needed for local orchestration, diagnostics, and publishing. Rewriting those integrations for every guest language would duplicate a large amount of behavior and significantly increase maintenance cost.
Instead, guest languages only declare resources such as [`AddRedis`](/reference/api/csharp/aspire.hosting.redis/redisbuilderextensions/methods/#addredis "Adds a Redis container to the application model.")[`addRedis`](/reference/api/typescript/aspire.hosting.redis/addredis/ "Adds a Redis container to the application model."), [`AddPostgres`](/reference/api/csharp/aspire.hosting.postgresql/postgresbuilderextensions/methods/#addpostgres "Adds a PostgreSQL resource to the application model. A container is used for local development.")[`addPostgres`](/reference/api/typescript/aspire.hosting.postgresql/addpostgres/ "Adds a PostgreSQL resource to the application model. A container is used for local development."), and [`WithReference`](/reference/api/csharp/aspire.hosting/resourcebuilderextensions/methods/#withreference "Injects a connection string as an environment variable from the source resource into the destination resource, using the source resource's name as the...")[`withReference`](/reference/api/typescript/aspire.hosting/withreference/ "Adds a reference to another resource"), while the .NET host handles orchestration concerns such as starting containers, wiring service discovery, running health checks, and generating deployment artifacts. The trade-off is a local IPC hop, which is much cheaper than maintaining the same integration surface independently in multiple languages.
## Guest/host model
[Section titled “Guest/host model”](#guesthost-model)
When you run a TypeScript AppHost, the Aspire CLI orchestrates two processes:
* **Guest**: your `apphost.mts` process, running in Node.js
* **Host**: the Aspire orchestration server, running on .NET
The guest communicates with the host via JSON-RPC over a local transport: Unix sockets on macOS and Linux, and named pipes on Windows. Your code calls methods such as `addRedis()` or `withReference()`, and the generated SDK translates those calls into RPC requests.
```
flowchart TB
subgraph Guest["Guest Process (Node.js)"]
TS["apphost.mts"]
SDK["Generated SDK (.aspire/modules/)"]
Client["JSON-RPC Client"]
TS --> SDK --> Client
end
subgraph Host["Host Process (Aspire Server)"]
RPC["JSON-RPC Server"]
Dispatcher["Capability Dispatcher"]
Integrations["Hosting Integrations"]
RPC --> Dispatcher --> Integrations
end
subgraph Managed["Managed Resources"]
Dashboard["Dashboard"]
Containers["Containers"]
Discovery["Service Discovery"]
end
Client <-->|"Local transport"| RPC
Integrations --> Dashboard & Containers & Discovery
```
## Startup sequence
[Section titled “Startup sequence”](#startup-sequence)
1. The CLI prepares the host process with the required hosting packages.
2. The ATS scanner inspects assemblies for exports and generates the TypeScript SDK into `.aspire/modules/`.
3. The CLI starts the host process and creates the local socket or pipe endpoint.
4. The CLI starts the guest process and passes connection details through environment variables.
5. The guest connects and invokes capabilities such as [`CreateBuilder`](/reference/api/csharp/aspire.hosting/distributedapplication/methods/#createbuilder "Creates a new instance of the Aspire.Hosting.IDistributedApplicationBuilder interface.")[`createBuilder`](/reference/api/typescript/aspire.hosting/createbuilder/ "Creates a new distributed application builder"), `addRedis`, `build`, and [`RunAsync`](/reference/api/csharp/aspire.hosting/distributedapplication/methods/#runasync "Runs an application and returns a Task that only completes when the token is triggered or shutdown is triggered and all...")[`run`](/reference/api/typescript/aspire.hosting/distributedapplication/run/ "Runs the distributed application").
6. The host orchestrates resources, starts the dashboard, and manages the application lifecycle.
## Token-based authentication
[Section titled “Token-based authentication”](#token-based-authentication)
The guest process authenticates to the host with a one-time token generated for that session and passed through environment variables at startup. The local transport is also protected by operating system file permissions, so only processes running as the same user can connect. There are no public network ports involved in guest-to-host communication.
## Aspire Type System
[Section titled “Aspire Type System”](#aspire-type-system)
The Aspire Type System, or ATS, is the contract that bridges .NET and guest languages. Every exported type that crosses the boundary gets a portable type identity derived from its assembly and type name.
### Type categories
[Section titled “Type categories”](#type-categories)
| Category | Description | Serialization |
| --------------------- | ----------------------------------------------------------- | ------------------------------------------- |
| **Primitive** | `string`, `int`, `bool`, `double`, and similar scalar types | JSON native values |
| **Enum** | .NET enum types | String member names |
| **Handle** | Opaque references to host-side objects | JSON handle envelopes |
| **DTO** | Data transfer objects marked for export | JSON objects |
| **Callback** | Guest-provided delegate functions | Callback identifiers |
| **Array** | Immutable collections | JSON arrays |
| **List / dictionary** | Mutable collections | Handles for properties, JSON for parameters |
### How ATS maps to TypeScript
[Section titled “How ATS maps to TypeScript”](#how-ats-maps-to-typescript)
| .NET type | TypeScript representation |
| -------------- | ---------------------------------------- |
| Primitives | Native TypeScript primitives |
| Enums | String literal unions |
| Resource types | Typed handle objects with fluent methods |
| DTOs | Interfaces serialized as JSON |
| Collections | Arrays and `Record` |
| Delegates | Async callback functions |
Resource types are passed by handle. The actual instance remains in the host process, while the TypeScript SDK keeps a reference and dispatches method calls as JSON-RPC requests.
## Polymorphism flattening
[Section titled “Polymorphism flattening”](#polymorphism-flattening)
.NET APIs rely on inheritance, interfaces, and generics. Guest SDKs do not need to expose that full shape directly. During scanning, ATS flattens the exported type system so the generated guest API is easier to consume:
* Concrete types receive the full set of applicable capabilities.
* Interface relationships are expanded into directly callable members.
* Generic constraints are resolved into exportable concrete surfaces.
That flattening means a resource such as `RedisResource` can expose fluent methods from shared interfaces alongside Redis-specific APIs without requiring guest languages to model the original inheritance tree.
## SDK generation
[Section titled “SDK generation”](#sdk-generation)
The TypeScript SDK is generated from hosting integration assemblies. When you add an integration with `aspire add`, the CLI:
1. Loads the integration assembly.
2. Scans exported methods and types.
3. Applies ATS rules, including polymorphism flattening.
4. Emits typed TypeScript wrappers into `.aspire/modules/`.
This keeps the SDK in sync with the .NET implementation. Integration authors do not hand-write TypeScript bindings; they export their .NET APIs and the CLI generates the guest surface automatically.
If you’re building a hosting integration and want it to work with TypeScript AppHosts, see [Multi-language integrations](/extensibility/multi-language-integration-authoring/).
## Same model, different syntax
[Section titled “Same model, different syntax”](#same-model-different-syntax)
The AppHost model is the same regardless of language. A TypeScript AppHost defines the same resources, references, and dependency graph as a C# AppHost. The difference is the authoring syntax.
| Concept | C# | TypeScript |
| -------------- | -------------------------------------------- | --------------------------------- |
| Create builder | `DistributedApplication.CreateBuilder(args)` | `await createBuilder()` |
| Add resource | `builder.AddRedis("cache")` | `await builder.addRedis("cache")` |
| Reference | `.WithReference(db)` | `.withReference(db)` |
| Wait for | `.WaitFor(api)` | `.waitFor(api)` |
| Build and run | `builder.Build().Run()` | `await builder.build().run()` |
The resulting dashboard, service discovery behavior, health checks, and deployment artifacts come from the same host-side orchestration engine.
## See also
[Section titled “See also”](#see-also)
* [Build your first app](/get-started/first-app/?lang=typescript) — get started with a TypeScript AppHost
* [Resource model](/architecture/resource-model/) — understand how Aspire models resources and relationships
* [Multi-language integrations](/extensibility/multi-language-integration-authoring/) — make your integration work with TypeScript AppHosts
# Aspire architecture overview
> Learn the overall architecture of Aspire — the AppHost, resource model, orchestration, networking, dashboard, and how multi-language integrations fit together.
Aspire brings together a powerful suite of tools and libraries, designed to deliver a seamless and intuitive experience for developers. Its modular and extensible architecture empowers you to define your application model with precision, orchestrating intricate systems composed of services, containers, and executables. Whether your components span different programming languages, platforms, stacks, or operating systems, Aspire ensures they work harmoniously, simplifying the complexity of modern cloud-native app development.
## App model architecture
[Section titled “App model architecture”](#app-model-architecture)
Resources are the building blocks of your app model. They’re used to represent abstract concepts like services, containers, executables, and external integrations. Specific resources enable developers to define dependencies on concrete implementations of these concepts. For example, a `Redis` resource can be used to represent a Redis cache, while a `Postgres` resource can represent a PostgreSQL database.
While the app model is often synonymous with a collection of resources, it’s also a high level representation of your entire application topology. This is important, as it’s architected for lowering. In this way, Aspire can be thought of as a compiler for application topology.
### Lowering the model
[Section titled “Lowering the model”](#lowering-the-model)
In a traditional compiler, the process of “lowering” involves translating a high-level programming language into progressively simpler representations:
* **Intermediate Representation (IR):** The first step abstracts away language-specific features, creating a platform-neutral representation.
* **Machine Code:** The IR is then transformed into machine-specific instructions tailored to a specific CPU architecture.
Similarly, Aspire applies this concept to applications, treating the app model as the high-level language:
* **Intermediate constructs:** The app model is first lowered into intermediate constructs, such as cloud development kit (CDK)-style object graphs. These constructs might be platform-agnostic or partially tailored to specific targets.
* **Target runtime representation:** Finally, a publisher generates the deployment-ready artifacts—YAML, HCL, JSON, or other formats—required by the target platform.
This layered approach unlocks several key benefits:
* **Validation and enrichment:** Models can be validated and enriched during the transformation process, ensuring correctness and completeness.
* **Multi-target support:** Aspire supports multiple deployment targets, enabling flexibility across diverse environments.
* **Customizable workflow:** Developers can hook into each phase of the process to customize behavior, tailoring the output to specific needs.
* **Clean and portable models:** The high-level app model remains expressive, portable, and free from platform-specific concerns.
Most importantly, the translation process itself is highly extensible. You can define custom transformations, enrichments, and output formats, allowing Aspire to seamlessly adapt to your unique infrastructure and deployment requirements. This extensibility ensures that Aspire remains a powerful and versatile tool, capable of evolving alongside your application’s needs.
### Modality and extensibility
[Section titled “Modality and extensibility”](#modality-and-extensibility)
Aspire operates in two primary modes, each tailored to streamline your specific needs—detailed in the following section. Both modes use a robust set of familiar APIs and a rich ecosystem of [integrations](/integrations/gallery/). Each integration simplifies working with a common service, framework, or platform, such as Redis, PostgreSQL, Azure services, or Orleans, for example. These integrations work together like puzzle pieces, enabling you to define resources, express dependencies, and configure behavior effortlessly—whether you’re running locally or deploying to production.
Why is modality important when it comes to the AppHost’s execution context? This is because it allows you to define your app model once and with the appropriate APIs, specify how resources operate in each mode. Consider the following collection of resources:
* Database: PostgreSQL
* Cache: Redis
* AI service: Ollama or OpenAI
* Backend: ASP.NET Core minimal API
* Frontend: React app
Depending on the mode, the AppHost might treat these resources differently. For example, in run mode, the AppHost might use a local PostgreSQL database and Redis cache—using containers, while in publish mode, it might generate deployment artifacts for Azure PostgreSQL and Redis Cache.
#### Run mode
[Section titled “Run mode”](#run-mode)
The default mode is run mode, which is ideal for local development and testing. In this mode, the Aspire AppHost orchestrates your application model, including processes, containers, and cloud emulators, to facilitate fast and iterative development. Resources behave like real runtime entities with lifecycles that mirror production. With a simple F5`F5`F5`F5`F5`F5`, the AppHost launches everything in your app model—storage, databases, caches, messaging, jobs, APIs, frontends—all fully configured and ready to debug locally. Let’s consider the app model from the previous section—where AppHost would orchestrate the following resources locally:

For more information on how run mode works, see [Dev-time orchestration](#dev-time-orchestration).
#### Publish mode
[Section titled “Publish mode”](#publish-mode)
The publish mode generates deployment-ready artifacts tailored to your target environment. The Aspire AppHost compiles your app model into outputs like Kubernetes manifests, Terraform configs, Bicep/ARM templates, Docker Compose files, or CDK constructs—ready for integration into any deployment pipeline. The output format depends on the chosen publisher, giving you flexibility across deployment scenarios. When you consider the app model from the previous section, the AppHost doesn’t orchestrate anything—instead, it emits publish artifacts that can be used to deploy your application to a cloud provider. For example, let’s assume you want to deploy to Azure—the AppHost would emit Bicep templates that define the following resources:

## Dev-time orchestration
[Section titled “Dev-time orchestration”](#dev-time-orchestration)
In run mode, [the AppHost orchestrates](/get-started/app-host/) all resources defined in your app model. But how does it achieve this?
Caution
The AppHost isn’t a production runtime. It’s a development-time orchestration tool that simplifies the process of running and debugging your application locally.
In this section, several key questions are answered to help you understand how the AppHost orchestrates your app model:
* **What powers the orchestration?**
Orchestration is delegated to the [Microsoft Developer Control Plane](#developer-control-plane) (DCP), which manages resource lifecycles, startup order, dependencies, and network configurations across your app topology.
* **How is the app model used?**
The app model defines all resources via implementations of `IResource`, including containers, processes, databases, and external services—forming the blueprint for orchestration.
* **What role does the AppHost play?**
The AppHost provides a high-level declaration of the desired application state. It delegates execution to DCP, which interprets the app model and performs orchestration accordingly.
* **What resources are monitored?**
All declared resources—including containers, executables, and integrations—are monitored to ensure correct behavior and to support a fast and reliable development workflow.
* **How are containers and executables managed?**
Containers and processes are initialized with their configurations and launched concurrently, respecting the dependency graph defined in the app model. DCP ensures their readiness and connectivity during orchestration, starting resources as quickly as possible while maintaining the correct order dictated by their dependencies.
* **How are resource dependencies handled?**
Dependencies are defined in the app model and evaluated by DCP to determine correct startup sequencing, ensuring resources are available before dependents start.
* **How is networking configured?**
Networking—such as port bindings—is autoconfigured unless explicitly defined. DCP resolves conflicts and ensures availability, enabling seamless communication between services.
The orchestration process follows a layered architecture. At its core, the AppHost represents the developer’s desired view of the distributed application’s resources. DCP ensures that this desired state is realized by orchestrating the resources and maintaining consistency.
The [app model](/get-started/app-host/) serves as a blueprint for DCP to orchestrate your application. Under the hood, the AppHost is a .NET console application powered by the [`📦 Aspire.Hosting.AppHost`](https://www.nuget.org/packages/Aspire.Hosting.AppHost) NuGet package. This package includes build targets that register orchestration dependencies, enabling seamless dev-time orchestration.
DCP is a Kubernetes-compatible API server, meaning it uses the same network protocols and conventions as Kubernetes. This compatibility allows the Aspire AppHost to leverage existing Kubernetes libraries for communication. Specifically, the AppHost contains an implementation of the `k8s.KubernetesClient` (from the [📦 KubernetesClient](https://www.nuget.org/packages/KubernetesClient) NuGet package), which is a .NET client for Kubernetes. This client is used to communicate with the DCP API server, enabling the AppHost to delegate orchestration tasks to DCP.
When you run the AppHost, it performs the first step of “lowering” by translating the general-purpose Aspire app model into a DCP-specific model tailored for local execution in run mode. This DCP model is then handed off to DCP, which evaluates it and orchestrates the resources accordingly. This separation ensures that the AppHost focuses on adapting the Aspire app model for local execution, while DCP specializes in executing the tailored model. The following diagram helps to visualize this orchestration process:

### Developer control plane
[Section titled “Developer control plane”](#developer-control-plane)
DCP is at the core of the Aspire AppHost orchestration functionality. It’s responsible for orchestrating all resources defined in your app model, starting the developer dashboard, ensuring that everything is set up correctly for local development and testing. DCP manages the lifecycle of resources, applies network configurations, and resolves dependencies.
DCP is written in Go, aligning with Kubernetes and its ecosystem, which are also Go-based. This choice enables deep, native integration with Kubernetes APIs, efficient concurrency, and access to mature tooling like Kubebuilder. DCP is delivered as two executables:
* `dcp.exe`: API server that exposes a Kubernetes-like API endpoint for the AppHost to communicate with. Additionally, it exposes log streaming to the AppHost, which ultimately streams logs to the developer dashboard.
* `dcpctrl.exe`: Controller that monitors the API server for new objects and changes, ensuring that the real-world environment matches the specified model.
Note
DCP operates on the principle of “eventual consistency,” meaning that changes to the model and the real-world environment are applied asynchronously. While this approach may introduce noticeable delays, DCP is designed to diligently synchronize both states. Unlike a “strongly consistent” system that might fail immediately on encountering issues, DCP persistently retries until the desired state is achieved or an error is conclusively determined, often resulting in a more robust alignment between the model and the real world.
When the AppHost runs, it uses Kubernetes client libraries to communicate with DCP. It translates the app model into a format DCP can process by converting the model’s resources into specifications. Specifically, this involves generating Kubernetes Custom Resource Definitions (CRDs) that represent the application’s desired state.
DCP performs the following tasks:
* Prepares the resources for execution:
* Configures service endpoints.
* Assigns names and ports dynamically, unless explicitly set (DCP ensures that the ports are available and not in use by other processes).
* Initializes container networks.
* Pulls container images based on their applied `ImagePullPolicy`.
* Creates and starts containers.
* Runs executables with the required arguments and environment variables.
* Monitors resources:
* Provides change notifications about objects managed within DCP, including process IDs, running status, and exit codes (the AppHost subscribes to these changes to manage the [application’s lifecycle](/app-host/eventing/) effectively).
* Starts the developer dashboard.
Continuing from the diagram in the previous section, consider the following diagram that helps to visualize the responsibilities of DCP:

DCP logs are streamed back to the AppHost, which then forwards them to the developer dashboard. While the developer dashboard exposes commands such as start, stop, and restart, these commands are not part of DCP itself. Instead, they are implemented by the app model runtime, specifically within its “dashboard service” component. These commands operate by manipulating DCP objects—creating new ones, deleting old ones, or updating their properties. For example, restarting a .NET project involves stopping and deleting the existing `ExecutableResource` representing the project and creating a new one with the same specifications.
## Developer dashboard
[Section titled “Developer dashboard”](#developer-dashboard)
The Aspire developer dashboard is a powerful tool designed to simplify local development and resource management. It also supports a standalone mode and integrates seamlessly when publishing to Azure Container Apps. With its intuitive interface, the dashboard empowers developers to monitor, manage, and interact with application resources effortlessly.
### Monitor and manage resources
[Section titled “Monitor and manage resources”](#monitor-and-manage-resources)
The dashboard provides a user-friendly interface for inspecting resource states, viewing logs, and executing commands. Whether you’re debugging locally or deploying to the cloud, the dashboard ensures you have full visibility into your application’s behavior.
### Built-in and custom commands
[Section titled “Built-in and custom commands”](#built-in-and-custom-commands)
The dashboard provides a set of commands for managing resources, such as start, stop, and restart. While commands appear as intuitive actions in the dashboard UI, under the hood, they operate by manipulating DCP objects. For more information, see *Stop or Start a resource*.
In addition to these built-in commands, you can define custom commands tailored to your application’s needs. These custom commands are registered in the app model and seamlessly integrated into the dashboard, providing enhanced flexibility and control.
### Real-time log streaming
[Section titled “Real-time log streaming”](#real-time-log-streaming)
Stay informed with the dashboard’s real-time log streaming feature. Logs from all resources in your app model are streamed from DCP to the AppHost and displayed in the dashboard. With advanced filtering options—by resource type, severity, and more—you can quickly pinpoint relevant information and troubleshoot effectively.
The developer dashboard is more than just a tool—it’s your command center for building, debugging, and managing Aspire applications with confidence and ease.
# Resource API Patterns
> Discover common API resource patterns in Aspire, including how to add and configure resources, use annotations, and compose hosting and client integrations.
Aspire’s resource model allows you to define and configure resources in a structured way, enabling seamless integration and management of your application’s components. This guide provides details the common patterns for adding and configuring resources in Aspire.
## API patterns
[Section titled “API patterns”](#api-patterns)
Aspire separates **resource data models** from **behavior** using **fluent extension methods**.
* **Resource classes** define only constructors and properties.
* **Extension methods** implement resource creation, configuration, and runtime wiring.
This guide describes each pattern and shows a **verbatim Redis example** at the end. It also covers how to publish manifests via custom resources.
## Adding resources with `AddX(...)`
[Section titled “Adding resources with AddX(...)”](#adding-resources-with-addx)
An `AddX(...)` method executes:
1. **Validate inputs** (`builder`, `name`, required arguments).
2. **Instantiate** the data-only resource (`new TResource(...)`).
3. **Register** it with `builder.AddResource(resource)`.
4. **Optional wiring** of endpoints, health checks, container settings, environment variables, command-line arguments, and event subscriptions.
### Signature pattern
[Section titled “Signature pattern”](#signature-pattern)
* TypeScript
```typescript
// Resources are added via async builder methods:
const resource = await builder.addRedis("name" /*, optional params */);
// The builder handles validation, instantiation,
// and registration internally. Optional wiring:
// .withEndpoint(...)
// .withHealthCheck(...)
// .withImage(...)
// .withEnvironment(...)
// .withArgs(...)
// builder.addEventingSubscriber(...)
```
* C#
```csharp
public static IResourceBuilder AddX(
this IDistributedApplicationBuilder builder,
[ResourceName] string name,
/* optional parameters */)
{
// 1. Validate inputs
// 2. Instantiate resource
// 3. builder.AddResource(resource)
// 4. Optional wiring:
// .WithEndpoint(...)
// .WithHealthCheck(...)
// .WithImage(...)
// .WithEnvironment(...)
// .WithArgs(...)
// Eventing.Subscribe<...>(...)
}
```
### Optional wiring examples
[Section titled “Optional wiring examples”](#optional-wiring-examples)
**Endpoints**:
* TypeScript
```typescript
resource.withEndpoint({ port: hostPort, targetPort: containerPort, name: endpointName });
```
* C#
```csharp
.WithEndpoint(port: hostPort, targetPort: containerPort, name: endpointName)
```
**Health checks**:
* TypeScript
```typescript
resource.withHealthCheck(healthCheckKey);
```
* C#
```csharp
.WithHealthCheck(healthCheckKey)
```
**Container images / registries**:
* TypeScript
```typescript
resource.withImage(imageName, imageTag);
resource.withImageRegistry(registryUrl);
```
* C#
```csharp
.WithImage(imageName, imageTag)
.WithImageRegistry(registryUrl)
```
**Entrypoint and args**:
* TypeScript
```typescript
resource.withEntrypoint("/bin/sh");
resource.withArgs(["--flag", "value"]);
```
* C#
```csharp
.WithEntrypoint("/bin/sh")
.WithArgs(context => { /* build args */ return Task.CompletedTask; })
```
**Environment variables**:
* TypeScript
```typescript
resource.withEnvironment("ENV_VAR", value);
```
* C#
```csharp
.WithEnvironment(context => new("ENV_VAR", valueProvider))
```
**Event subscriptions**:
* TypeScript
```typescript
builder.addEventingSubscriber(async (context) => {
context.onBeforeStart(async (event) => {
// Handle event
});
});
```
* C#
```csharp
builder.Eventing.Subscribe(resource, handler);
```
### Summary table
[Section titled “Summary table”](#summary-table)
| Step | Call/Method | Purpose |
| --------------- | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
| Validate | `ArgumentNullException.ThrowIfNull(...)` | Ensure non-null builder, name, and args |
| Instantiate | `new TResource(name, …)` | Create data-only instance |
| Register | `builder.AddResource(resource)` | Add resource to the application model |
| Optional wiring | `.WithEndpoint(…)`, `.WithHealthCheck(…)`, `.WithImage(…)`, `.WithEnvironment(…)`, `.WithArgs(…)`, `Eventing.Subscribe(…)` | Configure container details, wiring, and runtime hooks |
## Configuring resources with `WithX(...)`
[Section titled “Configuring resources with WithX(...)”](#configuring-resources-with-withx)
`WithX(...)` methods **attach annotations** to resource builders.
### Signature pattern
[Section titled “Signature pattern”](#signature-pattern-1)
* TypeScript
Note
The TypeScript SDK doesn’t expose `withAnnotation(...)` directly. Configuration methods like `withEndpoint(...)` and `withEnvironment(...)` handle annotation attachment internally.
* C#
```csharp
public static IResourceBuilder WithX(
this IResourceBuilder builder,
FooOptions options) =>
builder.WithAnnotation(new FooAnnotation(options));
```
- **Target**: `IResourceBuilder`.
- **Action**: `WithAnnotation(...)`.
- **Returns**: `IResourceBuilder`.
### Summary table
[Section titled “Summary table”](#summary-table-1)
| Method | Target | Action |
| ------------ | ----------------------------- | ------------------------------------------------------ |
| `WithX(...)` | `IResourceBuilder` | Attaches `XAnnotation` using the `WithAnnotation` API. |
| Returns | `IResourceBuilder` | Enables fluent chaining . |
## Annotations
[Section titled “Annotations”](#annotations)
Annotations are **public** metadata types implementing `IResourceAnnotation`. They can be added or removed dynamically at runtime via hooks or events. Consumers can query annotations using `TryGetLastAnnotation()` when necessary.
### Definition and attachment
[Section titled “Definition and attachment”](#definition-and-attachment)
* TypeScript
Note
The TypeScript SDK doesn’t expose annotations as a user-facing pattern. Resource metadata is managed internally by the SDK’s configuration methods.
* C#
```csharp
public sealed record PersistenceAnnotation(
TimeSpan? Interval,
int KeysChangedThreshold) : IResourceAnnotation;
builder.WithAnnotation(new PersistenceAnnotation(
TimeSpan.FromSeconds(60),
100));
```
### Summary table
[Section titled “Summary table”](#summary-table-2)
| Concept | Pattern | Notes |
| --------------- | ------------------------------------------------------- | ---------------------------------------- |
| Annotation Type | `public record XAnnotation(...) : IResourceAnnotation` | Public to support dynamic runtime use. |
| Attach | `builder.WithAnnotation(new XAnnotation(...))` | Adds metadata to resource builder. |
| Query | `resource.TryGetLastAnnotation(out var a)` | Consumers inspect annotations as needed. |
## Custom value objects
[Section titled “Custom value objects”](#custom-value-objects)
Custom value objects defer evaluation and allow the framework to discover dependencies between resources.
### Core interfaces
[Section titled “Core interfaces”](#core-interfaces)
| Interface | Member | Mode | Purpose |
| ------------------------------- | ----------------------------------------------------------- | ---------------- | ----------------------------------------------------------------------------- |
| `IValueProvider` | `ValueTask GetValueAsync(CancellationToken)` | Run | Resolve live values at runtime |
| `IManifestExpressionProvider` | `string ValueExpression { get; }` | Publish | Emit structured expressions in manifests |
| `IExpressionValue` | Inherits `IValueProvider` and `IManifestExpressionProvider` | Run and publish | Mark a value object as usable wherever an expression-backed value is accepted |
| `IValueWithReferences` *(opt.)* | `IEnumerable