Skip to content

--isolated does not randomize AppHost dashboard / OTLP / resource-service ports — concurrent runs collide #17872

@joperezr

Description

@joperezr

Summary

aspire run --isolated does not randomize the AppHost-process ports (dashboard frontend, OTLP endpoint, resource service). Running two AppHosts in parallel — even with --isolated — fails because both bind the same fixed ports from the launch profile.

Repro

  1. Have an AppHost with a launch profile that pins the standard dashboard URLs. This is the default for both C# AppHosts (Properties/launchSettings.json) and TypeScript/polyglot AppHosts (aspire.config.json profiles.https.environmentVariables):

    "applicationUrl": "https://localhost:17236;http://localhost:15103",
    "environmentVariables": {
      "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL":   "https://localhost:21130",
      "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22064"
    }
  2. From two different worktrees of the same repo, run aspire run --isolated (or aspire start --isolated) concurrently.

  3. Second instance crashes:

    System.IO.IOException: Failed to bind to address https://127.0.0.1:22064: address already in use.
    Capability Aspire.Hosting/run failed with InvalidOperationException
    

Observed on Aspire CLI 13.4.0, Windows. Reproduces with both TS and C# AppHosts.

Expected

--isolated advertises "randomized ports and isolated user secrets, allowing multiple instances to run simultaneously." That should include the AppHost-process ports, not only DCP-allocated resource endpoints. Two --isolated runs of the same AppHost should coexist on the same machine without manual port juggling.

Why this happens

ConfigureIsolatedModeAsync in src/Aspire.Cli/Projects/DotNetAppHostProject.cs only does two things:

env["DcpPublisher__RandomizePorts"] = "true";   // randomizes DCP child-resource ports
env["DOTNET_USER_SECRETS_ID"] = isolatedUserSecretsId;

It never overrides the launch-profile env vars (ASPNETCORE_URLS, ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL, ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL), which are subsequently applied verbatim from the profile by the same class.

Inside the AppHost process, DashboardServiceHost.ConfigureKestrel already supports random ports — it binds port: 0 when no URL is configured — but the env var from the launch profile short-circuits that path:

var uri = configuration.GetUri(KnownConfigNames.ResourceServiceEndpointUrl) ?? ...;
if (uri is null) { kestrelOptions.Listen(IPAddress.Loopback, port: 0, ConfigureListen); }
else            { kestrelOptions.Listen(ip, uri.Port, ConfigureListen); }   // ← collides

Same logic governs the dashboard frontend (ASPNETCORE_URLS) and OTLP endpoint.

This is language-agnostic — the TS polyglot path and the C# path both end up in the same DashboardServiceHost running inside the AppHost process, both bound by the launch-profile URLs.

Suggested fix

In ConfigureIsolatedModeAsync (and the equivalent polyglot/single-file path), when isolated mode is on, either:

  • unset ASPNETCORE_URLS, ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL, ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL after the launch profile is applied, so the Kestrel port: 0 fallback kicks in, OR
  • replace them with :0 URIs that still drive the same code path but yield ephemeral ports.

The dashboard front-end URL would then need to be surfaced back to the CLI (it already is, via the backchannel for the dashboard login URL print-out) so the user still gets a clickable link.

Workaround today

Strip the four pinned URLs out of the launch profile (or maintain a per-worktree profile), so isolated mode actually has free ports to grab.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-app-modelIssues pertaining to the APIs in Aspire.Hosting, e.g. DistributedApplication‼️regression-from-last-releaseThis used to work in an earlier version of Aspire and we broke it!

    Type

    No fields configured for Bug.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions