Skip to main content
SEMastery
DevOpsbeginner

Standalone Aspire Dashboard Setup for Distributed .NET Applications

Learn to run the standalone Aspire dashboard in Docker and view traces, logs, and metrics from your distributed .NET apps over OTLP, step by step.

13 min readUpdated September 20, 2025

A CCTV screen for your delivery hub

Imagine a busy courier hub in your city. Parcels arrive, get sorted, move to vans, and leave for delivery. The hub manager cannot stand next to every belt at once. So the manager sits in a small room with a wall of CCTV screens. Each screen shows one part of the hub. When a parcel goes missing or a belt jams, the manager looks at the screens and quickly sees where the problem is.

The standalone Aspire dashboard is that wall of screens for your software. Your distributed .NET app may have many parts: an API, a background worker, a payment service, a database. Each part sends little reports about what it is doing. The dashboard collects all those reports and shows them on one neat screen. You see request timelines, log messages, and live numbers, all in one place.

The best part is that you do not need to change your build, your deployment, or your project structure. You just run one small container, point your apps at it, and watch. In this guide we will set it up step by step using .NET 10 (the current LTS), but the same steps work on .NET 8 and 9.

What the dashboard actually does

The dashboard does not generate any data by itself. It is a receiver and a viewer. Your apps push telemetry to it using the OpenTelemetry Protocol, usually shortened to OTLP. OTLP is a shared, open wire format for sending three kinds of signals: traces, logs, and metrics.

Here is the whole idea in one picture.

Figure 1: Your apps export OTLP to the standalone dashboard, and you view it in the browser.

Because OTLP is an open standard, the dashboard does not care what language your service is written in. A C# API, a Python script, and a Java service can all send to the same dashboard. For this guide we focus on .NET, but keep that flexibility in mind.

Let us be clear about the three signals, since they show up on different tabs in the dashboard.

SignalWhat it isExample you will see
TracesThe journey of one request across servicesA waterfall showing API to database, 240 ms total
LogsText messages your code writes"Order 1042 saved", with level and timestamp
MetricsNumbers measured over timeRequests per second, memory used, error count
ResourcesThe list of apps sending dataapi, worker, payment, each with a status

The ports you need to know

The container uses a few ports. Mixing them up is the most common mistake, so it helps to learn them once. The numbers inside the container are fixed by Microsoft. You map them to ports on your own machine (the host) when you run the container.

Inside containerCommon host portPurpose
1888818888The web dashboard you open in the browser
188894317OTLP over gRPC, where apps send telemetry
188904318OTLP over HTTP, an alternative for apps

So when you start the container, you forward the browser port and at least one OTLP port. The dashboard UI lives on 18888. Your apps talk to 4317 or 4318.

Ports at a glance

18888 UI
4317 gRPC
4318 HTTP

Steps

1

18888 UI

Open this in your browser

2

4317 gRPC

App sends OTLP here (default)

3

4318 HTTP

App sends OTLP here (fallback)

Three port jobs: one to look at, two to send to.

Step 1: Run the dashboard in Docker

You need Docker installed and running. That is the only requirement. There is nothing to install into your .NET projects for the dashboard itself.

Open a terminal and run this command.

// This is a shell command, shown here for copy-paste convenience.
// docker run --rm -it -d \
//   -p 18888:18888 \
//   -p 4317:18889 \
//   -p 4318:18890 \
//   --name aspire-dashboard \
//   mcr.microsoft.com/dotnet/aspire-dashboard:latest

Let us read that command slowly, because each part has a job.

  • --rm removes the container when it stops, so you do not pile up old containers.
  • -it keeps it interactive, and -d runs it in the background (detached).
  • -p 18888:18888 maps the dashboard UI to your machine.
  • -p 4317:18889 maps the host's 4317 to the dashboard's gRPC OTLP port.
  • -p 4318:18890 maps the host's 4318 to the dashboard's HTTP OTLP port.
  • --name aspire-dashboard gives the container a friendly name so you can find it later.

Once it is running, open http://localhost:18888 in your browser. You will see a login page asking for a token. Do not panic. That is on purpose.

Figure 2: The startup flow from pulling the image to seeing the UI.

Step 2: Find your login token

By default the dashboard is locked so that no one nearby can peek at your telemetry. When the container starts, it writes a login token to its own logs. You read it and paste it in.

