# Troubleshooting

> Symptom, cause and fix for AppointMe problems in local .NET 10 development. Docker, dev certificates, Aspire, Keycloak, SQL Server, Mailpit, the Vite+ dev server, API client generation and 403s.

Source page: https://appointme.dev/docs/reference/troubleshooting/

Each row starts from what you see: an error message, a log line or a behavior. Messages in code font are quoted from the AppointMe source or from the tool that prints them (Docker, .NET, Keycloak, SQL Server or Vite), and rows are sorted alphabetically within each area. For the normal setup, start with the [Quick start](/docs/start/quick-start/); for ports and volumes, see [Local services & ports](/docs/reference/local-services/).

## Docker

**Aspire and Compose share host ports, and their data lives in named volumes. Most Docker problems come from one of those two facts.**

| Symptom                                                                                | Cause                                                                                                                                  | Fix                                                                                                                                                                                                       |
| -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Bind for 0.0.0.0:60740 failed: port is already allocated` (or `8082`, `8026`, `1026`) | Aspire’s persistent containers and the Compose containers claim the same host ports. Aspire’s keep running after you stop the AppHost. | Stop one set: `docker compose stop`, or stop the Aspire containers in Docker. `docker ps` shows which holds the port.                                                                                     |
| Containers never start, or `docker compose` can’t connect to the Docker daemon         | Docker isn’t running                                                                                                                   | Start Docker and check with `docker info`                                                                                                                                                                 |
| Data is still there after `docker compose down`                                        | `down` keeps the `sqlserver-data` and `keycloak-data` volumes                                                                          | `docker compose down -v` removes them too                                                                                                                                                                 |
| Keycloak never turns `healthy` under Compose                                           | `docker/keycloak/certs/keycloak.crt` or `keycloak.key` is missing, and `compose.yaml` mounts both                                      | Run `./docker/keycloak/export-dev-cert.sh`, then `docker compose up -d --force-recreate keycloak`. If either path exists as an empty folder, Docker created it on the first `up`: delete it, then export. |

## Certificates and HTTPS

**The API and Keycloak use the ASP.NET Core dev certificate. The AppHost checks it before it starts anything.**

| Symptom                                                                                                                      | Cause                                                                                                                             | Fix                                                                                                                                                                             |
| ---------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| The AppHost exits with `error: the ASP.NET Core HTTPS development certificate is missing or not trusted.`                    | `dotnet dev-certs https --check --trust` failed on Windows or macOS                                                               | `dotnet dev-certs https --trust`. If a certificate exists but is still untrusted, run `dotnet dev-certs https --clean` first.                                                   |
| Keycloak under Compose serves a certificate the browser or the API rejects                                                   | The dev certificate isn’t trusted, or you regenerated it (for example with `--clean`) after exporting, so `keycloak.crt` is stale | Run `./docker/keycloak/export-dev-cert.sh` again, which trusts and then exports, then `docker compose up -d --force-recreate keycloak`                                          |
| Login fails with an unexplained `500`                                                                                        | The certificate wasn’t trusted, so Aspire served Keycloak over plain HTTP. On Linux the AppHost only warns about this.            | Trust the certificate, then restart the AppHost                                                                                                                                 |
| `warning: the HTTPS development certificate is missing or not trusted. Run 'dotnet dev-certs https --trust' if HTTPS fails.` | Linux has no single trust store the tool can check, so the AppHost warns and carries on                                           | If HTTPS then fails, trust the certificate the way your distribution needs: see [Enforce HTTPS in ASP.NET Core](https://learn.microsoft.com/aspnet/core/security/enforcing-ssl) |

## Build and startup

**Aspire starts resources in dependency order. The API reads its required settings at startup and stops with a named error if one is missing.**

| Symptom                                                                        | Cause                                                                                                                                                    | Fix                                                                       |
| ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
| `appointme-api` stays in **Waiting**                                           | It waits for SQL Server, Keycloak and Mailpit. The first run also pulls three images and imports the realm.                                              | Wait, or open each container’s logs in the Aspire dashboard               |
| `appointme-frontend` fails to start                                            | Node.js is older than `22.18`, which Vite+ 1.4 requires                                                                                                  | Install Node 24, the version in `src/AppointMe.Frontend/.node-version`    |
| The Aspire dashboard asks for a token                                          | You opened `https://localhost:17082` without the token the AppHost generated                                                                             | Use the dashboard URL the AppHost prints in its console                   |
| The generated solution doesn’t compile                                         | `dotnet new appointme -n` got a name that isn’t a dotted identifier, such as `my-booking-app`. `dotnet new` still reports success.                       | Generate again with a name like `Contoso.Booking`                         |
| `Section 'Authentication:Keycloak' not found in configuration.`                | The API started outside the Development environment, for example with `--no-launch-profile`. That section exists only in `appsettings.Development.json`. | Use `--launch-profile https`, or set `ASPNETCORE_ENVIRONMENT=Development` |
| `Unknown Wolverine:Transport '…'. Expected 'SqlDurable' or 'AzureServiceBus'.` | A typo in `Wolverine:Transport`                                                                                                                          | Use one of the two values                                                 |

