Skip to content

Running Locally

The recommended way to run Cargonerds locally is through the .NET Aspire AppHost (src/Cargonerds.AppHost). It is a single orchestrator process that provisions the supporting infrastructure (SQL Server, Redis, RabbitMQ) as Docker containers, runs the database migrator, starts every backend service and the Next.js frontend, wires service discovery and environment variables between them, and opens a web dashboard where you watch and control the whole graph.

This page assumes you are already set up

If you have not cloned the repo, installed the prerequisites, run abp install-libs, or generated the OpenIddict certificate yet, do the one-time steps in the Quick Start and Prerequisites first. This page goes deeper on the day-to-day run experience: the dashboard, ports, containers, hot reload and troubleshooting.

Run with .NET Aspire

From the repository root:

dotnet run --project src/Cargonerds.AppHost

In an IDE, set Cargonerds.AppHost as the startup project and press F5 (Visual Studio 2022 / JetBrains Rider). This builds the resource graph declared in src/Cargonerds.AppHost/Program.cs, calls builder.Build().EnsureDockerRunningIfLocalDebug().Run(), and launches your browser at the dashboard.

That single command:

  • Provisions SQL Server (container spark-db), Redis (container spark-redis, plus spark-redis-insight and spark-redis-commander) and RabbitMQ (container spark-rabbitmq, management plugin enabled) as persistent Docker containers.
  • Runs the db-migrator resource (Cargonerds.DbMigrator) to apply EF Core migrations and seed data, then waits for it to complete before starting the dependent services.
  • Starts auth (Cargonerds.AuthServer), api (Cargonerds.HttpApi.Host) and admin (Cargonerds.Blazor).
  • Discovers and starts every frontend under frontend/ that has a Dockerfile (currently the realtime Next.js app) via AddAllFrontends(), injecting APP_URL, API_URL, AuthServer__Authority, AUTH_CLIENT_ID and the MapTiler key through WithNextAuth.
  • Registers this documentation site, built from a Dockerfile. It is not started automatically — it has WithExplicitStart(), so you start it on demand from the dashboard.
  • Opens the Aspire dashboard, which lists the live URLs, health status, structured logs, distributed traces and metrics for every resource.

Docker must be running

Program.cs ends with EnsureDockerRunningIfLocalDebug() (a Nextended.Aspire helper). If Docker Desktop is not running, the AppHost fails fast with a clear message instead of timing out container-by-container. Start Docker Desktop before you run.

What gets started (resource graph)

graph TD
    subgraph Infra["Docker containers (persistent)"]
        DB[("spark-db<br/>SQL Server")]
        REDIS[("spark-redis<br/>Redis")]
        MQ[("spark-rabbitmq<br/>RabbitMQ")]
    end

    MIG["db-migrator<br/>Cargonerds.DbMigrator"]
    AUTH["auth<br/>Cargonerds.AuthServer"]
    API["api<br/>Cargonerds.HttpApi.Host"]
    ADMIN["admin<br/>Cargonerds.Blazor"]
    RT["realtime<br/>frontend/realtime (Next.js)"]
    DOCS["documentation<br/>(explicit start)"]

    DB --> MIG
    MIG -. WaitForCompletion .-> AUTH
    MIG -. WaitForCompletion .-> API
    DB --> AUTH
    REDIS --> AUTH
    MQ --> AUTH
    DB --> API
    REDIS --> API
    MQ --> API
    AUTH --> API
    AUTH --> ADMIN
    API --> ADMIN
    API --> RT
    AUTH --> RT

The wait/health edges (dotted) are conditional — see Speeding up local startup.

The Aspire dashboard

When the AppHost starts, it opens the dashboard at the URL from src/Cargonerds.AppHost/Properties/launchSettings.json:

Launch profile Dashboard URL
https (default) https://localhost:17176
http http://localhost:15198

The same launchSettings.json also pins the dashboard's OTLP ingestion endpoint (DOTNET_DASHBOARD_OTLP_ENDPOINT_URL = https://localhost:21236) and the resource-service endpoint, so the telemetry ports are stable across runs.

From the dashboard you get, per resource:

  • Resources view — live state (Running / Starting / Waiting / Exited), the assigned endpoint URLs, environment variables, and the source project/container.
  • Console logs — raw stdout/stderr of each process or container.
  • Structured logs, Traces and Metrics — OpenTelemetry data emitted by every .NET host via AddServiceDefaults() (see Aspire Integration).
  • Commands — per-resource actions. Notably each frontend gets a "Generate Proxies" command (see Frontend / hot reload), and resources started with WithExplicitStart() (the documentation site) get a Start button.

Always read URLs from the dashboard

Endpoints are assigned by Aspire and surfaced in the dashboard. Prefer the dashboard's links over hard-coded ports — the run-mode ports below are the exception, not the rule.

