Migrate from Docker Compose to Aspire
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”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”| 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”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_onsupports 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 to inspect logs, traces, and metrics, including through coding-agent CLI or MCP workflows. Standalone mode doesn’t add AppHost resource controls to Compose.
For detailed API mappings, see Docker Compose to Aspire AppHost API reference.
Common migration patterns
Section titled “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”This example shows a typical three-tier application with a frontend, API, and database.
Docker Compose example:
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:
import { function createBuilder(): IDistributedApplicationBuilder
Creates a new distributed application builder
createBuilder } from './.aspire/modules/aspire.mjs';
const const builder: IDistributedApplicationBuilder
builder = await function createBuilder(): IDistributedApplicationBuilder
Creates a new distributed application builder
createBuilder();
// Add PostgreSQL with explicit version and persistent storageconst const database: PostgresDatabaseResource
database = (await const builder: IDistributedApplicationBuilder
builder.IDistributedApplicationBuilder.addPostgres(name: string, options?: { userName?: string | ParameterResource; password?: string | ParameterResource; port?: number;}): PostgresServerResource (+1 overload)
Adds a PostgreSQL resource to the application model. A container is used for local development.
addPostgres("postgres") .ContainerResource.withImageTag(tag: string): PostgresServerResource
Allows overriding the image tag on a container.
withImageTag("15") .PostgresServerResource.withDataVolume(options?: { name?: string; isReadOnly?: boolean;} | undefined): PostgresServerResource (+1 overload)
Adds a named volume for the data folder to a PostgreSQL container resource.
withDataVolume()) .PostgresServerResource.addDatabase(name: string, options?: { databaseName?: string;} | undefined): PostgresDatabaseResource (+1 overload)
Adds a PostgreSQL database to the application model.
addDatabase("myapp");
// Add the API project with proper dependenciesconst const api: ProjectResource
api = await const builder: IDistributedApplicationBuilder
builder.IDistributedApplicationBuilder.addProject(name: string, projectPath: string, options?: { launchProfileOrOptions?: ProjectResourceOptions;}): ProjectResource (+1 overload)
Adds a .NET project resource
addProject("api", "./MyApp.Api/MyApp.Api.csproj") .ProjectResource.withHttpEndpoint(options?: { port?: number; targetPort?: number; name?: string; env?: string; isProxied?: boolean;} | undefined): ProjectResource (+1 overload)
Adds an HTTP endpoint
withHttpEndpoint({ port?: number | undefined
port: 5000 }) .ProjectResource.withHttpHealthCheck(path?: string, statusCode?: number, endpointName?: string): ProjectResource (+1 overload)
Adds a health check to the resource which is mapped to a specific endpoint.
withHttpHealthCheck("/health") .ProjectResource.withReference(source: EndpointReference | string | uri, connectionName?: string, optional?: boolean, name?: string): ProjectResource (+1 overload)
Adds a reference to another resource
withReference(const database: PostgresDatabaseResource
database, "DefaultConnection") .ProjectResource.waitFor(dependency: IResource | IResourceWithConnectionString, waitBehavior?: WaitBehavior): ProjectResource
Waits for another resource to be ready
waitFor(const database: PostgresDatabaseResource
database);
// Add the frontend project with dependenciesconst const frontend: ProjectResource
frontend = await const builder: IDistributedApplicationBuilder
builder.IDistributedApplicationBuilder.addProject(name: string, projectPath: string, options?: { launchProfileOrOptions?: ProjectResourceOptions;}): ProjectResource (+1 overload)
Adds a .NET project resource
addProject("frontend", "./MyApp.Frontend/MyApp.Frontend.csproj") .ProjectResource.withHttpEndpoint(options?: { port?: number; targetPort?: number; name?: string; env?: string; isProxied?: boolean;} | undefined): ProjectResource (+1 overload)
Adds an HTTP endpoint
withHttpEndpoint({ port?: number | undefined
port: 3000 }) .ProjectResource.withReference(source: EndpointReference | string | uri, options?: { connectionName?: string; optional?: boolean; name?: string;} | undefined): ProjectResource (+1 overload)
Adds a reference to another resource
withReference(const api: ProjectResource
api) .ProjectResource.withEnvironment(name: string, value: string | IResourceWithConnectionString | IValueProvider): ProjectResource
Sets an environment variable
withEnvironment("API_URL", const api: ProjectResource
api.ProjectResource.getEndpoint(name: string): EndpointReference
Gets an endpoint reference
getEndpoint("http")) .ProjectResource.waitFor(dependency: IResource | IResourceWithConnectionString, waitBehavior?: WaitBehavior): ProjectResource
Waits for another resource to be ready
waitFor(const api: ProjectResource
api);
await const builder: IDistributedApplicationBuilder
builder.IDistributedApplicationBuilder.build(): DistributedApplication
Builds the distributed application
build().DistributedApplication.run(cancellationToken?: cancellationToken): void
Runs the distributed application
run();var builder = DistributedApplication.CreateBuilder(args);
// Add PostgreSQL with explicit version and persistent storagevar database = builder.AddPostgres("postgres") .WithImageTag("15") .WithDataVolume() .AddDatabase("myapp");
// Add the API project with proper dependenciesvar api = builder.AddProject<Projects.MyApp_Api>("api") .WithHttpEndpoint(port: 5000) .WithHttpHealthCheck("/health") .WithReference(database, "DefaultConnection") .WaitFor(database);
// Add the frontend project with dependenciesvar frontend = builder.AddProject<Projects.MyApp_Frontend>("frontend") .WithHttpEndpoint(port: 3000) .WithReference(api) .WithEnvironment("API_URL", api.GetEndpoint("http")) .WaitFor(api);
builder.Build().Run();Key differences explained:
- Build vs. project — Docker Compose
build:services becomeAddProject<T>()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_onwith health conditions; Aspire usesWaitFor()for startup ordering - Service discovery —
WithReference()only configures service discovery and connection strings; it doesn’t control startup order - Connection strings — By default,
WithReference(database)providesConnectionStrings__myappusing the resource name fromAddDatabase(). To match a different name likeDefaultConnection, use a named reference:.WithReference(database, "DefaultConnection") - Volumes —
WithDataVolume()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”This example shows a mix of existing container images and a Dockerfile-built service being orchestrated.
Docker Compose example:
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:
import { function createBuilder(): IDistributedApplicationBuilder
Creates a new distributed application builder
createBuilder } from './.aspire/modules/aspire.mjs';
const const builder: IDistributedApplicationBuilder
builder = await function createBuilder(): IDistributedApplicationBuilder
Creates a new distributed application builder
createBuilder();
// Add backing services with explicit versionsconst const redis: RedisResource
redis = await const builder: IDistributedApplicationBuilder
builder.IDistributedApplicationBuilder.addRedis(name: string, options?: { port?: number; password?: string | ParameterResource;}): RedisResource (+1 overload)
Adds a Redis container to the application model.
addRedis("redis") .ContainerResource.withImageTag(tag: string): RedisResource
Allows overriding the image tag on a container.
withImageTag("7") .RedisResource.withHostPort(port: number | null): RedisResource
Configures the host port that the Redis resource is exposed on instead of using randomly assigned port.
withHostPort(6379);
const const postgres: PostgresDatabaseResource
postgres = (await const builder: IDistributedApplicationBuilder
builder.IDistributedApplicationBuilder.addPostgres(name: string, options?: { userName?: string | ParameterResource; password?: string | ParameterResource; port?: number;}): PostgresServerResource (+1 overload)
Adds a PostgreSQL resource to the application model. A container is used for local development.
addPostgres("postgres") .ContainerResource.withImageTag(tag: string): PostgresServerResource
Allows overriding the image tag on a container.
withImageTag("15") .PostgresServerResource.withDataVolume(options?: { name?: string; isReadOnly?: boolean;} | undefined): PostgresServerResource (+1 overload)
Adds a named volume for the data folder to a PostgreSQL container resource.
withDataVolume()) .PostgresServerResource.addDatabase(name: string, options?: { databaseName?: string;} | undefined): PostgresDatabaseResource (+1 overload)
Adds a PostgreSQL database to the application model.
addDatabase("main");
// Build the web app from a Dockerfile (matches Docker Compose "build: .")const const web: ContainerResource
web = await const builder: IDistributedApplicationBuilder
builder.IDistributedApplicationBuilder.addDockerfile(name: string, contextPath: string, options?: { dockerfilePath?: string; stage?: string;}): ContainerResource (+1 overload)
Adds a Dockerfile to the application model that can be treated like a container resource.
addDockerfile("web", ".") .ContainerResource.withHttpEndpoint(options?: { port?: number; targetPort?: number; name?: string; env?: string; isProxied?: boolean;} | undefined): ContainerResource (+1 overload)
Adds an HTTP endpoint
withHttpEndpoint({ port?: number | undefined
port: 8080, targetPort?: number | undefined
targetPort: 8080 }) .ContainerResource.withReference(source: EndpointReference | string | uri, options?: { connectionName?: string; optional?: boolean; name?: string;} | undefined): ContainerResource (+1 overload)
Adds a reference to another resource
withReference(const redis: RedisResource
redis) .ContainerResource.withReference(source: EndpointReference | string | uri, options?: { connectionName?: string; optional?: boolean; name?: string;} | undefined): ContainerResource (+1 overload)
Adds a reference to another resource
withReference(const postgres: PostgresDatabaseResource
postgres) .ContainerResource.waitFor(dependency: IResource | IResourceWithConnectionString, waitBehavior?: WaitBehavior): ContainerResource
Waits for another resource to be ready
waitFor(const redis: RedisResource
redis) .ContainerResource.waitFor(dependency: IResource | IResourceWithConnectionString, waitBehavior?: WaitBehavior): ContainerResource
Waits for another resource to be ready
waitFor(const postgres: PostgresDatabaseResource
postgres);
await const builder: IDistributedApplicationBuilder
builder.IDistributedApplicationBuilder.build(): DistributedApplication
Builds the distributed application
build().DistributedApplication.run(cancellationToken?: cancellationToken): void
Runs the distributed application
run();var builder = DistributedApplication.CreateBuilder(args);
// Add backing services with explicit versionsvar 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();Key differences explained:
- Image versions — Explicitly specified with
WithImageTag()to match Docker Compose - Dockerfile builds — Docker Compose
build: .maps toAddDockerfile("web", "."), which builds a container image from a Dockerfile. UseAddContainer()for pre-built images that useimage: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 Composedepends_onwith conditions - Connection strings —
WithReference()provides Aspire-format connection strings (ConnectionStrings__*), not URL-format variables
Environment variables and configuration
Section titled “Environment variables and configuration”This example shows different approaches to configuration management.
Docker Compose approach:
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=infoAspire approach:
import { function createBuilder(): IDistributedApplicationBuilder
Creates a new distributed application builder
createBuilder } from './.aspire/modules/aspire.mjs';
const const builder: IDistributedApplicationBuilder
builder = await function createBuilder(): IDistributedApplicationBuilder
Creates a new distributed application builder
createBuilder();
// Add external parameter for secretsconst const apiKey: ParameterResource
apiKey = const builder: IDistributedApplicationBuilder
builder.IDistributedApplicationBuilder.addParameter(name: string, options?: { value?: string; publishValueAsDefault?: boolean; secret?: boolean;}): ParameterResource (+1 overload)
Adds a parameter resource
addParameter("apiKey", { secret?: boolean | undefined
secret: true });
const const database: PostgresDatabaseResource
database = (await const builder: IDistributedApplicationBuilder
builder.IDistributedApplicationBuilder.addPostgres(name: string, options?: { userName?: string | ParameterResource; password?: string | ParameterResource; port?: number;}): PostgresServerResource (+1 overload)
Adds a PostgreSQL resource to the application model. A container is used for local development.
addPostgres("db")) .PostgresServerResource.addDatabase(name: string, options?: { databaseName?: string;} | undefined): PostgresDatabaseResource (+1 overload)
Adds a PostgreSQL database to the application model.
addDatabase("myapp");
const const cache: RedisResource
cache = await const builder: IDistributedApplicationBuilder
builder.IDistributedApplicationBuilder.addRedis(name: string, options?: { port?: number; password?: string | ParameterResource;}): RedisResource (+1 overload)
Adds a Redis container to the application model.
addRedis("cache");
const const app: ContainerResource
app = await const builder: IDistributedApplicationBuilder
builder.IDistributedApplicationBuilder.addContainer(name: string, image: AddContainerOptions): ContainerResource
Adds a container resource to the application.
addContainer("app", { AddContainerOptions.image?: string | undefined
image: "myapp", AddContainerOptions.tag?: string | undefined
tag: "latest" }) .ContainerResource.withReference(source: EndpointReference | string | uri, options?: { connectionName?: string; optional?: boolean; name?: string;} | undefined): ContainerResource (+1 overload)
Adds a reference to another resource
withReference(const database: PostgresDatabaseResource
database) .ContainerResource.withReference(source: EndpointReference | string | uri, options?: { connectionName?: string; optional?: boolean; name?: string;} | undefined): ContainerResource (+1 overload)
Adds a reference to another resource
withReference(const cache: RedisResource
cache) .ContainerResource.withEnvironment(name: string, value: string | IResourceWithConnectionString | IValueProvider): ContainerResource
Sets an environment variable
withEnvironment("API_KEY", const apiKey: ParameterResource
apiKey) .ContainerResource.withEnvironment(name: string, value: string | IResourceWithConnectionString | IValueProvider): ContainerResource
Sets an environment variable
withEnvironment("LOG_LEVEL", "info");
await const builder: IDistributedApplicationBuilder
builder.IDistributedApplicationBuilder.build(): DistributedApplication
Builds the distributed application
build().DistributedApplication.run(cancellationToken?: cancellationToken): void
Runs the distributed application
run();var builder = DistributedApplication.CreateBuilder(args);
// Add external parameter for secretsvar 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();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");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”Docker Compose example:
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:
import { function createBuilder(): IDistributedApplicationBuilder
Creates a new distributed application builder
createBuilder } from './.aspire/modules/aspire.mjs';
const const builder: IDistributedApplicationBuilder
builder = await function createBuilder(): IDistributedApplicationBuilder
Creates a new distributed application builder
createBuilder();
const const app: ContainerResource
app = await const builder: IDistributedApplicationBuilder
builder.IDistributedApplicationBuilder.addContainer(name: string, image: AddContainerOptions): ContainerResource
Adds a container resource to the application.
addContainer("app", { AddContainerOptions.image?: string | undefined
image: "myapp", AddContainerOptions.tag?: string | undefined
tag: "latest" }) .ContainerResource.withVolume(target: string, options?: { name?: string; isReadOnly?: boolean;} | undefined): ContainerResource (+1 overload)
Adds a volume to a container resource.
withVolume("/data", { name?: string | undefined
name: "app-data", isReadOnly?: boolean | undefined
isReadOnly: true }) .ContainerResource.withBindMount(source: string, target: string, options?: { isReadOnly?: boolean;} | undefined): ContainerResource (+1 overload)
Adds a bind mount to a container resource.
withBindMount("./config", "/app/config", { isReadOnly?: boolean | undefined
isReadOnly: true });
const const worker: ContainerResource
worker = await const builder: IDistributedApplicationBuilder
builder.IDistributedApplicationBuilder.addContainer(name: string, image: AddContainerOptions): ContainerResource
Adds a container resource to the application.
addContainer("worker", { AddContainerOptions.image?: string | undefined
image: "myworker", AddContainerOptions.tag?: string | undefined
tag: "latest" }) .ContainerResource.withVolume(target: string, options?: { name?: string; isReadOnly?: boolean;} | undefined): ContainerResource (+1 overload)
Adds a volume to a container resource.
withVolume("/shared", { name?: string | undefined
name: "app-data" });
await const builder: IDistributedApplicationBuilder
builder.IDistributedApplicationBuilder.build(): DistributedApplication
Builds the distributed application
build().DistributedApplication.run(cancellationToken?: cancellationToken): void
Runs the distributed application
run();var builder = DistributedApplication.CreateBuilder(args);
// Create a named volume for sharing datavar 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()for host directory access
Networking
Section titled “Networking”Docker Compose supports custom networks to isolate groups of services from each other:
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.
Migration strategy
Section titled “Migration strategy”Successfully migrating from Docker Compose to Aspire requires a systematic approach.
-
Assess your current setup
Section titled “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_ondeclarations - 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
-
Create the Aspire AppHost
Section titled “Create the Aspire AppHost”Start by creating a new Aspire project:
Terminal window aspire new aspire-starter -o MyApp -
Migrate services incrementally
Section titled “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<T>()for better integration - Convert Dockerfile-built containers using
AddDockerfile()to matchbuild:directives - Convert pre-built images using
AddContainer()to matchimage:directives - Configure dependencies with
WithReference()for service discovery - Add startup ordering with
WaitFor()to matchdepends_onbehavior - Set up environment variables — Note that connection string formats will differ
- Migrate health checks — Use
WithHttpHealthCheck()orWithHealthCheck()for custom checks
- Add backing services like PostgreSQL, Redis with specific versions using
-
Handle data migration
Section titled “Handle data migration”For persistent data:
- Use
WithDataVolume()for automatic volume management with integrations - Use
WithVolume()for named volumes that need to persist data - Use
WithBindMount()for host directory mounts when you need direct access to host files
- Use
-
Test and validate
Section titled “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”Common issues and solutions
Section titled “Common issues and solutions”Connection string format mismatch
Section titled “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():
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`);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”WithReference() only configures service discovery, not startup ordering.
Solution: Use WaitFor() to ensure dependencies are ready:
const api = await builder.addProject("api", "./Api/Api.csproj", "https") .withReference(database) // Service discovery .waitFor(database); // Startup orderingvar api = builder.AddProject<Projects.Api>("api") .WithReference(database) // Service discovery .WaitFor(database); // Startup orderingVolume mounting issues
Section titled “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”Aspire automatically assigns random ports by default.
Solution: Use WithHostPort() or WithHttpEndpoint(port:) for static port mapping:
const redis = await builder.addRedis("cache") .withHostPort(6379);var redis = builder.AddRedis("cache") .WithHostPort(6379);Health check migration
Section titled “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():
const api = await builder.addProject("api", "./Api/Api.csproj", "https") .withHttpHealthCheck("/health");var api = builder.AddProject<Projects.Api>("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:
const rabbit = await builder.addContainer("rabbitmq", "rabbitmq", "4.1.4-management-alpine") .withHealthCheck("rabbitmq-health");
// WaitFor uses the registered health check to determine readinessconst app = await builder.addProject("app", "./App/App.csproj", "https") .waitFor(rabbit);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 readinessvar app = builder.AddProject<Projects.App>("app") .WaitFor(rabbit);Next steps
Section titled “Next steps”After migrating to Aspire:
- Explore Aspire integrations to replace custom container configurations
- Set up health checks for better monitoring
- Learn about deployment options for production environments
- Consider testing your distributed application
- Review telemetry configuration for observability