## Keycloak and Mailpit

**Keycloak imports its realm once and keeps it in a volume. Its email goes to Mailpit, whose web UI is on host port 8026.**

### Keycloak

| Symptom                                           | Cause                                                                                                                                                 | Fix                                                                                                                                                              |
| ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| A new `KC_BOOTSTRAP_ADMIN_PASSWORD` has no effect | Keycloak creates the bootstrap admin only on the first start of an empty database                                                                     | Change the password in the admin console, or remove the Keycloak volume                                                                                          |
| Changes to `appointme-realm.json` don’t show up   | The realm already exists in Keycloak’s data volume, so the import skips it                                                                            | Remove the Keycloak container and its volume (`AppointMe-Keycloak` under Aspire, `appointme_keycloak-data` under Compose). This also deletes local users.        |
| “Continue with Google” fails                      | The realm’s `google` identity provider has an empty client secret                                                                                     | Configure your own Google OAuth client in the realm, or remove the provider and the button                                                                       |
| `Invalid parameter: redirect_uri`                 | The sign-in started from an origin the realm doesn’t list. Its clients allow redirects only to `https://localhost:5173` and `https://localhost:7233`. | Open the app on `https://localhost:5173` and run the API with `--launch-profile https`. To use other ports, add them to the clients’ redirect URIs in the realm. |

### Mailpit

| Symptom                       | Cause                                                                                                 | Fix                                                                                                     |
| ----------------------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| Emails are gone               | Mailpit has no volume, so recreating its container empties it                                         | Expected. Sign up again to get a new email.                                                             |
| No verification email arrives | You opened port 8025, which is Mailpit’s container port, or Keycloak can’t resolve the `Mailpit` host | Open `http://localhost:8026`. Under Compose, keep the `Mailpit` network alias on the `mailpit` service. |

## SQL Server

**SQL Server listens on host port 60740, and each module keeps its tables in its own schema.**

| Symptom                                                                                                | Cause                                                                       | Fix                                                                                                                                   |
| ------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `A network-related or instance-specific error occurred while establishing a connection to SQL Server.` | Nothing is listening on `localhost,60740`                                   | Under Compose, check `docker compose ps` for a `healthy` `sqlserver`. Under Aspire, the API waits for it.                             |
| A SQL client rejects the server’s certificate                                                          | The container uses a self-signed certificate                                | Turn on “Trust server certificate”, as the connection string does with `TrustServerCertificate=True`                                  |
| `Login failed for user 'sa'.` after you changed the password                                           | SQL Server sets the `sa` password when the data volume is first initialized | Change it back, or remove the SQL Server volume. Migrations recreate the schema.                                                      |
| Module tables aren’t in `dbo`                                                                          | Each module has its own schema                                              | Look in `identity`, `organizations`, `crm`, `booking` and `hangfire`. Wolverine’s `wolverine_*` tables *are* in `dbo` at this commit. |

## Frontend

**The dev server proxies the API, and CI enforces Oxfmt formatting. Generated client files are never edited by hand.**