Ports

Most endpoints are dynamic, but the .NET service hosts and the realtime frontend use fixed local ports in run mode, defined in src/Cargonerds.AppHost/RunModeServicePorts.cs:

Aspire resource Project / app Local URL Source
auth Cargonerds.AuthServer https://localhost:44345 RunModeServicePorts.AuthServerHttpsPort
api Cargonerds.HttpApi.Host https://localhost:44354 RunModeServicePorts.ApiHttpsPort
admin Cargonerds.Blazor https://localhost:44381 RunModeServicePorts.AdminHttpsPort
realtime frontend/realtime (Next.js) http://localhost:4200 RunModeServicePorts.RealtimeHttpPort
db-migrator Cargonerds.DbMigrator console (runs to completion)
documentation this MkDocs site http://localhost:8001 (explicit start) Program.cs

The fixed HTTPS ports are applied only in run mode — Program.cs wraps each WithEndpoint("https", …) in IfRunMode(...). For example the API:

apiHost
    .IfRunMode(b =>
        b.WithEndpoint("https", endpoint => endpoint.Port = RunModeServicePorts.ApiHttpsPort)
            .If(!skipWaitingInBackend, _ => b.WithHttpHealthCheck("/api/abp/application-configuration", 200))
    )
    // …

The realtime app's port comes from a small map in src/Cargonerds.AppHost/Extensions/DistributedAppBuilderExtensions.cs (FrontendPorts["realtime"] = RunModeServicePorts.RealtimeHttpPort); other frontends would get a random port. Internally, RunModeServicePorts also defines DbPort = 53938, but the SQL container's TCP listener is pinned separately (see below).

Fixed ports must be free

44345, 44354, 44381 and 4200 are hard-coded. If another process already holds one of them, startup fails. The SQL container's TCP endpoint is pinned to 14330 and is un-proxied (IsProxied = false), so a stray local SQL Server on that port will also conflict.

Containers

The infrastructure resources are declared in Program.cs with persistent lifetimes and named data volumes, so your data survives restarts of the AppHost.

SQL Server (spark-db)

Unless you opt into the Azure-Spark-DB mode (below), the AppHost runs SQL Server locally:

builder
    .AddAzureSqlServer("sqlserver")
    .RunAsContainer(s =>
    {
        s.WithEndpoint("tcp", endpoint => { endpoint.Port = 14330; endpoint.IsProxied = false; })
            .WithDataVolume("spark-db")
            .WithContainerName("spark-db")
            .WithLifetime(ContainerLifetime.Persistent)
            .WithPassword(dbPassword);     // builder.AddParameter("dbPassword", "securePassword-2026!")
    })
    .AddDatabase("db")
    .WithDefaultAzureSku();

Redis (spark-redis)

Redis is added via AddAzureRedis(...).RunAsContainer(...) with RedisInsight and Redis Commander sidecars (both exposed as external HTTP endpoints), a spark-redis data volume and the shared internal-service password.

AddAzureRedis is obsolete on purpose

The Redis block is wrapped in #pragma warning disable CS0618 with the comment "Switching to AzureManagedRedis increases costs". Do not "fix" the warning.

RabbitMQ (spark-rabbitmq)

var rabbitmq = builder
    .AddRabbitMQ(CargonerdsConsts.Aspire.Service.Messaging.Name, password: unsecurePassword)
    .WithManagementPlugin()
    .WithExternalHttpEndpoints()
    .WithLifetime(ContainerLifetime.Persistent)
    .IfRunMode(c => c.WithContainerName("spark-rabbitmq"))
    .PublishAsContainer();

The management UI is reachable from the RabbitMQ resource's external HTTP endpoint in the dashboard. The shared password (unsecurePassword) is the constant CargonerdsConsts.InternalServicePassword ("UnsecurePassword"), used for both Redis and RabbitMQ.

Inspecting and resetting containers

Containers appear in docker ps under their spark-* names and in the dashboard. Because they are ContainerLifetime.Persistent with named volumes, stopping the AppHost leaves them in place. To wipe local state (fresh DB / cache / queues), remove the containers and their volumes, e.g. docker rm -f spark-db spark-redis spark-rabbitmq followed by docker volume rm spark-db spark-redis. The migrator will re-create and re-seed the database on the next run.

How services find each other

You rarely need to configure URLs by hand because the AppHost wires service discovery for you. For every project it sets an environment variable per target:

// ResourceBuilderExtensions.WithServiceReference
var configPath = $"services__{target.Resource.Name}__https__0";
// run mode → target.GetFirstExistingEndpoint(); publish mode → https://{name}.{DEPLOYMENT_DOMAIN}

