Skip to content
fullstackhero

Concept

Local Orchestration with .NET Aspire

What spins up when you run the AppHost, and the decisions behind the local topology — Postgres, Valkey, MinIO, the migrator, the API, and both React apps.

views 0 Last updated

One command brings up the entire stack — databases, cache, object storage, the API, and both React apps — wired together with service discovery, health gating, and a single dashboard:

Terminal window
dotnet run --project src/Host/FSH.Starter.AppHost

The composition lives in src/Host/FSH.Starter.AppHost/AppHost.cs. This page documents what it starts and why it’s wired the way it is.

What spins up by default

ResourceAspire nameWhat it isLifetime
PostgreSQLpostgresPrimary database server; hosts the fsh-db databasePersistent (data volume)
pgAdmin(sidecar)Web UI on :5050, auto-discovers every database on the serverPersistent
ValkeyredisCache + Hangfire store (Valkey — a Redis-compatible, BSD-licensed Redis fork; resource name stays redis)Persistent (data volume)
RedisInsight(sidecar)Key browser auto-connected to the Valkey instance for inspecting cache keys in devPersistent
MinIOminioS3-compatible object storage, :9000 (API) / :9001 (console)Persistent (data volume)
MinIO initminio-initOne-shot: creates the fsh-uploads bucket + download policy, then exitsRun-once
DB migratorfsh-db-migratorOne-shot: applies migrations across the tenant catalog + every tenant’s module DBs, then exitsRun-once
APIfsh-apiThe ASP.NET Core API (net10.0)Long-running
Admin appfsh-adminOperator React + Vite SPA on :5173Long-running
Dashboard appfsh-dashboardTenant React + Vite SPA on :5174 (with SSE live feed)Long-running

The Aspire dashboard opens automatically and shows every resource’s state, logs, traces, and endpoints. The API’s Scalar UI is at /scalar.

Design decisions

Persistent infra, run-once jobs

Postgres, Valkey, and MinIO use ContainerLifetime.Persistent with named data volumes. Your data, pgAdmin layout, and uploaded files survive dotnet run restarts, so the inner loop stays fast. The migrator and MinIO bootstrap are run-once — they do their job and exit rather than linger as “unhealthy.”

MinIO is S3, so dev matches prod

Object storage uses the same Storage__Provider = "s3" code path locally as in production — only ServiceUrl and ForcePathStyle differ. The Files module’s presigned-URL upload flow (browser → storage directly, bytes never proxied through the API) is exercised end-to-end in dev. MinIO is configured to accept browser PUTs from the admin (:5173) and dashboard (:5174) origins via MINIO_API_CORS_ALLOW_ORIGIN.

The migrator is the production deploy step too

fsh-db-migrator is the same FSH.Starter.DbMigrator project you run as an explicit step in production (dotnet run --project ... -- apply). The database is never migrated at API startup — locally or in the cloud. One mechanism, two contexts.

Valkey for cache + Hangfire, with a RedisInsight browser

The cache/store engine is Valkey (a Redis-compatible, BSD-licensed Redis fork), pulled as valkey/valkey:8-alpine. It speaks the Redis protocol (RESP), so the .NET client (StackExchange.Redis) and every CachingOptions:Redis config key are unchanged — and the Aspire resource keeps the name redis in code. Aspire also auto-wires a RedisInsight browser sidecar connected to the Valkey instance, so you can inspect cache keys, TTLs, and the SignalR backplane in dev with no manual configuration.

Redis over plain TCP

Frontends target the API’s HTTPS endpoint

Both React apps get VITE_API_BASE_URL pointing at the API’s https endpoint, not http. The API uses UseHttpsRedirection(), so a call to the http endpoint 307-redirects to https — and browsers strip the Authorization header on cross-origin redirects (a different scheme/port is cross-origin per the Fetch spec). Hitting https directly preserves the bearer token on every request. The Vite apps run un-proxied on fixed ports (5173 / 5174) so HMR works and the origins line up with the MinIO CORS allow-list.

The React apps are optional

The //#if (frontend) directives around the two AddJavaScriptApp(...) calls are dotnet new template conditionals. Scaffold with the backend-only option and the AppHost omits both React apps entirely.

Ports

ServicePort
API7030 (https) · 5030 (http)
Admin app5173
Dashboard app5174
Postgres5432
pgAdmin5050
Valkey6379
MinIO9000 (API) · 9001 (console)

From local to cloud

The local topology maps almost one-to-one onto AWS: Postgres → RDS, Valkey → ElastiCache, MinIO → S3, the API container → ECS Fargate, and the two React apps → S3 + CloudFront. See Deploy to AWS with Terraform.