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:
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 (containerspark-redis, plusspark-redis-insightandspark-redis-commander) and RabbitMQ (containerspark-rabbitmq, management plugin enabled) as persistent Docker containers. - Runs the
db-migratorresource (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) andadmin(Cargonerds.Blazor). - Discovers and starts every frontend under
frontend/that has aDockerfile(currently therealtimeNext.js app) viaAddAllFrontends(), injectingAPP_URL,API_URL,AuthServer__Authority,AUTH_CLIENT_IDand the MapTiler key throughWithNextAuth. - Registers this
documentationsite, built from aDockerfile. It is not started automatically — it hasWithExplicitStart(), 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()(thedocumentationsite) 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 theDefaultconnection string.authServer/apiHostcallWaitForCompletionIf(!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 |
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:
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/:
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. |
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:
- Apply migrations once:
dotnet run --project src/Cargonerds.DbMigrator. - 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.
Related pages¶
- Quick Start — one-time setup and first run.
- Prerequisites — required tooling and versions.
- Aspire Integration — orchestration internals,
ServiceDefaults, service discovery, token resolution. - Debugging — debug setups and standalone hosts.
- Configuration Reference — config layering,
USE_ASPIRE_CONFIG,SPARK_ENVIRONMENTvsAZURE_ENVIRONMENT, token engines. - Migrations — the DbMigrator, gating and
/migration/status. - Realtime Frontend — the Next.js app and its env vars.
- Architecture Overview — where the hosts and modules fit.