Standalone Aspire Dashboard Setup for Distributed .NET Applications

Picture this. You have a distributed .NET application running across multiple services, each with its own database, and a message queue connecting them. One morning a request starts taking three seconds instead of three hundred milliseconds, and nobody can tell you which service is responsible. This is the exact moment where good observability tooling earns its keep.

Most teams reach for .NET Aspire when they hear this problem, and assume they need to adopt the full orchestration model to get any benefit from it. That is not true. The Aspire Dashboard, the piece that gives you distributed tracing, structured logs, and live metrics, runs perfectly well as a standalone container. You do not need to touch your existing Docker Compose or Kubernetes setup to use it.

This is useful when you already have a deployment story sorted out and just want the observability layer bolted on. It is also handy when you are debugging an existing system that was never built with Aspire in mind. In both cases you get a working setup in under five minutes.

Why run the dashboard on its own

The full Aspire orchestration model is genuinely useful when you are building a new distributed application from scratch, since it manages service discovery, container lifecycles, and configuration for you. But rewriting an existing Docker Compose or Kubernetes setup just to get tracing is a lot of effort for very little gain. The standalone dashboard skips all of that and gives you only the observability piece.

  • Drop-in observability: add one container to your existing setup, nothing else changes
  • Full OpenTelemetry support: works with any application that speaks OTLP, not just .NET
  • Developer friendly: built for local development and debugging sessions, not for running a production fleet
  • Immediate value: traces, logs, and metrics show up within minutes of wiring it in

There is one important catch worth calling out before you get attached to this setup. The dashboard stores everything in memory, so a restart wipes your data clean. That is fine for a debugging session at your desk, but it rules the dashboard out as a production monitoring solution. For production workloads you would still reach for something like Jaeger for traces, Prometheus for metrics, or a commercial APM product such as Application Insights.

Step 1: add the dashboard container

Add this service to your existing docker-compose.yml file. There is no orchestration logic here, just a plain container definition.

aspire-dashboard:
  container_name: aspire-dashboard
  image: mcr.microsoft.com/dotnet/aspire-dashboard:13.0
  ports:
    - 18888:18888

That single block is the entire setup for the dashboard itself. Run docker compose up and the container starts immediately, but if you navigate to http://localhost:18888 you will be asked for a login token rather than dropped straight into the UI. This is a security default, since the dashboard has no user accounts of its own.

Aspire Dashboard login screen asking for a token
Aspire Dashboard login screen asking for a token

The token is generated fresh every time the container starts, and you will find it in the container logs along with a ready-made login link. Run docker logs aspire-dashboard and look near the top of the output.

Login link with token printed in the container logs
Login link with token printed in the container logs

Click through that link and you land on an empty dashboard. Nothing to see yet because none of your services are sending it any data. That is what the next two steps fix.

Step 2: point your services at the dashboard

Every service that should show up in the dashboard needs to know where to send its telemetry. You do this with two environment variables on each API container, no code changes required at this stage.

users.api:
  image: ${DOCKER_REGISTRY-}usersapi
  build:
    context: .
    dockerfile: Users.Api/Dockerfile
  ports:
    - 5100:5100
    - 5101:5101
  environment:
    - OTEL_EXPORTER_OTLP_ENDPOINT=http://aspire-dashboard:18889
    - OTEL_EXPORTER_OTLP_PROTOCOL=grpc
  depends_on:
    - users.database

Notice the port number here is 18889, not 18888. The dashboard actually exposes two ports: 18888 serves the web UI you log into, while 18889 is the OTLP ingestion endpoint that receives telemetry data over gRPC. Mixing these two up is a common mistake, and the symptom is simply an empty dashboard with no error message telling you why.

Repeat this environment variable block for every service you want visibility into. There is no limit on how many services can point at the same dashboard instance, since it is just receiving OTLP data over the network.

Step 3: configure OpenTelemetry in your code

The environment variables tell your app where to send data, but your app still needs the OpenTelemetry libraries installed and configured to actually produce that data. Start by adding these packages to your API project.

