# 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.

Source page: https://appointme.dev/docs/reference/local-services/

* **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](/docs/start/quick-start/).

## URLs

**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

**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.

src/AppointMe.Aspire/Program.cs

```cs
var sqlServer = builder
    .AddSqlServer("appointme-sql", passwordParameter)
    .WithImage("mssql/server:2025-CU10-ubuntu-24.04")
    .WithHostPort(60740)
    .WithDataVolume()
    .WithLifetime(ContainerLifetime.Persistent);
```

## 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

**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:

src/AppointMe.Api/appsettings.Development.json

```json
"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

**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

**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?

`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.

## Next

* [Quick start](/docs/start/quick-start/) (Tutorial)
* [Commands](/docs/reference/commands/) (Reference)
* [Troubleshooting](/docs/reference/troubleshooting/) (Reference)
