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.
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; for ports and volumes, see Local services & ports.
Docker
Section titled “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
Section titled “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 |
Build and startup
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “Frontend”The dev server proxies the API, and CI enforces Oxfmt formatting. Generated client files are never edited by hand.
Dev server
Section titled “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
Section titled “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
Section titled “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 |
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. |
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. |