Run microservices locally with one F5. Aspire handles orchestration, service discovery, and observability so you can stop wrestling with Docker Compose and broken READMEs.
My first day at a fintech startup in 2019, the tech lead said I’d be "up and running by lunch." I nodded. I’d done this before.
By 4 PM I had 14 browser tabs open. The README was from 2017. The wiki contradicted the Docker Compose file. The Docker Compose file referenced environment variables that existed nowhere. I installed PostgreSQL locally, uninstalled it because the container needed that port, then reinstalled it because the container wouldn’t start. Redis was running but wanted a password nobody remembered. Slack had three unanswered messages asking "did you try the script in /tools?"
Th…
Run microservices locally with one F5. Aspire handles orchestration, service discovery, and observability so you can stop wrestling with Docker Compose and broken READMEs.
My first day at a fintech startup in 2019, the tech lead said I’d be "up and running by lunch." I nodded. I’d done this before.
By 4 PM I had 14 browser tabs open. The README was from 2017. The wiki contradicted the Docker Compose file. The Docker Compose file referenced environment variables that existed nowhere. I installed PostgreSQL locally, uninstalled it because the container needed that port, then reinstalled it because the container wouldn’t start. Redis was running but wanted a password nobody remembered. Slack had three unanswered messages asking "did you try the script in /tools?"
There was no /tools folder.
The tech lead walked by: "How’s it going?"
"Great," I lied. "Almost there."
I don’t do that anymore. Aspire fixed it.
TL;DR - Key Takeaways
- Aspire is Microsoft’s cloud-native stack for running distributed systems locally with a single F5
- No more Docker Compose YAML - describe your entire system in 15 lines of C#
- Automatic service discovery - services find each other without hardcoded URLs
- Built-in observability dashboard - logs, traces, and metrics across all services
- Works with .NET, Python, and JavaScript - Aspire 13 added polyglot support
- Production-ready deployment - generates Kubernetes manifests, Docker Compose, and Bicep templates
Table of Contents
The Problem With Distributed Systems Development
Here’s what typically happens. You join a team building microservices. The architecture diagram looks elegant: a frontend, an API, a worker service, Redis for caching, PostgreSQL for persistence, maybe RabbitMQ for messaging. Beautiful.
Then you try to run it locally.
The README says:
1. Install Docker
2. Install Redis
3. Install PostgreSQL
4. Run database migrations
5. Update connection strings
6. Set environment variables for service discovery
7. Start services in the correct order
8. Pray
Three hours later:
Connection refused: localhost:5432
Service 'api' cannot resolve 'worker-service'
Redis: WRONGPASS invalid username-password pair
You message the team: "How do you run this locally?"
They respond: "Oh, you need to also run script X, set secret Y, and port Z is probably in use. Also, check the wiki. It might be outdated."
It’s always outdated.
Enter Aspire (Or: The "It Just Works" Framework)
Aspire (formerly .NET Aspire, rebranded with version 13) is Microsoft’s answer to the question: "Why is local development of distributed systems so painful?"
It’s an opinionated, cloud-native stack that handles:
- Orchestration: Spinning up all your services, databases, and containers with one F5
- Service Discovery: Services find each other automatically. No hardcoded URLs.
- Observability: A built-in dashboard showing logs, traces, and metrics across your entire system
- Integrations: Pre-built NuGet packages for Redis, PostgreSQL, RabbitMQ, and dozens more
The pitch is simple: clone the repo, hit F5, everything runs. First try.
I was skeptical. The last "one-click setup" I tried was Dapr in 2021, and I still have flashbacks to the sidecar debugging. But after using Aspire on three projects now, I’m a convert.
The AppHost: Your Application’s Brain
At the heart of Aspire is the AppHost project. It’s a C# file that describes your entire distributed system. For a complete understanding, see Microsoft’s App Host overview:
var builder = DistributedApplication.CreateBuilder(args);
// Add infrastructure
var postgres = builder.AddPostgres("postgres")
.AddDatabase("ordersdb");
var redis = builder.AddRedis("cache");
var rabbitMq = builder.AddRabbitMQ("messaging");
// Add your services
var api = builder.AddProject<Projects.OrdersApi>("api")
.WithReference(postgres)
.WithReference(redis)
.WithReference(rabbitMq);
var worker = builder.AddProject<Projects.OrderProcessor>("worker")
.WithReference(postgres)
.WithReference(rabbitMq)
.WaitFor(rabbitMq);
var frontend = builder.AddProject<Projects.WebApp>("frontend")
.WithReference(api)
.WithExternalHttpEndpoints();
builder.Build().Run();
That’s it. That’s your entire orchestration. No YAML. No shell scripts. No "run these five commands in the right order."
When you press F5:
- PostgreSQL container spins up automatically
- Redis container spins up automatically
- RabbitMQ container spins up automatically
- Connection strings are injected into your services
- Service discovery URLs are configured
- Health checks are monitored
- The dashboard launches showing everything
No prayer required.
Why This Is Better Than Docker Compose
I can hear you: "I can do all this with Docker Compose."
Sure. Let me show you the equivalent Docker Compose:
version: '3.8'
services:
postgres:
image: postgres:16
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ordersdb
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
redis:
image: redis:7
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
rabbitmq:
image: rabbitmq:3-management
ports:
- "5672:5672"
- "15672:15672"
environment:
RABBITMQ_DEFAULT_USER: guest
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD}
api:
build:
context: .
dockerfile: src/OrdersApi/Dockerfile
environment:
- ConnectionStrings__ordersdb=Host=postgres;Database=ordersdb;Username=postgres;Password=${POSTGRES_PASSWORD}
- ConnectionStrings__cache=redis:6379
- ConnectionStrings__messaging=amqp://guest:${RABBITMQ_PASSWORD}@rabbitmq:5672
- ASPNETCORE_URLS=http://+:80
ports:
- "5001:80"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
rabbitmq:
condition: service_started
worker:
build:
context: .
dockerfile: src/OrderProcessor/Dockerfile
environment:
- ConnectionStrings__ordersdb=Host=postgres;Database=ordersdb;Username=postgres;Password=${POSTGRES_PASSWORD}
- ConnectionStrings__messaging=amqp://guest:${RABBITMQ_PASSWORD}@rabbitmq:5672
depends_on:
postgres:
condition: service_healthy
rabbitmq:
condition: service_started
frontend:
build:
context: .
dockerfile: src/WebApp/Dockerfile
environment:
- Services__api=http://api:80
- ASPNETCORE_URLS=http://+:80
ports:
- "5000:80"
depends_on:
- api
volumes:
postgres_data:
Now add a .env file. Now add Dockerfiles for each project. Now rebuild images every time you change code. Now figure out why your debugger won’t attach. Now add OpenTelemetry configuration. Now create a separate docker-compose.override.yml for development settings.
The Aspire version? 15 lines of C#. IntelliSense. Type safety. F5 debugging. Hot reload.
The Dashboard (The Feature That Sells Itself)
When your Aspire application starts, it launches the Aspire dashboard at http://localhost:18888 (or another high localhost port shown in your console). This dashboard shows:
Resources View
Shows all your containers and projects. Status, endpoints, environment variables. You get it.
Console Logs
This is the one I actually live in. Aggregated logs from every service, filterable, searchable. No more juggling five terminal windows with docker logs -f running in each.
Distributed Traces
Distributed traces showing requests flowing through your entire system. Click a trace and see exactly which service took how long, where errors occurred, and what the call chain looked like.
Metrics
CPU, memory, request rates, error rates. All broken down per service. Built on OpenTelemetry, so it works with your existing observability stack.
I cannot overstate how useful this is during development. Before Aspire, debugging a distributed system locally meant:
- Check frontend logs
- Check API logs
- Check worker logs
- Check database
- Give up and add more
Console.WriteLine
Now I open the dashboard, find the trace, and see exactly where things went wrong. Production-grade observability, during development.
Service Discovery (The Magic Part)
Okay, this is my favorite part. Remember this line?
var frontend = builder.AddProject<Projects.WebApp>("frontend")
.WithReference(api);
That .WithReference(api) does something beautiful. In your frontend’s code, you can now do:
builder.Services.AddHttpClient<OrdersClient>(client =>
{
client.BaseAddress = new Uri("https+http://api");
});
That https+http://api isn’t a real URL. It’s a service discovery scheme. Aspire automatically resolves it to whatever address the API is running on, whether that’s localhost:5001 during development or orders-api.internal in production.
No hardcoded URLs. No environment-specific configuration. The same code works everywhere.
Under the hood, Aspire uses .NET’s IConfiguration abstraction. The connection information is injected as configuration that the service discovery resolver reads. Change your deployment target, and the resolution changes. Your code doesn’t.
Integrations: Batteries Included
Between the core Aspire integrations and the Aspire Community Toolkit, you get pre-built packages for most things you’d need:
| Category | Integrations |
|---|---|
| Databases | PostgreSQL, SQL Server, MySQL, MongoDB, Oracle, CosmosDB |
| Caching | Redis, Valkey, Garnet |
| Messaging | RabbitMQ, Azure Service Bus, Kafka |
| Storage | Azure Blob, AWS S3*, Minio* |
| AI | Azure OpenAI, Ollama* |
| Observability | OpenTelemetry (built-in), Seq |
* Available via Community Toolkit
Each integration handles:
- Container provisioning (in development)
- Connection string and configuration management
- Health checks
- Telemetry instrumentation
Many also plug into the shared HTTP resiliency setup you configure in ServiceDefaults.
Adding Redis caching to your project:
AppHost:
var redis = builder.AddRedis("cache");
builder.AddProject<Projects.Api>("api")
.WithReference(redis);
Api Project:
builder.AddRedisDistributedCache("cache");
Two lines of code. Connection string? Injected. Health checks? Configured. Telemetry? Instrumented. Retry policies? Applied.
The ServiceDefaults Pattern
Every Aspire project includes a ServiceDefaults project. It’s a shared library that configures cross-cutting concerns for your .NET services. See the service defaults documentation for details. The generated code looks something like this:
public static IHostApplicationBuilder AddServiceDefaults(
this IHostApplicationBuilder builder)
{
builder.ConfigureOpenTelemetry();
builder.AddDefaultHealthChecks();
builder.Services.AddServiceDiscovery();
builder.Services.ConfigureHttpClientDefaults(http =>
{
http.AddStandardResilienceHandler();
});
return builder;
}
One call to builder.AddServiceDefaults() in each service, and you get:
- OpenTelemetry traces, metrics, and logs
- Health check endpoints (
/health,/alive) - Service discovery for HttpClient
- Retry policies with exponential backoff
- Circuit breakers
Consistent configuration across every service. Change it once in ServiceDefaults, and every service picks it up.
Project Structure That Works
Here’s what a typical Aspire solution looks like:
src/
├── MyApp.AppHost/ # Orchestration (the brain)
│ └── Program.cs
├── MyApp.ServiceDefaults/ # Shared configuration
│ └── Extensions.cs
├── MyApp.Api/ # Your API service
├── MyApp.Worker/ # Background processor
├── MyApp.Web/ # Frontend
└── MyApp.Domain/ # Shared business logic
tests/
├── MyApp.Api.Tests/
├── MyApp.Worker.Tests/
└── MyApp.Integration.Tests/
The AppHost references all projects. ServiceDefaults is referenced by all services. Dependencies flow inward. Clean architecture still applies; Aspire just handles the orchestration layer.
Common Mistakes I Keep Seeing
1. Treating AppHost Like Production Configuration
The AppHost is for development orchestration. It describes topology, not production secrets.
// Don't do this
var postgres = builder.AddPostgres("postgres")
.WithEnvironment("POSTGRES_PASSWORD", "production-password-123");
Your production PostgreSQL is probably Azure Database or AWS RDS. When you deploy, the Aspire CLI or azd uses your AppHost topology but injects real connection info and secrets from your cloud platform (Key Vault, environment variables, etc.). Not from hardcoded values in code.
2. Skipping WaitFor on Dependencies
// Bad: Worker might start before RabbitMQ is ready
builder.AddProject<Projects.Worker>("worker")
.WithReference(rabbitMq);
// Good: Worker waits for RabbitMQ to be healthy
builder.AddProject<Projects.Worker>("worker")
.WithReference(rabbitMq)
.WaitFor(rabbitMq);
Without WaitFor, your service might crash on startup because the dependency isn’t ready. Health checks exist for a reason.
3. Hardcoding Service URLs
// No. Stop.
var apiUrl = "http://localhost:5001";
// Yes. Service discovery.
var apiUrl = "https+http://api";
The whole point is letting Aspire manage service addresses. The moment you hardcode a URL, you’ve lost the portability benefits.
4. Not Using the Dashboard
I’ve watched developers add Console.WriteLine statements to debug distributed issues while the Aspire dashboard sits open in another tab, showing traces with the exact error and call stack.
Use the dashboard. It’s there. It’s good.
5. Putting Everything in AppHost
// Don't put business logic here
var api = builder.AddProject<Projects.Api>("api")
.WithEnvironment("FeatureFlag__NewCheckout", "true")
.WithEnvironment("PricingTier", "enterprise")
.WithEnvironment("MaxRetries", "5")
// 47 more environment variables
AppHost handles orchestration. Configuration belongs in appsettings.json or a proper configuration service. Keep AppHost focused on "what runs and how it connects."
When Not to Use Aspire
Aspire isn’t for everything:
- Simple single-project apps: If you don’t have distributed components, Aspire adds complexity without benefit.
- Polyglot systems without .NET, Python, or JavaScript: Aspire 13 added first-class support for Python and JavaScript alongside .NET. Other runtimes can still be orchestrated as containers/executables and participate in the dashboard via OpenTelemetry, but you won’t get the same integrated debugging experience.
- Teams allergic to Microsoft tooling: If your team has strong opinions about avoiding Microsoft ecosystem lock-in, Aspire might be a tough sell. That said, the rebranding to "Aspire" (dropping ".NET") signals a more polyglot future.
Deployment: The Part Where It Gets Real
Aspire handles development beautifully. But what about production?
The AppHost is primarily for dev-time orchestration. You don’t run it in production. But Aspire as a stack has a mature deployment story now.
Aspire CLI (GA): The aspire publish and aspire deploy commands (generally available since Aspire 9.4) generate Docker Compose files, Kubernetes manifests, Bicep templates, and more via extensible publishers. Aspire 13 introduces aspire do, a flexible, parallelizable pipeline for build, publish, and deploy operations.
Azure Developer CLI (azd): First-class support for deploying Aspire apps to Azure Container Apps. Run azd up and your Aspire app deploys with proper configuration.
Kubernetes: Use Aspir8 to generate Helm charts from your AppHost, or use Aspire’s built-in Kubernetes publisher.
The key insight: your AppHost describes the topology. The publishing/deployment tooling translates that topology to your target platform. Production secrets and configuration come from your cloud platform (Key Vault, parameter stores, environment variables), not hardcoded in AppHost.
The Checklist
Before complaining that "distributed development is hard":
- [ ] Are you using Aspire?
- [ ] Can a new developer clone and run the entire system in under 5 minutes?
- [ ] Is service discovery automated, or are you manually managing URLs?
- [ ] Do you have observability across all services during development?
- [ ] Are connection strings injected, or hardcoded in config files?
- [ ] Can you debug any service with F5?
If any answer is "no," you’re making distributed development harder than it needs to be.
Getting Started Tomorrow
Want to try Aspire? Here’s the 5-minute version:
- Install the Aspire workload:
dotnet workload install aspire
- Create a new Aspire starter (or use
aspire newwith the Aspire CLI):
dotnet new aspire-starter -n MyDistributedApp
Open in Visual Studio or VS Code and press F5. 1.
Watch as Redis, a frontend, and an API all spin up automatically. 1.
Open the dashboard at http://localhost:18888 and explore.
Done. You’re running a distributed system locally with full observability.
Frequently Asked Questions
What is Aspire and how does it help with microservices development?
Aspire (formerly .NET Aspire) is Microsoft’s cloud-native application stack for building distributed systems. It solves the "works on my machine" problem by providing one-click orchestration of all your services, databases, and containers during local development. Instead of manually managing Docker Compose files, connection strings, and startup order, Aspire handles it all with a simple C# configuration. Learn more in the official Aspire overview.
Is Aspire a Docker Compose alternative for .NET?
For local development? Yes. See the comparison above.
How does Aspire service discovery work?
Aspire uses a special URI scheme (https+http://servicename) that automatically resolves to the correct address at runtime. When you add .WithReference(api) in your AppHost, Aspire injects the connection information as configuration. Your code uses the same service discovery URL in development and production. Aspire handles the resolution. See Microsoft’s service discovery documentation for implementation details.
Can I use Aspire with non-.NET services?
Yes. Python and JavaScript/Node.js have first-class support since Aspire 13. Other languages work as containers with OpenTelemetry for dashboard integration. You won’t get F5 debugging for non-.NET stuff, but orchestration and observability work fine.
How do I deploy Aspire applications to production?
Aspire provides multiple deployment paths. The Aspire CLI (aspire publish, aspire deploy) generates Docker Compose files, Kubernetes manifests, and Bicep templates. The Azure Developer CLI (azd up) deploys directly to Azure Container Apps. For Kubernetes, you can use the built-in publisher or Aspir8 to generate Helm charts. See the deployment overview for all options.
What databases and services does Aspire support?
Most things you’d actually use. PostgreSQL, SQL Server, Redis, RabbitMQ, Kafka, MongoDB, the Azure stuff. The Community Toolkit fills gaps like AWS S3 and Ollama. If it’s not supported, you can always add it as a container. I’ve yet to hit a blocker here.
Final Thoughts
Distributed systems are genuinely complex. Service discovery, connection management, health checks, observability. These problems don’t disappear just because you’re developing locally.
But the tooling for managing that complexity has been stuck in the YAML-and-shell-scripts era for too long. Aspire represents a different approach: describe your system in code, let the tooling handle the rest. And with Aspire 13’s polyglot support, this isn’t just for .NET shops anymore.
Is it perfect? No. Is it better than my three-hour "why won’t this connect" debugging sessions? Absolutely.
The goal isn’t to hide the complexity of distributed systems. It’s to stop reinventing the orchestration wheel every time you start a new project.
Clone. F5. It works.
Now if you’ll excuse me, I have a legacy microservices solution with 12 README files and a 400-line Docker Compose to migrate. Wish me luck.
Sources:
- Microsoft Learn: Aspire Overview
- Microsoft Learn: What’s New in Aspire
- Microsoft Learn: AppHost Orchestration
- Microsoft Learn: Service Discovery
- Microsoft Learn: Aspire Dashboard
- Microsoft Learn: Aspire Telemetry
- Microsoft Learn: Service Defaults
- Microsoft Learn: Integrations Overview
- Microsoft Learn: Deployment Overview
- Microsoft Learn: Azure Container Apps Deployment
About the Author
I’m Mashrul Haque, a Systems Architect with over 15 years of experience building enterprise applications with .NET, Blazor, ASP.NET Core, and SQL Server. I specialize in Azure cloud architecture, AI integration, and performance optimization.
When production catches fire at 2 AM, I’m the one they call.
- LinkedIn: Connect with me
- GitHub: mashrulhaque
- Twitter/X: @mashrulthunder
Follow me here on dev.to for more .NET and cloud-native content.