### Dev server

| Symptom                                                  | Cause                                                                                                                                                | Fix                                                                                                          |
| -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| An editor formats files differently from CI              | The editor still uses Prettier or ESLint. There’s no `.prettierrc` or `eslint.config.js`; the rules live in `vite.config.ts`.                        | Use the Oxc plugin (JetBrains) or the `VoidZero.vite-plus-extension-pack` (VS Code), or run `npm run format` |
| `npm install` rewrote `package-lock.json`                | Your npm differs from the npm 11 that wrote the lockfile                                                                                             | Use `npm ci`, and discard the lockfile change                                                                |
| `npm run check` fails in CI                              | Oxfmt formatting is enforced, as are Oxlint’s correctness rules and the type-check                                                                   | Run `npm run check` locally, and `npm run format` for the formatting part                                    |
| The Vite terminal logs `http proxy error` for `/api/...` | The API isn’t reachable at the proxy target. Under Compose that’s `https://localhost:7233`, which the API serves only with `--launch-profile https`. | Start the API with `--launch-profile https`. Under Aspire, wait for `appointme-api` to run.                  |

### API client generation

| Symptom                                                                   | Cause                                                                                                                           | Fix                                                                 |
| ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
| Hand edits to `src/api/client.ts` disappear                               | orval runs with `clean: true` and rewrites `src/api/` on every run                                                              | Change the backend contract or `orval.config.ts` instead            |
| No diff in `src/api/` after a contract change                             | The change doesn’t show in the OpenAPI document                                                                                 | Inspect `src/AppointMe.Api/obj/openapi.json`                        |
| `npm run generate:api` fails in the `GenerateOpenApiDocuments` build step | The build runs the app’s startup in the `Codegen` environment, with no database, and new startup code needs real infrastructure | Gate that code on `CodeGenerationDetection.IsRunningGeneration()`   |
| On Windows, the build fails on locked DLLs                                | A Debug build collides with the API that Aspire runs from `bin/Debug`                                                           | Use `npm run generate:api`, which builds in Release for this reason |
| The client doesn’t match your latest backend change                       | `npx orval` read the last build’s `obj/openapi.json`                                                                            | Run `npm run generate:api`, which builds first                      |

## Login and 403s

**A `403` from a handler names its reason in the ProblemDetails `detail`. The last three rows quote those reasons.**

| Symptom                                                               | Cause                                                                                                                               | Fix                                                                                                                              |
| --------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `/admin/jobs` redirects you instead of showing the Hangfire dashboard | You aren’t signed in, or your email isn’t in `Authentication:SuperAdmins`                                                           | Sign in, and add your email to that list. Locally, `demo@appointme.dev` is listed.                                               |
| `/api/v1/login/demo` returns `404`                                    | `Demo:Enabled` is `false`, or `Demo:User` is missing                                                                                | Set both, as `appsettings.Development.json` does                                                                                 |
| `/api/v1/login/demo` returns `502` with `Demo login failed.`          | Keycloak rejected the password grant for the demo user. The log says `Keycloak demo login (ROPC) failed:` with the status and body. | Check that the realm user and `Demo:User` agree, and that the client allows direct access grants                                 |
| `403` `Active company was not specified.`                             | No `X-Company-Id` header, or a background handler ran without a tenant                                                              | See the [multi-tenancy gotchas](/docs/concepts/multi-tenancy/#gotchas)                                                           |
| `403` `The following permissions are required: …`                     | The user’s roles in this company don’t grant the permission                                                                         | Grant it in **Settings → Permissions**, or in the module’s default grant policy. See [Permissions](/docs/concepts/permissions/). |
| `403` `User is not authorized to access requested company.`           | The user isn’t an employee of the company in the header                                                                             | Pick one of the user’s own companies. See the [multi-tenancy gotchas](/docs/concepts/multi-tenancy/#gotchas).                    |

## Next

* [Local services & ports](/docs/reference/local-services/) (Reference)
* [Commands](/docs/reference/commands/) (Reference)
* [Multi-tenancy](/docs/concepts/multi-tenancy/) (Concept)