// Another shell command. Read the container logs:
// docker logs aspire-dashboard
//
// Look for a line like:
//   Login to the dashboard at http://localhost:18888/login?t=ab12cd34ef...
// Copy the value after t= and paste it into the login page,
// or just click the whole link.

If you are using Docker Desktop, you can also click the container and open the Logs tab to see the same line. The token changes every time the container restarts, so if you restart it, fetch the token again.

Skipping the login for local work

If you are only on your own machine and the constant token copying annoys you, you can turn authentication off. Add one environment variable when you start the container.

// Shell command with anonymous access turned on.
// Use this ONLY on your own machine, never on a shared server.
//
// docker run --rm -it -d \
//   -p 18888:18888 \
//   -p 4317:18889 \
//   -p 4318:18890 \
//   -e ASPIRE_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS=true \
//   --name aspire-dashboard \
//   mcr.microsoft.com/dotnet/aspire-dashboard:latest

With ASPIRE_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS set to true, the dashboard opens straight to the data with no login. This is handy for quick local demos. It is risky anywhere others can reach the dashboard, because telemetry can contain private details like user IDs or URLs. Keep the login on whenever the dashboard is not strictly private.

Step 3: Send telemetry from your .NET app

Now the fun part. We make a plain ASP.NET Core app export OpenTelemetry data to the dashboard. You do not need any Aspire packages. You only need the OpenTelemetry packages.

Add these NuGet packages to your project.

PackageWhy you need it
OpenTelemetry.Extensions.HostingWires OpenTelemetry into the .NET host
OpenTelemetry.Exporter.OpenTelemetryProtocolSends data using OTLP
OpenTelemetry.Instrumentation.AspNetCoreTraces incoming HTTP requests automatically
OpenTelemetry.Instrumentation.HttpTraces outgoing HttpClient calls automatically

Now configure them in Program.cs. The code below turns on traces, metrics, and logs, and points the OTLP exporter at the dashboard.

using OpenTelemetry.Logs;
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
 
var builder = WebApplication.CreateBuilder(args);
 
// Give this service a name so it shows up nicely in the dashboard.
var serviceName = "orders-api";
 
builder.Services.AddOpenTelemetry()
    .ConfigureResource(resource => resource.AddService(serviceName))
    .WithTracing(tracing => tracing
        .AddAspNetCoreInstrumentation()
        .AddHttpClientInstrumentation()
        .AddOtlpExporter())
    .WithMetrics(metrics => metrics
        .AddAspNetCoreInstrumentation()
        .AddRuntimeInstrumentation()
        .AddOtlpExporter());
 
// Logs are added on the logging builder.
builder.Logging.AddOpenTelemetry(logging =>
{
    logging.IncludeFormattedMessage = true;
    logging.IncludeScopes = true;
    logging.AddOtlpExporter();
});
 
var app = builder.Build();
 
app.MapGet("/", (ILogger<Program> logger) =>
{
    logger.LogInformation("Home endpoint was called");
    return "Hello from orders-api";
});
 
app.Run();

Notice that we call AddOtlpExporter() three times: once for tracing, once for metrics, and once for logs. Each one sends its own signal type. We do not pass an endpoint in the code. Instead we set the endpoint with environment variables, which keeps the code clean and easy to change per environment.

Step 4: Point the exporter at the dashboard

The OpenTelemetry SDK reads two standard environment variables to know where to send data and how to talk. Set them before you run the app.

// Shell, for the terminal where you run the app.
//
// Use gRPC on port 4317 (the .NET default):
//   setx OTEL_EXPORTER_OTLP_PROTOCOL grpc
//   setx OTEL_EXPORTER_OTLP_ENDPOINT http://localhost:4317
//
// Or use HTTP on port 4318 instead:
//   setx OTEL_EXPORTER_OTLP_PROTOCOL http/protobuf
//   setx OTEL_EXPORTER_OTLP_ENDPOINT http://localhost:4318
//
// On Linux or macOS, use: export OTEL_EXPORTER_OTLP_ENDPOINT=...

The variable OTEL_EXPORTER_OTLP_ENDPOINT tells the SDK where the dashboard is. The variable OTEL_EXPORTER_OTLP_PROTOCOL tells it whether to use gRPC or HTTP. Because we mapped the container ports to 4317 and 4318 earlier, the app and the dashboard now agree on the address.