Each .NET host is also launched with USE_ASPIRE_CONFIG=true (set by AddProjectWithDefaults), which makes it load its committed appsettings.aspire.json. That file is the "consumer" side: it contains {token} placeholders such as {api}, {auth}, {realtime}, {redis} and {messaging} that are resolved at startup from the services__* variables (or connection strings). The full mechanism — token resolution order, the two distinct token engines, and the fail-fast behaviour when a token is unwired — is documented in Aspire Integration and the Configuration Reference. ABP's own take on this is the .NET Aspire integration guide.

Database migration and seeding

The db-migrator resource (Cargonerds.DbMigrator, an ABP migrator running DbMigratorHostedService) is gated by the database and gates the servers:

  • migrator.WaitFor(db).WithReference(db, "Default") — waits for SQL Server, gets the Default connection string.
  • authServer / apiHost call WaitForCompletionIf(!skipWaitingInBackend, migrator) — they do not start until migrations finish (unless you skip waiting; see below).

You do not run the migrator yourself when using Aspire. It applies pending EF Core migrations and seeds initial data, including the admin user. For the standalone/infra-only workflow you do run it once (see Infrastructure only (without Aspire)). Details of the migrator, the --enable-api /migration/status endpoint, and VALIDATE_MIGRATIONS_ONLY live in Migrations.

Default admin credentials

Seeded by the migrator from CargonerdsConsts (Cargonerds.Domain.Shared):

Field Value
Username admin
Email admin@cargonerds.com
Password 1q2w3E*

Sign in via the AuthServer from either the admin (Blazor) app or the realtime Next.js app.

Running the frontend and hot reload