<PackageReference Include="Npgsql.OpenTelemetry" Version="9.0.3" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0" />

Swap Npgsql.OpenTelemetry for whatever database driver instrumentation matches your stack, SQL Server and MySQL both have equivalent packages. Once the packages are in place, wire them up in Program.cs.

builder.Services.AddOpenTelemetry()
    .ConfigureResource(resource => resource.AddService(builder.Environment.ApplicationName))
    .WithTracing(tracing => tracing
        .AddHttpClientInstrumentation()
        .AddAspNetCoreInstrumentation()
        .AddNpgsql())
    .WithMetrics(metrics => metrics
        .AddHttpClientInstrumentation()
        .AddAspNetCoreInstrumentation());
 
builder.Logging.AddOpenTelemetry(options =>
{
    options.IncludeScopes = true;
    options.IncludeFormattedMessage = true;
});
 
builder.Services.AddOpenTelemetry().UseOtlpExporter();

AddService gives each service a name in the dashboard, so use something recognizable like the service name itself rather than a generic string. The tracing block instruments outgoing HTTP calls, incoming ASP.NET Core requests, and Npgsql database queries, while the metrics block covers request duration and throughput for the same two sources. The IncludeScopes and IncludeFormattedMessage options on the logging side matter more than they look, since without them your structured logs lose the extra context fields that make correlating a log entry with a trace possible.

The last line, UseOtlpExporter, is what actually reads the OTEL_EXPORTER_OTLP_ENDPOINT variable from your environment and ships everything to the dashboard. If you forget this call, the rest of the configuration runs fine locally but nothing ever leaves the process.

What shows up once it is running

Start your services and send a handful of requests through them. The dashboard populates within seconds, no refresh or extra configuration needed on your end.

Structured logs

Every log line carries its full context along with it, trace identifiers, the request path, and any user identity attached to the call. Clicking into an individual entry expands the complete structured payload rather than just the formatted message text.

Structured logs view with full request context
Structured logs view with full request context

Distributed traces

This is where the setup earns its money. The trace view shows the complete path a request took across every service it touched, which database call inside that path was slow, and which downstream HTTP call actually failed. For the three-second-request scenario from the start of this article, this is the screen that tells you where those three seconds actually went.

Distributed trace view across services
Distributed trace view across services

Clicking into any single trace opens up its individual spans along with whatever metadata was attached to each one, which is usually enough to pinpoint the exact database query or external call causing the slowdown.

Span-level detail inside a single trace
Span-level detail inside a single trace

Real time metrics

Response times, error rates, and throughput update live as traffic flows through your services. This view is particularly useful while running a load test, since you can watch how latency and error rate move together as load increases, instead of reconstructing that story afterward from log files.

Live request metrics in the dashboard
Live request metrics in the dashboard

Where this fits and where it does not

Treat the standalone dashboard as a debugging tool for your desk, not as infrastructure you deploy anywhere near production. Because everything lives in memory, a container restart during a demo or a long debugging session throws all your trace history away with no warning. There has been talk on the Aspire roadmap of adding persistence, but as things stand today, plan around the in-memory limitation rather than against it.

For an actual production rollout, keep the OpenTelemetry instrumentation code exactly as it is in this article, since that part does not change, and simply point the OTEL_EXPORTER_OTLP_ENDPOINT variable at a proper backend instead of the dashboard container. Azure Monitor and Application Insights both accept OTLP directly, which makes this a one-line configuration change rather than a rewrite. Jaeger and Prometheus are solid open source alternatives if you are not on Azure.

The overall time investment here is genuinely small for what you get back. One container, two environment variables per service, and a handful of lines of OpenTelemetry configuration gets you tracing, logging, and metrics that would otherwise take a proper observability stack to assemble. Use it during development, and swap the endpoint when you are ready for production.

Leave a Reply

Discover more from Behind the Stack

Subscribe now to keep reading and get access to the full archive.

Continue reading