Run the app, then open the home endpoint a few times. Switch to the dashboard in your browser and click around. Within a second or two you should see your service appear under Resources, your requests under Traces, your messages under Structured logs, and live numbers under Metrics.

End to end data flow

Request
Instrumentation
OTLP exporter
Dashboard
You read it

Steps

1

Request

User hits your API

2

Instrumentation

SDK records a span and logs

3

OTLP exporter

Sends to localhost:4317

4

Dashboard

Stores it in memory

5

You read it

View traces and logs in UI

From a single request to a line on the dashboard.

How a trace travels across services

The real value shows up when you have more than one service. Say your orders-api calls a payments-api over HTTP. OpenTelemetry adds a tiny header to that call so both services share the same trace id. The dashboard then stitches the two halves into one timeline. This is called distributed tracing, and it is exactly the CCTV-wall idea from the start.

Figure 3: A trace id is passed along so the dashboard can join both services into one timeline.

When you open that trace in the dashboard, you see a waterfall. The top bar is the orders-api request. Nested under it is the payments-api call. If the payment step is slow, the bar is wide and you can see it at a glance. This makes it easy to answer the question every team asks: where did the time go?

Keeping the dashboard running with Docker Compose

Typing the long docker run command every day gets boring. A small docker-compose.yml file lets you start the dashboard with one short command. It also keeps your settings in version control so your whole team uses the same setup.

// docker-compose.yml (YAML, shown in a code block for copy-paste)
//
// services:
//   aspire-dashboard:
//     image: mcr.microsoft.com/dotnet/aspire-dashboard:latest
//     container_name: aspire-dashboard
//     ports:
//       - "18888:18888"
//       - "4317:18889"
//       - "4318:18890"
//     environment:
//       - ASPIRE_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS=true

Now you start it with docker compose up -d and stop it with docker compose down. This is the friendliest way to share the dashboard with teammates, because everyone gets the same ports and the same options.

A few things that trip people up

A short list of common problems will save you time. Most setup pain comes from one of these.

  • No data appears. Check the OTLP endpoint. The app must point at http://localhost:4317 (gRPC) or http://localhost:4318 (HTTP), and the matching host port must be mapped in your docker run command.
  • Connection refused on gRPC. Some networks block gRPC. Switch OTEL_EXPORTER_OTLP_PROTOCOL to http/protobuf and send to 4318 instead.
  • Login token keeps changing. That is normal. Each container restart prints a fresh token. For local-only work, set anonymous access on.
  • Data disappears after a restart. The dashboard keeps telemetry in memory only. Restarting the container clears everything. This is fine for development. Use a durable backend for anything you must keep.
  • Logs look empty. Make sure you called AddOtlpExporter() on the logging builder, not only on tracing and metrics.

When to move beyond the standalone dashboard

The standalone dashboard is wonderful for local development and demos. It is light, fast, and needs no setup in your code beyond OpenTelemetry. But it is not a long-term store. It holds data in memory, and it caps how many traces and logs it keeps so it does not eat all your RAM.

For staging and production you usually send the same OTLP data to a durable system, such as Jaeger for traces, Prometheus and Grafana for metrics, or a hosted observability service. The lovely part is that your app code does not change at all. You only change the value of OTEL_EXPORTER_OTLP_ENDPOINT. Because everything speaks OTLP, you can keep the dashboard for local work and a bigger backend for production, using the exact same instrumentation in your code. That is the whole point of building on an open standard.

Quick recap

  • The standalone Aspire dashboard is a single Docker container that receives and shows telemetry. You do not need Aspire packages in your code.
  • It accepts OTLP from any app. Your .NET app uses the normal OpenTelemetry packages and an OTLP exporter.
  • Three ports matter: 18888 for the browser UI, 4317 for OTLP over gRPC, and 4318 for OTLP over HTTP.
  • The dashboard prints a login token to its logs by default. For local-only work you can set ASPIRE_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS to true.
  • Point your app with OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_EXPORTER_OTLP_PROTOCOL. Add AddOtlpExporter() for traces, metrics, and logs.
  • Distributed tracing joins requests across services into one waterfall using a shared trace id.
  • The dashboard stores data in memory, so it is great for development. Point OTLP at a durable backend for production without changing your code.

References and further reading

Related Posts