Local services & ports
Every container and process the AppointMe Aspire AppHost and compose.yaml start for local .NET 10 development, with images, tags, ports, credentials, volumes and URLs.
- Containers
- SQL Server, Keycloak, Mailpit
- Processes
- The API and the Vite dev server
- Defined in
AppointMe.Aspire/Program.cs,compose.yaml- Same in both
- Images, host ports, credentials
The AppHost in src/AppointMe.Aspire/Program.cs starts three containers and two processes. compose.yaml starts the same three containers on the same host ports, with the same images and credentials, and leaves the API and the frontend to you. This page lists what each one runs, where to reach it and where its data lives; to start them, see the Quick start.
The app is at https://localhost:5173. Everything else is a tool for you, not for users.
| Service | URL | Notes |
|---|---|---|
| API | https://localhost:7233, http://localhost:5179 |
The https launch profile. Aspire always uses it. With dotnet run, pass --launch-profile https. |
| Aspire dashboard | https://localhost:17082 |
Aspire only. Also http://localhost:15181, OTLP on https://localhost:21061, resource service on https://localhost:22014. |
| Frontend (the app) | https://localhost:5173 |
Vite+ dev server. Proxies /api, /signin-oidc and /admin to the API. |
| Hangfire dashboard | https://localhost:5173/admin/jobs |
Super admins only (Authentication:SuperAdmins) |
| Keycloak | https://localhost:8082 |
Admin console, and the appointme realm at /realms/appointme |
| Mailpit web UI | http://localhost:8026 |
Every email Keycloak sends |
| OpenAPI document | https://localhost:7233/openapi/v1.json |
Development only |
| SQL Server | localhost,60740 |
TDS, not HTTP |
Under Aspire, the API and frontend ports come from launch profiles and endpoint settings, not from Docker. The API’s ports are in src/AppointMe.Api/Properties/launchSettings.json, the dashboard’s in src/AppointMe.Aspire/Properties/launchSettings.json. The AppHost pins the frontend to 5173 because the Keycloak redirect URIs and Frontend:BaseUrl expect exactly that origin.
Containers
Section titled “Containers”Three images, with the same tags under Aspire and in compose.yaml. Dependabot doesn’t update container tags, so bump both by hand.
| Service | Image | Host port → container port | Aspire resource | Compose service / container |
|---|---|---|---|---|
| Keycloak | quay.io/keycloak/keycloak:26.6 |
8082 → 8443 under Compose |
keycloak |
keycloak / appointme-keycloak |
| Mailpit | axllent/mailpit:v1.32.0 |
8026 → 8025 (web UI), 1026 → 1025 (SMTP) |
Mailpit |
mailpit / appointme-mailpit |
| SQL Server | mcr.microsoft.com/mssql/server:2025-CU10-ubuntu-24.04 |
60740 → 1433 |
appointme-sql, database resource AppointMeSql |
sqlserver / appointme-sql |
Under Aspire, Keycloak’s image is the default of Aspire.Hosting.Keycloak 13.6.4-preview.1.26523.11, which is the same 26.6 tag; it moves when that package does. The AppHost names the host port 8082 and serves HTTPS on it when the dev certificate is trusted.
var sqlServer = builder .AddSqlServer("appointme-sql", passwordParameter) .WithImage("mssql/server:2025-CU10-ubuntu-24.04") .WithHostPort(60740) .WithDataVolume() .WithLifetime(ContainerLifetime.Persistent);Processes
Section titled “Processes”Aspire runs the API and the frontend as local processes, not containers. Under Compose you start both yourself.
| Process | Aspire resource | Started by | Reaches |
|---|---|---|---|
| API | appointme-api |
AddProject<AppointMe_Api>(..., launchProfileName: "https") |
SQL Server, Keycloak (OIDC and the admin API) |
| Frontend | appointme-frontend |
AddViteApp(...), which runs npm install --no-save and then the dev script |
The API, through the dev-server proxy |
The frontend serves TLS with vite-plugin-mkcert, which makes its own local certificate. The API and Keycloak use the ASP.NET Core dev certificate.
Credentials
Section titled “Credentials”Every value here is a committed local default. Never reuse one in a deployed environment.
| What | Value | Set in |
|---|---|---|
Demo user (realm appointme) |
demo@appointme.dev / AppointMe1 |
appointme-realm.json, and Demo:User in appsettings.Development.json |
| Keycloak admin (master realm) | admin / admin |
The username and password parameters in Program.cs; KC_BOOTSTRAP_ADMIN_USERNAME and KC_BOOTSTRAP_ADMIN_PASSWORD in compose.yaml |
Keycloak client appointme-api |
A client secret | appointme-realm.json, and KeycloakAdmin:ClientSecret |
Keycloak client appointme-frontend |
A public client; the API still sends Authentication:Keycloak:FrontendClientSecret |
appsettings.Development.json |
| SQL Server | sa / Password1 |
The sqlPassword parameter in Program.cs; MSSQL_SA_PASSWORD in compose.yaml |
The API’s connection string when it runs outside Aspire:
"ConnectionStrings": { "AppointMeSql": "Server=localhost,60740;Database=AppointMe;User ID=sa;Password=Password1;TrustServerCertificate=True"},Under Aspire, WithReference(database) injects its own AppointMeSql connection string to the same container and database. The API’s tables live in the schemas identity, organizations, crm, booking, wolverine and hangfire of the AppointMe database.
Persistence
Section titled “Persistence”SQL Server and Keycloak keep their data in named volumes. Mailpit keeps nothing once its container is gone.
| Service | Under Aspire | Under Compose | To reset |
|---|---|---|---|
| Keycloak | Volume AppointMe-Keycloak, persistent container |
Volume keycloak-data (appointme_keycloak-data) |
Remove the container and the volume. Needed to re-import an edited realm. |
| Mailpit | No volume, persistent container | No volume | Remove the container |
| SQL Server | A data volume named by Aspire (WithDataVolume()), persistent container |
Volume sqlserver-data (appointme_sqlserver-data) |
Remove the container and the volume. Migrations recreate the schema on the next start. |
Aspire’s persistent containers keep running after you stop the AppHost, and the next run reuses them. Under Compose, docker compose stop keeps everything, docker compose down removes the containers but keeps the volumes, and docker compose down -v removes both.
How the services find each other
Section titled “How the services find each other”Containers talk to each other by name. The API and the frontend get their targets from Aspire, or from committed config when Aspire isn’t running.
| From → to | Under Aspire | Under Compose |
|---|---|---|
| API → Keycloak | Authentication__Keycloak__Authority and KeycloakAdmin__BaseUrl, built from Keycloak’s endpoint |
https://localhost:8082 in appsettings.Development.json |
| API → SQL Server | The AppointMeSql connection string from WithReference(database) |
The connection string above |
| Frontend → API | services__appointme-api__https__0, which vite.config.ts prefers |
VITE_API_PROXY_TARGET=https://localhost:7233 in .env.development |
| Keycloak → Mailpit | Host Mailpit, port 1025 (the realm’s smtpServer) |
The same host, through the network alias Mailpit on the mailpit service |
Only Keycloak sends email. The API has no SMTP client, so Mailpit’s host port 1026 is free for your own tools.
Why do Aspire and Compose share every port?1 min · optional
appsettings.Development.json, .env.development and the realm’s redirect URIs all name fixed local addresses. Keeping the host ports identical means one committed config works under both, and you can switch between them without editing a file.
The cost is that you can’t run both at once: Aspire’s persistent containers and the Compose containers claim the same host ports, so stop one set before starting the other.