The realtime frontend is not a separate manual step under Aspire — AddAllFrontends() discovers it and runs it as a managed resource. AddAllNpmAppsInPath enumerates frontend/*, keeps the folders that contain a Dockerfile (today only realtime; ui-library is a workspace dependency, not a hosted app), and for each registers a JavaScriptApp with the npm start script.

Which npm script runs

The start script is chosen as the first script whose name contains "aspire" or "start", falling back to "start":

string startScript =
    scriptNames?.FirstOrDefault(n => n.Contains("aspire") || n.Contains("start")) ?? "start";

For frontend/realtime that resolves to aspire-dev, defined in package.json as:

"aspire-dev": "npm i --force && next dev --turbopack --hostname 0.0.0.0"

So under Aspire the realtime app runs Next.js dev with Turbopack — i.e. Fast Refresh / hot reload is on. Edit a file under frontend/realtime/src and the browser updates without a manual rebuild. The resource is registered with WithEnvironment("BROWSER", "none") (no extra browser tab), a / HTTP health check, the OTLP exporter, and in run mode WithEnvironment("NODE_TLS_REJECT_UNAUTHORIZED", "0") so it can call the local HTTPS API with the dev certificate.

Frontend environment variables

WithNextAuth injects the values the Next.js app's OIDC setup (react-oidc-context / oidc-client-ts) needs, derived from the live Aspire endpoints and the white-label options:

Env var Source
APP_URL the frontend's own HTTPS endpoint
API_URL the api resource endpoint
AuthServer__Authority the auth resource endpoint
AUTH_CLIENT_ID the frontend resource name (e.g. realtime)
MAPTILER_API_KEY WhiteLabelingOptions.ExternalApis.MapTiler.ApiKey (from appsettings.rohlig.json)

For what the frontend does with these, see Realtime Frontend.

.NET ↔ frontend hot reload

The .NET hosts run as normal dotnet processes under Aspire. For C# hot reload, use your IDE's Hot Reload (Visual Studio / Rider) when debugging the individual host process, or run a single host standalone (see Debugging). The Blazor admin app also supports WebAssembly debugging via the inspectUri in its launchSettings.json.

Regenerating API client proxies

When you change an ABP application service signature, the frontend's typed client must be regenerated. Each frontend exposes a "Generate Proxies" dashboard command (WithGenerateProxyCommandCommand) that runs ABP's dynamic client proxy generator against frontend/:

// CommandHelper
string command = "abp generate-proxy -t ng";   // run from the frontend directory

You can also run abp generate-proxy -t ng yourself from the frontend folder.

Speeding up local startup

Iterating on one service is faster if you skip the inter-service wait/health gating. The AppHost reads two environment variables (see src/Cargonerds.AppHost/EnvVars.cs). Both are checked for the literal value "1":

Env var Effect
SKIP_WAITING_IN_BACKEND Don't wait for the migrator/auth/health checks before starting dependents (WaitForIf / WaitForCompletionIf are skipped, and the API's readiness health check is not added).
EXPLICIT_FRONTEND_START Frontends are registered with WithExplicitStartIf, so they require a manual Start click in the dashboard instead of starting automatically.
$env:SKIP_WAITING_IN_BACKEND = "1"
dotnet run --project src/Cargonerds.AppHost

Skipping waits can surface transient errors

With SKIP_WAITING_IN_BACKEND=1 the API/Auth servers may briefly start before migrations finish or before Redis/RabbitMQ are ready, producing first-request errors that resolve once dependencies catch up. Use it for fast iteration, not as a default.

SPARK environment selection

The backend selects which Hub data source to talk to (Hub DB, blob storage, service bus) via the SPARK_ENVIRONMENT variable. Valid values come from SparkEnvironment (modules/hub/src/Hub.Domain.Shared/Consts/SparkEnvironment.cs): local, dev, test, prod, prod-read-only. The default is dev (FromName(null) → Dev).

The value picks appsettings.spark.<env>.json (and an optional, git-ignored appsettings.spark.<env>.user.json) layered into config by AddServiceDefaults(). The AppHost forwards its own SPARK_ENVIRONMENT to the API via PassConfigurationValue(EnvVars.SparkEnvironmentName).

For local Hub development against a local Hub DB, set SPARK_ENVIRONMENT=local and create src/Cargonerds.HttpApi.Host/appsettings.spark.local.user.json with your Hub connection details.

A typo in SPARK_ENVIRONMENT crashes startup

SparkEnvironment.FromName returns Dev for null/empty but throws on an unknown name — so SPARK_ENVIRONMENT=develop fails fast rather than silently defaulting. Note also that SPARK_ENVIRONMENT (Hub DB) and AZURE_ENVIRONMENT (the Default/Spark DB and the Services block) are independent knobs. See the Configuration Reference.

Infrastructure only (without Aspire)

If you prefer to run a single .NET host from your IDE while still using containerised infrastructure, start just the dependencies from etc/docker:

pwsh etc/docker/up.ps1     # start Redis and RabbitMQ on a 'cargonerds' docker network
pwsh etc/docker/down.ps1   # stop them

This is a different container set from the Aspire one

etc/docker is a plain docker-compose setup, separate from the spark-* containers the AppHost creates. It uses standard host ports — Redis 6379, RabbitMQ 5672 (AMQP) and 15672 (management) — and starts only Redis and RabbitMQ (up.ps1 brings up containers/redis.yml and containers/rabbitmq.yml). It does not start SQL Server; provide a Default connection string yourself (e.g. LocalDB, the default in the hosts' appsettings.json). See etc/docker/README.md.

When running a host this way you must replicate what the AppHost does for you:

  1. Apply migrations once: dotnet run --project src/Cargonerds.DbMigrator.
  2. Start the individual host(s) you need (Cargonerds.AuthServer, Cargonerds.HttpApi.Host, Cargonerds.Blazor, …).

USE_ASPIRE_CONFIG is not set in standalone runs

Running a host directly does not set USE_ASPIRE_CONFIG, so it uses its static appsettings.json instead of appsettings.aspire.json (per EnvVars.UseAspireConfig: "If not set the default ABP configuration will be used which is not compatible with the Aspire AppHost."). Configure connection strings and AuthServer:Authority for the local/deployed targets accordingly. For stepping through a single service against a deployed environment's neighbours, see the recipe in Debugging.

Troubleshooting

Symptom Likely cause / fix
AppHost exits immediately with a Docker message Docker Desktop isn't running. EnsureDockerRunningIfLocalDebug() aborts early — start Docker and retry.
Startup fails: address/port already in use One of the fixed ports 44345 / 44354 / 44381 / 4200 (or SQL 14330) is taken. Stop the other process, or edit RunModeServicePorts.cs.
Child host throws InvalidOperationException listing "Available: …" services at startup A {token} in an appsettings.aspire.json value has no matching services__*/connection string — the token isn't wired in Program.cs. See Aspire Integration.
api/auth show first-request errors right after start They started before the migrator/Redis/RabbitMQ were ready — usually only with SKIP_WAITING_IN_BACKEND=1. Wait, or run without skipping.
App won't start: bad SPARK_ENVIRONMENT SparkEnvironment.FromName threw on an unknown name. Use one of local, dev, test, prod, prod-read-only.
TLS errors from the frontend calling the API The local dev certificate isn't trusted. Run dotnet dev-certs https --clean then --trust, and regenerate openiddict.pfx (see Quick Start).
Stale DB / cache / queue data Remove the persistent containers and volumes (docker rm -f spark-db spark-redis spark-rabbitmq; docker volume rm spark-db spark-redis) and re-run — the migrator re-seeds.
documentation resource never comes up It is intentionally WithExplicitStart() — click Start on it in the dashboard.
Service Bus / external integrations silent locally Expected: with SPARK_ENVIRONMENT=local the hubServiceBus connection string is empty, so the consumer logs a warning and does nothing. See Messaging.

For the guided first-run walkthrough and setup-time issues, see the Quick Start Guide. For debugging individual hosts (full stack, single service, Blazor WASM, Next.js), see Debugging.