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.

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.

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.

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.

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.

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.

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