From 44b68b5e09931eb1fadd3cf3705fc8a750d6ed18 Mon Sep 17 00:00:00 2001 From: Stephen Walsh <868017+stphnwlsh@users.noreply.github.com> Date: Fri, 6 Nov 2026 23:14:45 +0000 Subject: [PATCH 1/3] Give each template its own block of local ports Every template bound 8080, 4317, 4318 and 18888 on the host, so two stacks couldn't run side by side. Each template now publishes everything in its own block of ten (Minimal API 8710, MCP 8720, GraphQL 8730, Web API 8740, Lambda Function 8750), with the same offsets in every block. Inside the containers the app stays on 8080. --- .agents/skills/docker-and-compose/SKILL.md | 25 +++++++++++++--- .agents/skills/mcp-inspector/SKILL.md | 14 ++++----- .agents/skills/serilog/SKILL.md | 4 +-- .agents/skills/telemetry/SKILL.md | 6 ++-- AGENTS.md | 2 +- NUGET.md | 2 +- README.md | 8 ++--- docker-compose.yml | 15 ++++++---- docs/RUN.md | 30 ++++++++++++++----- .../Properties/launchSettings.json | 2 +- 10 files changed, 72 insertions(+), 36 deletions(-) diff --git a/.agents/skills/docker-and-compose/SKILL.md b/.agents/skills/docker-and-compose/SKILL.md index 93a892f..2e274d8 100644 --- a/.agents/skills/docker-and-compose/SKILL.md +++ b/.agents/skills/docker-and-compose/SKILL.md @@ -19,9 +19,26 @@ One multi-stage `Dockerfile` does everything the pipeline needs: | `publish` | Publishes the Presentation project | | `run` | The runtime image: non-root `$APP_UID`, `WORKDIR /app`, data in `/app/data`, a `HEALTHCHECK` on `/health/live` | -`docker-compose.yml` runs the app with `DOTNET_ENVIRONMENT=docker`, the `movie-reviews-data` volume a pinned OpenTelemetry Collector that receives the app's OTLP logs, traces and metrics, configured by `otel-collector.yaml`, and a pinned Aspire dashboard the collector forwards them to (`telemetry`). The dashboard UI is at , and the collector's OTLP ports are published on 4317 and 4318 so a `dotnet run` can send to it too. The dashboard runs with `ASPIRE_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS` and the collector's OTLP ports take anything sent to them, on purpose: the compose file is for a developer's machine, with nothing to sign in to. Keep it that way rather than adding sign-in, and don't offer the compose file for a shared or deployed environment. Images are built by CI but not pushed anywhere; the template ships the Dockerfile so users can. +`docker-compose.yml` runs the app with `DOTNET_ENVIRONMENT=docker`, the `movie-reviews-data` volume a pinned OpenTelemetry Collector that receives the app's OTLP logs, traces and metrics, configured by `otel-collector.yaml`, and a pinned Aspire dashboard the collector forwards them to (`telemetry`). The dashboard UI and the collector's OTLP ports are published on the template's own ports (below), so a `dotnet run` can send to the collector too. The dashboard runs with `ASPIRE_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS` and the collector's OTLP ports take anything sent to them, on purpose: the compose file is for a developer's machine, with nothing to sign in to. Keep it that way rather than adding sign-in, and don't offer the compose file for a shared or deployed environment. Images are built by CI but not pushed anywhere; the template ships the Dockerfile so users can. -The Lambda template's `run` stage is the AWS Lambda .NET base image instead. It runs as user `65534` from `/var/task`, keeps its database in `/tmp`, and has no `HEALTHCHECK`. Its compose file runs the collector and the Aspire dashboard as well. Compose maps the runtime interface emulator to port 9000, so the function can be invoked with a `POST` to `/2015-03-31/functions/function/invocations`. +The Lambda template's `run` stage is the AWS Lambda .NET base image instead. It runs as user `65534` from `/var/task`, keeps its database in `/tmp`, and has no `HEALTHCHECK`. Its compose file runs the collector and the Aspire dashboard as well. Compose maps the runtime interface emulator to 8750, so the function can be invoked with a `POST` to `/2015-03-31/functions/function/invocations`. + +### Ports + +Every port published on the developer's machine, by the compose file or by `launchSettings.json`, sits in the template's own block of ten, so any two Lean Mean Clean solutions run side by side. The blocks follow the template order: Minimal API 8710, MCP 8720, GraphQL 8730, Web API 8740, Lambda Function 8750. Each port has the same place in every block: + +| Offset | Port | +| --- | --- | +| +0 | The app over HTTP, from Docker Compose or `dotnet run` | +| +1 | The app over HTTPS, `dotnet run` only | +| +2 | The Aspire dashboard | +| +3 | The collector's OTLP over gRPC | +| +4 | The collector's OTLP over HTTP | +| +5 | The MCP Inspector | +| +6 | The MCP chat demo | +| +7 to +9 | Spare | + +`docs/RUN.md` lists the ports this solution uses. Inside the containers nothing moves: the app listens on 8080, the .NET default, the dashboard on 18888 and the collector on 4317 and 4318, so only the left-hand side of a mapping is ours and the image deploys anywhere unchanged. A new service takes a spare port from the block, never a product's well-known one such as 3000, 5432 or 9000. `docker compose port ` prints where a port ended up. The `demo` profile adds Open WebUI for a chat over the tools (`mcp-inspector`). Keep it behind the profile so plain `docker compose up` stays the same. Solutions generated with authentication on leave it out. @@ -44,7 +61,7 @@ The `demo` profile adds Open WebUI for a chat over the tools (`mcp-inspector`). ``` bash docker build . --target run --tag local-check -docker compose up -d --build && curl -fsS http://localhost:8080/health/ready -# Lambda: curl -fsS -X POST http://localhost:9000/2015-03-31/functions/function/invocations -d '{}' +docker compose up -d --build && curl -fsS "http://$(docker compose port api 8080)/health/ready" +# Lambda: curl -fsS -X POST "http://$(docker compose port function 8080)/2015-03-31/functions/function/invocations" -d '{}' docker compose down ``` diff --git a/.agents/skills/mcp-inspector/SKILL.md b/.agents/skills/mcp-inspector/SKILL.md index 4a8554d..7cccd3d 100644 --- a/.agents/skills/mcp-inspector/SKILL.md +++ b/.agents/skills/mcp-inspector/SKILL.md @@ -13,14 +13,14 @@ metadata: | Service | Address | | --- | --- | -| MCP server | | -| MCP Inspector | | -| Aspire dashboard (logs, traces, metrics) | | +| MCP server | | +| MCP Inspector | | +| Aspire dashboard (logs, traces, metrics) | | ## Steps 1. Start everything: `docker compose up -d --build`. -2. Open , flip the switch on the server card to connect, then open **Tools**, **Resources** or **Prompts**. +2. Open , flip the switch on the server card to connect, then open **Tools**, **Resources** or **Prompts**. 3. After changing a tool, check its description and every argument's description read well, the annotations are right, and a call returns the result model you expect. 4. Try the failure paths too: an unknown id, a star rating of 6. The model should get a message it can act on. 5. Find the tool call in the Aspire dashboard if something looks wrong: its trace shows the tool, the use case and each SQL command, and its logs sit beside it (`telemetry`). @@ -28,17 +28,17 @@ metadata: ## Chat demo -To try the tools the way a model does, add the optional `demo` profile: `docker compose --profile demo up -d --build`. It starts [Open WebUI](https://github.com/open-webui/open-webui) at , already connected to the server and using Kilo's free gateway as the model, with no account or key. It's online only, and your prompts and tool results go to Kilo, a third party, so use it with sample data. Plain `docker compose up` doesn't start it, and solutions created with `--include-auth` leave it out, because Open WebUI has no token to send. +To try the tools the way a model does, add the optional `demo` profile: `docker compose --profile demo up -d --build`. It starts [Open WebUI](https://github.com/open-webui/open-webui) at , already connected to the server and using Kilo's free gateway as the model, with no account or key. It's online only, and your prompts and tool results go to Kilo, a third party, so use it with sample data. Plain `docker compose up` doesn't start it, and solutions created with `--include-auth` leave it out, because Open WebUI has no token to send. ## Don't -- Expose port 6274 beyond `127.0.0.1`. The Inspector can call every tool. +- Expose port 8725 beyond `127.0.0.1`. The Inspector can call every tool. - Use `latest` for the Inspector image. Change the pinned version on purpose. - Treat a manual check as a test. Write the integration test as well (`presentation-integration-tests`). ## Check ``` bash -docker compose up -d --build && curl -fsS http://localhost:8080/health/ready +docker compose up -d --build && curl -fsS http://localhost:8720/health/ready docker compose down ``` diff --git a/.agents/skills/serilog/SKILL.md b/.agents/skills/serilog/SKILL.md index ada5ea5..2c4c22e 100644 --- a/.agents/skills/serilog/SKILL.md +++ b/.agents/skills/serilog/SKILL.md @@ -14,7 +14,7 @@ Serilog is the logger. Application and Infrastructure code log through `Microsof | Environment | Sink | Where to look | | --- | --- | --- | | `local` | Console, with the output template in `appsettings.local.json` | The terminal | -| `docker` | Console, and OTLP through the collector to the Aspire dashboard | , or `docker compose logs` | +| `docker` | Console, and OTLP through the collector to the Aspire dashboard | The Aspire dashboard, on the port `docs/RUN.md` lists, or `docker compose logs` | | Lambda, anywhere | Console, as compact JSON, and OTLP when the endpoint is set | CloudWatch Logs, or `docker compose logs` for the emulator, and the Aspire dashboard in Docker Compose | | Any, when `OTEL_EXPORTER_OTLP_ENDPOINT` is set | Also OTLP, through `Serilog.Sinks.OpenTelemetry` | Whatever receives the OTLP (`telemetry`) | @@ -108,6 +108,6 @@ _ = builder.Services.AddSerilog((services, loggerConfiguration) => ``` bash dotnet run --project ./src/Presentation # watch the console, then make a request -docker compose up -d --build # then open http://localhost:18888 +docker compose up -d --build # then open the Aspire dashboard on the port docs/RUN.md lists dotnet test tests/Application.Tests.Unit # LoggingBehaviourTests check the messages with FakeLogger ``` diff --git a/.agents/skills/telemetry/SKILL.md b/.agents/skills/telemetry/SKILL.md index 34d0eb8..1c7e7ae 100644 --- a/.agents/skills/telemetry/SKILL.md +++ b/.agents/skills/telemetry/SKILL.md @@ -40,7 +40,7 @@ The Lambda template builds its tracer and meter providers once in `Startup` with The app sends to an OpenTelemetry Collector, never straight to a backend, because that's how it should run in production: the collector owns batching, retries, sampling, scrubbing and the choice of backend, so none of that lives in the app. `otel-collector.yaml` holds its configuration: an OTLP receiver on 4317 (gRPC) and 4318 (HTTP), the `memory_limiter` and `batch` processors, and one exporter per backend. -Locally, Docker Compose runs the collector (`otel/opentelemetry-collector`) and the Aspire dashboard (`mcr.microsoft.com/dotnet/aspire-dashboard`), and the collector forwards everything to the dashboard. The dashboard is only a container that receives OTLP and shows logs, traces and metrics at ; it keeps everything in memory, so it's empty after a restart. In production, run a collector beside the app or as a gateway and swap the dashboard exporter for your backend's. +Locally, Docker Compose runs the collector (`otel/opentelemetry-collector`) and the Aspire dashboard (`mcr.microsoft.com/dotnet/aspire-dashboard`), and the collector forwards everything to the dashboard. The dashboard is only a container that receives OTLP and shows logs, traces and metrics on the template's dashboard port (`docker-and-compose`); it keeps everything in memory, so it's empty after a restart. In production, run a collector beside the app or as a gateway and swap the dashboard exporter for your backend's. ## Steps @@ -51,7 +51,7 @@ Locally, Docker Compose runs the collector (`otel/opentelemetry-collector`) and 5. Use stable packages only. When the only instrumentation is a prerelease package, go without that signal and add it to the watch list below. 6. Test spans with an `ActivityListener` that keeps only the test's own trace (`unit-tests`). The integration tests send a `traceparent` header and check the request and use case spans join up. 7. To send telemetry somewhere new, add an exporter to `otel-collector.yaml` and list it in the pipelines. The app's settings don't change. -8. To see it locally, run `docker compose up -d --build` and open . For a `dotnet run`, start the collector with `docker compose up -d otel-collector` and set `OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317`. +8. To see it locally, run `docker compose up -d --build` and open the dashboard on the port `docs/RUN.md` lists. For a `dotnet run`, start the collector with `docker compose up -d otel-collector` and set `OTEL_EXPORTER_OTLP_ENDPOINT` to its gRPC port, `http://$(docker compose port otel-collector 4317)`. ## Example @@ -110,5 +110,5 @@ These signals and packages aren't stable for .NET yet. Check them when dependenc ``` bash dotnet test --filter "FullyQualifiedName~Telemetry|FullyQualifiedName~LoggingBehaviour" # use case span and end-to-end trace tests -docker compose up -d --build # then open http://localhost:18888 and make a request +docker compose up -d --build # then open the dashboard on the port docs/RUN.md lists and make a request ``` diff --git a/AGENTS.md b/AGENTS.md index 8e48c35..3059078 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,7 +27,7 @@ Run these before you say a change is done. They're what CI runs. dotnet build # warnings are errors dotnet test # every test project dotnet format --verify-no-changes # style, whitespace and usings -dotnet run --project ./src/Presentation # the MCP server at http://localhost:8080/mcp +dotnet run --project ./src/Presentation # the MCP server at http://localhost:8720/mcp ``` The `coverage` skill has the command that checks the 95% line coverage gate locally. diff --git a/NUGET.md b/NUGET.md index 5b3700a..c239741 100644 --- a/NUGET.md +++ b/NUGET.md @@ -48,7 +48,7 @@ docker compose up -d --build dotnet test ``` -Then open the [MCP Inspector](https://github.com/modelcontextprotocol/inspector) at . It's already pointed at the server at , so flip the switch to connect and open **Tools**. +Then open the [MCP Inspector](https://github.com/modelcontextprotocol/inspector) at . It's already pointed at the server at , so flip the switch to connect and open **Tools**. The server is open unless you create it with `--include-auth --authority` (and usually `--audience`), and then `/mcp` asks for a bearer token and publishes protected resource metadata so clients can sign in. Leave `--include-auth` out and there's no auth code at all. diff --git a/README.md b/README.md index 51a3277..ac9e91a 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ That's three layers, four test projects, a Dockerfile and a CI pipeline. Go and docker compose up -d --build # Ask the movies_search tool for a page of movies -curl http://localhost:8080/mcp \ +curl http://localhost:8720/mcp \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{ @@ -85,11 +85,11 @@ The answer comes back as an event stream, and its result carries the movies as s The sample movies are all made up, and the critics are harsh. -Curl is a fine way to prove it works and a terrible way to spend an afternoon. Open the [MCP Inspector](https://github.com/modelcontextprotocol/inspector) at instead. It's already pointed at the server, so flip the switch to connect and open **Tools**. +Curl is a fine way to prove it works and a terrible way to spend an afternoon. Open the [MCP Inspector](https://github.com/modelcontextprotocol/inspector) at instead. It's already pointed at the server, so flip the switch to connect and open **Tools**. -For a chat over the same tools, `docker compose --profile demo up -d --build` adds [Open WebUI](https://github.com/open-webui/open-webui) at . It's optional and online only, and your prompts go to Kilo's free gateway, a third party. [Run it](docs/RUN.md#chat-demo) has the details. +For a chat over the same tools, `docker compose --profile demo up -d --build` adds [Open WebUI](https://github.com/open-webui/open-webui) at . It's optional and online only, and your prompts go to Kilo's free gateway, a third party. [Run it](docs/RUN.md#chat-demo) has the details. -There are six tools shaped around jobs rather than tables: `movies_search`, `movies_get`, `authors_search`, `reviews_list`, `reviews_write` and `reviews_delete`. Each has a description written for agents and hints that say what's read-only and what's destructive. Lists come back ten at a time, with a `totalCount` and a `nextOffset` when there's more to come, so an agent doesn't drown in its own context window. There are resources for every movie, author and review, and prompts for jobs like comparing two movies. Its logs, traces and metrics land in the [Aspire dashboard](https://learn.microsoft.com/dotnet/aspire/fundamentals/dashboard/standalone) at . +There are six tools shaped around jobs rather than tables: `movies_search`, `movies_get`, `authors_search`, `reviews_list`, `reviews_write` and `reviews_delete`. Each has a description written for agents and hints that say what's read-only and what's destructive. Lists come back ten at a time, with a `totalCount` and a `nextOffset` when there's more to come, so an agent doesn't drown in its own context window. There are resources for every movie, author and review, and prompts for jobs like comparing two movies. Its logs, traces and metrics land in the [Aspire dashboard](https://learn.microsoft.com/dotnet/aspire/fundamentals/dashboard/standalone) at . No Docker? `dotnet run --project src/Presentation` works too. Every other way to run it, from plain `docker run` to stamping a version into the image, is in [docs/RUN.md](docs/RUN.md). diff --git a/docker-compose.yml b/docker-compose.yml index 519e3d1..264a6a2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,7 @@ services: VERSION_PREFIX: 1.0.0.0 VERSION_SUFFIX: docker ports: - - "8080:8080" + - "8720:8080" environment: DOTNET_ENVIRONMENT: docker OTEL_EXPORTER_OTLP_ENDPOINT: http://otel-collector:4317 @@ -21,8 +21,11 @@ services: inspector: image: ghcr.io/modelcontextprotocol/inspector:2.13.0 command: ["--web", "--transport", "http", "--server-url", "http://api:8080/mcp"] + environment: + # The Inspector only accepts calls from the origin it listens on, so it listens on the same port inside and out. + CLIENT_PORT: "8725" ports: - - "127.0.0.1:6274:6274" + - "127.0.0.1:8725:8725" networks: - cleanmodelcontextprotocol_network depends_on: @@ -32,8 +35,8 @@ services: volumes: - ./otel-collector.yaml:/etc/otelcol/config.yaml:ro ports: - - "4317:4317" - - "4318:4318" + - "8723:4317" + - "8724:4318" networks: - cleanmodelcontextprotocol_network depends_on: @@ -41,7 +44,7 @@ services: aspire-dashboard: image: mcr.microsoft.com/dotnet/aspire-dashboard:13.6.2 ports: - - "18888:18888" + - "8722:18888" environment: ASPIRE_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS: "true" networks: @@ -53,7 +56,7 @@ services: profiles: - demo ports: - - "127.0.0.1:3000:8080" + - "127.0.0.1:8726:8080" environment: # Kilo's free gateway takes no API key, and a dummy one gets a 401. OPENAI_API_BASE_URL: https://api.kilo.ai/api/gateway diff --git a/docs/RUN.md b/docs/RUN.md index f55d1c1..c8055b8 100644 --- a/docs/RUN.md +++ b/docs/RUN.md @@ -23,16 +23,16 @@ dotnet run --project ./src/Presentation ## Docker Compose -`docker-compose.yml` starts the app, the [MCP Inspector](https://github.com/modelcontextprotocol/inspector) already pointed at it, and the [Aspire dashboard](https://learn.microsoft.com/dotnet/aspire/fundamentals/dashboard/standalone), which shows its logs, traces and metrics. The app sends everything to an [OpenTelemetry Collector](https://opentelemetry.io/docs/collector/), and the collector forwards it to the dashboard, the same shape as a production setup. The dashboard is only a container that receives OTLP; nothing in the solution depends on Aspire. The dashboard has no login and the collector accepts OTLP from anyone on ports 4317 and 4318. That's on purpose for a developer's machine, so don't run this compose file anywhere other people can reach. +`docker-compose.yml` starts the app, the [MCP Inspector](https://github.com/modelcontextprotocol/inspector) already pointed at it, and the [Aspire dashboard](https://learn.microsoft.com/dotnet/aspire/fundamentals/dashboard/standalone), which shows its logs, traces and metrics. The app sends everything to an [OpenTelemetry Collector](https://opentelemetry.io/docs/collector/), and the collector forwards it to the dashboard, the same shape as a production setup. The dashboard is only a container that receives OTLP; nothing in the solution depends on Aspire. The dashboard has no login and the collector accepts OTLP from anyone on ports 8723 and 8724. That's on purpose for a developer's machine, so don't run this compose file anywhere other people can reach. ``` bash docker compose up -d --build ``` -- App: (MCP endpoint, Streamable HTTP) -- MCP Inspector: . Flip the switch on the server card to connect, then open **Tools** to see every tool with its description and input schema and run it. -- Aspire dashboard: -- Health checks: (the process is up) and (the database can be reached). Both answer without a token. The image's `HEALTHCHECK` uses the live one. +- App: (MCP endpoint, Streamable HTTP) +- MCP Inspector: . Flip the switch on the server card to connect, then open **Tools** to see every tool with its description and input schema and run it. +- Aspire dashboard: +- Health checks: (the process is up) and (the database can be reached). Both answer without a token. The image's `HEALTHCHECK` uses the live one. The container runs as the image's non-root `app` user. @@ -51,7 +51,23 @@ Want to ask for things in plain English? The optional `demo` profile adds [Open docker compose --profile demo up -d --build ``` -Open , dismiss the release notes and ask it to list the movies. It uses Kilo's free gateway as the model, so there's no account or key, but it only works online, and your prompts and tool results go to Kilo, a third party. Keep it to sample data. Solutions created with `--include-auth` leave it out, because Open WebUI has no token to send. Stop it with `docker compose --profile demo down`. +Open , dismiss the release notes and ask it to list the movies. It uses Kilo's free gateway as the model, so there's no account or key, but it only works online, and your prompts and tool results go to Kilo, a third party. Keep it to sample data. Solutions created with `--include-auth` leave it out, because Open WebUI has no token to send. Stop it with `docker compose --profile demo down`. + +## Ports + +Every Lean Mean Clean template has its own block of ten ports on your machine, so this solution runs beside one made from another template without a clash. This one uses 8720 to 8729: + +| Port | What listens | +| --- | --- | +| 8720 | The app over HTTP, from Docker Compose or `dotnet run` | +| 8721 | The app over HTTPS, `dotnet run` only | +| 8722 | The Aspire dashboard | +| 8723 | The OpenTelemetry Collector, OTLP over gRPC | +| 8724 | The OpenTelemetry Collector, OTLP over HTTP | +| 8725 | The MCP Inspector | +| 8726 | The chat demo | + +Inside the containers nothing moves: the app listens on 8080, the .NET default, so the image runs anywhere without changes, and the dashboard and collector keep their usual ports. To move a port, change the left-hand side of its mapping in `docker-compose.yml`, or `applicationUrl` in `launchSettings.json`. The Inspector is the one exception: it only accepts calls from the origin it listens on, so it listens on 8725 inside its container too. 8727 to 8729 are spare for anything you add. ## Telemetry @@ -63,7 +79,7 @@ To see a `dotnet run` in the dashboard, start the collector and the dashboard, a ``` bash docker compose up -d otel-collector -OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 dotnet run --project ./src/Presentation +OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:8723 dotnet run --project ./src/Presentation ``` Every span, metric and log carries `service.version`, so you can always tell which build sent it. diff --git a/src/Presentation/Properties/launchSettings.json b/src/Presentation/Properties/launchSettings.json index 79e77e4..193db27 100644 --- a/src/Presentation/Properties/launchSettings.json +++ b/src/Presentation/Properties/launchSettings.json @@ -5,7 +5,7 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": false, - "applicationUrl": "https://localhost:8081;http://localhost:8080", + "applicationUrl": "https://localhost:8721;http://localhost:8720", "environmentVariables": { "DOTNET_ENVIRONMENT": "local" } From 1e8d45b56a774101bd4c0f9b1fad82e68d34de7e Mon Sep 17 00:00:00 2001 From: Stephen Walsh <868017+stphnwlsh@users.noreply.github.com> Date: Fri, 6 Nov 2026 23:49:42 +0000 Subject: [PATCH 2/3] Move to the 73x0 blocks and use the same port inside the container 87x0 had no room past six templates, so the blocks now start at 7310, which leaves space for fifteen. The app also listens on its own port inside its container, so dotnet run, docker run and Docker Compose all use one number. Lambda's emulator stays on 8080 inside, because it can't be moved. --- .agents/skills/docker-and-compose/SKILL.md | 8 ++--- .agents/skills/mcp-inspector/SKILL.md | 16 +++++----- .github/workflows/build-pipeline.yml | 6 ++-- AGENTS.md | 2 +- Dockerfile | 5 +-- NUGET.md | 2 +- README.md | 8 ++--- docker-compose.yml | 18 +++++------ docs/RUN.md | 32 +++++++++---------- .../Properties/launchSettings.json | 2 +- 10 files changed, 50 insertions(+), 49 deletions(-) diff --git a/.agents/skills/docker-and-compose/SKILL.md b/.agents/skills/docker-and-compose/SKILL.md index 2e274d8..f81e038 100644 --- a/.agents/skills/docker-and-compose/SKILL.md +++ b/.agents/skills/docker-and-compose/SKILL.md @@ -21,11 +21,11 @@ One multi-stage `Dockerfile` does everything the pipeline needs: `docker-compose.yml` runs the app with `DOTNET_ENVIRONMENT=docker`, the `movie-reviews-data` volume a pinned OpenTelemetry Collector that receives the app's OTLP logs, traces and metrics, configured by `otel-collector.yaml`, and a pinned Aspire dashboard the collector forwards them to (`telemetry`). The dashboard UI and the collector's OTLP ports are published on the template's own ports (below), so a `dotnet run` can send to the collector too. The dashboard runs with `ASPIRE_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS` and the collector's OTLP ports take anything sent to them, on purpose: the compose file is for a developer's machine, with nothing to sign in to. Keep it that way rather than adding sign-in, and don't offer the compose file for a shared or deployed environment. Images are built by CI but not pushed anywhere; the template ships the Dockerfile so users can. -The Lambda template's `run` stage is the AWS Lambda .NET base image instead. It runs as user `65534` from `/var/task`, keeps its database in `/tmp`, and has no `HEALTHCHECK`. Its compose file runs the collector and the Aspire dashboard as well. Compose maps the runtime interface emulator to 8750, so the function can be invoked with a `POST` to `/2015-03-31/functions/function/invocations`. +The Lambda template's `run` stage is the AWS Lambda .NET base image instead. It runs as user `65534` from `/var/task`, keeps its database in `/tmp`, and has no `HEALTHCHECK`. Its compose file runs the collector and the Aspire dashboard as well. Compose maps the runtime interface emulator to 7350, so the function can be invoked with a `POST` to `/2015-03-31/functions/function/invocations`. ### Ports -Every port published on the developer's machine, by the compose file or by `launchSettings.json`, sits in the template's own block of ten, so any two Lean Mean Clean solutions run side by side. The blocks follow the template order: Minimal API 8710, MCP 8720, GraphQL 8730, Web API 8740, Lambda Function 8750. Each port has the same place in every block: +Every port published on the developer's machine, by the compose file or by `launchSettings.json`, sits in the template's own block of ten, so any two Lean Mean Clean solutions run side by side. The blocks follow the template order: Minimal API 7310, MCP 7320, GraphQL 7330, Web API 7340, Lambda Function 7350. Each port has the same place in every block: | Offset | Port | | --- | --- | @@ -38,7 +38,7 @@ Every port published on the developer's machine, by the compose file or by `laun | +6 | The MCP chat demo | | +7 to +9 | Spare | -`docs/RUN.md` lists the ports this solution uses. Inside the containers nothing moves: the app listens on 8080, the .NET default, the dashboard on 18888 and the collector on 4317 and 4318, so only the left-hand side of a mapping is ours and the image deploys anywhere unchanged. A new service takes a spare port from the block, never a product's well-known one such as 3000, 5432 or 9000. `docker compose port ` prints where a port ended up. +`docs/RUN.md` lists the ports this solution uses. The app listens on its own port everywhere: under `dotnet run`, in the image (the `Dockerfile` sets `ASPNETCORE_HTTP_PORTS`, `EXPOSE` and the `HEALTHCHECK` to it) and in the compose file, so `docker run -p` maps it to the same number. Change all four together. The Lambda template is the one exception: Lambda's runtime interface emulator always listens on 8080 inside its container, so only the host side of its mapping is ours. The dashboard, the collector and other third-party images keep their usual ports inside (18888, 4317 and 4318), and only the host side of their mappings moves. A new service takes a spare port from the block, never a product's well-known one such as 3000, 5432 or 9000. `docker compose port ` prints where a port ended up. The `demo` profile adds Open WebUI for a chat over the tools (`mcp-inspector`). Keep it behind the profile so plain `docker compose up` stays the same. Solutions generated with authentication on leave it out. @@ -61,7 +61,7 @@ The `demo` profile adds Open WebUI for a chat over the tools (`mcp-inspector`). ``` bash docker build . --target run --tag local-check -docker compose up -d --build && curl -fsS "http://$(docker compose port api 8080)/health/ready" +docker compose up -d --build --wait # waits for the image's HEALTHCHECK # Lambda: curl -fsS -X POST "http://$(docker compose port function 8080)/2015-03-31/functions/function/invocations" -d '{}' docker compose down ``` diff --git a/.agents/skills/mcp-inspector/SKILL.md b/.agents/skills/mcp-inspector/SKILL.md index 7cccd3d..f8a490e 100644 --- a/.agents/skills/mcp-inspector/SKILL.md +++ b/.agents/skills/mcp-inspector/SKILL.md @@ -9,18 +9,18 @@ metadata: ## The rule -`docker-compose.yml` runs the official [MCP Inspector](https://github.com/modelcontextprotocol/inspector) next to the server, already pointed at `http://api:8080/mcp` over streamable HTTP. It's the quickest way to see exactly what a client gets: every tool with its description, input and output schema and annotations, every resource and template, and every prompt. The image version is pinned in the compose file, and the port is bound to `127.0.0.1` only. +`docker-compose.yml` runs the official [MCP Inspector](https://github.com/modelcontextprotocol/inspector) next to the server, already pointed at `http://api:7320/mcp` over streamable HTTP. It's the quickest way to see exactly what a client gets: every tool with its description, input and output schema and annotations, every resource and template, and every prompt. The image version is pinned in the compose file, and the port is bound to `127.0.0.1` only. | Service | Address | | --- | --- | -| MCP server | | -| MCP Inspector | | -| Aspire dashboard (logs, traces, metrics) | | +| MCP server | | +| MCP Inspector | | +| Aspire dashboard (logs, traces, metrics) | | ## Steps 1. Start everything: `docker compose up -d --build`. -2. Open , flip the switch on the server card to connect, then open **Tools**, **Resources** or **Prompts**. +2. Open , flip the switch on the server card to connect, then open **Tools**, **Resources** or **Prompts**. 3. After changing a tool, check its description and every argument's description read well, the annotations are right, and a call returns the result model you expect. 4. Try the failure paths too: an unknown id, a star rating of 6. The model should get a message it can act on. 5. Find the tool call in the Aspire dashboard if something looks wrong: its trace shows the tool, the use case and each SQL command, and its logs sit beside it (`telemetry`). @@ -28,17 +28,17 @@ metadata: ## Chat demo -To try the tools the way a model does, add the optional `demo` profile: `docker compose --profile demo up -d --build`. It starts [Open WebUI](https://github.com/open-webui/open-webui) at , already connected to the server and using Kilo's free gateway as the model, with no account or key. It's online only, and your prompts and tool results go to Kilo, a third party, so use it with sample data. Plain `docker compose up` doesn't start it, and solutions created with `--include-auth` leave it out, because Open WebUI has no token to send. +To try the tools the way a model does, add the optional `demo` profile: `docker compose --profile demo up -d --build`. It starts [Open WebUI](https://github.com/open-webui/open-webui) at , already connected to the server and using Kilo's free gateway as the model, with no account or key. It's online only, and your prompts and tool results go to Kilo, a third party, so use it with sample data. Plain `docker compose up` doesn't start it, and solutions created with `--include-auth` leave it out, because Open WebUI has no token to send. ## Don't -- Expose port 8725 beyond `127.0.0.1`. The Inspector can call every tool. +- Expose port 7325 beyond `127.0.0.1`. The Inspector can call every tool. - Use `latest` for the Inspector image. Change the pinned version on purpose. - Treat a manual check as a test. Write the integration test as well (`presentation-integration-tests`). ## Check ``` bash -docker compose up -d --build && curl -fsS http://localhost:8720/health/ready +docker compose up -d --build && curl -fsS http://localhost:7320/health/ready docker compose down ``` diff --git a/.github/workflows/build-pipeline.yml b/.github/workflows/build-pipeline.yml index baae758..2cfa455 100644 --- a/.github/workflows/build-pipeline.yml +++ b/.github/workflows/build-pipeline.yml @@ -357,16 +357,16 @@ jobs: echo "smoke=failed" >> "$GITHUB_OUTPUT" # The database goes in /app/data, the only folder the non-root user can write to, as it does # in docker-compose.yml. Logs stay on the console so a failure shows up below. - docker run --detach --name api --publish 8080:8080 \ + docker run --detach --name api --publish 7320:7320 \ --env ConnectionStrings__MovieReviews="Data Source=/app/data/movie-reviews.db" \ --env Database__SeedSampleData=true "$IMAGE" for attempt in $(seq 1 30); do - if curl --silent --fail --max-time 5 http://localhost:8080/health/ready > /dev/null; then + if curl --silent --fail --max-time 5 http://localhost:7320/health/ready > /dev/null; then break fi sleep 1 done - RESPONSE=$(curl --silent --fail --max-time 30 http://localhost:8080/mcp \ + RESPONSE=$(curl --silent --fail --max-time 30 http://localhost:7320/mcp \ --header 'Content-Type: application/json' \ --header 'Accept: application/json, text/event-stream' \ --data '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "movies_search", "arguments": { "limit": 2 } } }' || true) diff --git a/AGENTS.md b/AGENTS.md index 3059078..f15ec7b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,7 +27,7 @@ Run these before you say a change is done. They're what CI runs. dotnet build # warnings are errors dotnet test # every test project dotnet format --verify-no-changes # style, whitespace and usings -dotnet run --project ./src/Presentation # the MCP server at http://localhost:8720/mcp +dotnet run --project ./src/Presentation # the MCP server at http://localhost:7320/mcp ``` The `coverage` skill has the command that checks the 95% line coverage gate locally. diff --git a/Dockerfile b/Dockerfile index d5f746a..797715c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -59,7 +59,8 @@ RUN dotnet publish ./src/**/Presentation.csproj --no-restore -c Release -v quiet # Runtime Image FROM ${BASE_IMAGE_REPO}/${BASE_IMAGE_RUNTIME}:${BASE_IMAGE_RUNTIME_TAG} AS run WORKDIR /app -EXPOSE 8080 +ENV ASPNETCORE_HTTP_PORTS=7320 +EXPOSE 7320 COPY --from=publish /sln/app . # The database lives in /app/data, the only folder the non-root app user can write to. @@ -67,6 +68,6 @@ RUN mkdir -p /app/data && chown $APP_UID /app/data USER $APP_UID HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ - CMD wget -qO- http://localhost:8080/health/live || exit 1 + CMD wget -qO- http://localhost:7320/health/live || exit 1 ENTRYPOINT ["dotnet", "Presentation.dll"] diff --git a/NUGET.md b/NUGET.md index c239741..3ee7e17 100644 --- a/NUGET.md +++ b/NUGET.md @@ -48,7 +48,7 @@ docker compose up -d --build dotnet test ``` -Then open the [MCP Inspector](https://github.com/modelcontextprotocol/inspector) at . It's already pointed at the server at , so flip the switch to connect and open **Tools**. +Then open the [MCP Inspector](https://github.com/modelcontextprotocol/inspector) at . It's already pointed at the server at , so flip the switch to connect and open **Tools**. The server is open unless you create it with `--include-auth --authority` (and usually `--audience`), and then `/mcp` asks for a bearer token and publishes protected resource metadata so clients can sign in. Leave `--include-auth` out and there's no auth code at all. diff --git a/README.md b/README.md index ac9e91a..4bb4caa 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ That's three layers, four test projects, a Dockerfile and a CI pipeline. Go and docker compose up -d --build # Ask the movies_search tool for a page of movies -curl http://localhost:8720/mcp \ +curl http://localhost:7320/mcp \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{ @@ -85,11 +85,11 @@ The answer comes back as an event stream, and its result carries the movies as s The sample movies are all made up, and the critics are harsh. -Curl is a fine way to prove it works and a terrible way to spend an afternoon. Open the [MCP Inspector](https://github.com/modelcontextprotocol/inspector) at instead. It's already pointed at the server, so flip the switch to connect and open **Tools**. +Curl is a fine way to prove it works and a terrible way to spend an afternoon. Open the [MCP Inspector](https://github.com/modelcontextprotocol/inspector) at instead. It's already pointed at the server, so flip the switch to connect and open **Tools**. -For a chat over the same tools, `docker compose --profile demo up -d --build` adds [Open WebUI](https://github.com/open-webui/open-webui) at . It's optional and online only, and your prompts go to Kilo's free gateway, a third party. [Run it](docs/RUN.md#chat-demo) has the details. +For a chat over the same tools, `docker compose --profile demo up -d --build` adds [Open WebUI](https://github.com/open-webui/open-webui) at . It's optional and online only, and your prompts go to Kilo's free gateway, a third party. [Run it](docs/RUN.md#chat-demo) has the details. -There are six tools shaped around jobs rather than tables: `movies_search`, `movies_get`, `authors_search`, `reviews_list`, `reviews_write` and `reviews_delete`. Each has a description written for agents and hints that say what's read-only and what's destructive. Lists come back ten at a time, with a `totalCount` and a `nextOffset` when there's more to come, so an agent doesn't drown in its own context window. There are resources for every movie, author and review, and prompts for jobs like comparing two movies. Its logs, traces and metrics land in the [Aspire dashboard](https://learn.microsoft.com/dotnet/aspire/fundamentals/dashboard/standalone) at . +There are six tools shaped around jobs rather than tables: `movies_search`, `movies_get`, `authors_search`, `reviews_list`, `reviews_write` and `reviews_delete`. Each has a description written for agents and hints that say what's read-only and what's destructive. Lists come back ten at a time, with a `totalCount` and a `nextOffset` when there's more to come, so an agent doesn't drown in its own context window. There are resources for every movie, author and review, and prompts for jobs like comparing two movies. Its logs, traces and metrics land in the [Aspire dashboard](https://learn.microsoft.com/dotnet/aspire/fundamentals/dashboard/standalone) at . No Docker? `dotnet run --project src/Presentation` works too. Every other way to run it, from plain `docker run` to stamping a version into the image, is in [docs/RUN.md](docs/RUN.md). diff --git a/docker-compose.yml b/docker-compose.yml index 264a6a2..4497dde 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,7 @@ services: VERSION_PREFIX: 1.0.0.0 VERSION_SUFFIX: docker ports: - - "8720:8080" + - "7320:7320" environment: DOTNET_ENVIRONMENT: docker OTEL_EXPORTER_OTLP_ENDPOINT: http://otel-collector:4317 @@ -20,12 +20,12 @@ services: - otel-collector inspector: image: ghcr.io/modelcontextprotocol/inspector:2.13.0 - command: ["--web", "--transport", "http", "--server-url", "http://api:8080/mcp"] + command: ["--web", "--transport", "http", "--server-url", "http://api:7320/mcp"] environment: # The Inspector only accepts calls from the origin it listens on, so it listens on the same port inside and out. - CLIENT_PORT: "8725" + CLIENT_PORT: "7325" ports: - - "127.0.0.1:8725:8725" + - "127.0.0.1:7325:7325" networks: - cleanmodelcontextprotocol_network depends_on: @@ -35,8 +35,8 @@ services: volumes: - ./otel-collector.yaml:/etc/otelcol/config.yaml:ro ports: - - "8723:4317" - - "8724:4318" + - "7323:4317" + - "7324:4318" networks: - cleanmodelcontextprotocol_network depends_on: @@ -44,7 +44,7 @@ services: aspire-dashboard: image: mcr.microsoft.com/dotnet/aspire-dashboard:13.6.2 ports: - - "8722:18888" + - "7322:18888" environment: ASPIRE_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS: "true" networks: @@ -56,14 +56,14 @@ services: profiles: - demo ports: - - "127.0.0.1:8726:8080" + - "127.0.0.1:7326:8080" environment: # Kilo's free gateway takes no API key, and a dummy one gets a 401. OPENAI_API_BASE_URL: https://api.kilo.ai/api/gateway DEFAULT_MODELS: kilo-auto/free ENABLE_OLLAMA_API: "False" WEBUI_AUTH: "False" - TOOL_SERVER_CONNECTIONS: '[{"url":"http://api:8080/mcp","path":"","type":"mcp","auth_type":"none","key":"","config":{"enable":true},"info":{"id":"mcp","name":"MCP server"}}]' + TOOL_SERVER_CONNECTIONS: '[{"url":"http://api:7320/mcp","path":"","type":"mcp","auth_type":"none","key":"","config":{"enable":true},"info":{"id":"mcp","name":"MCP server"}}]' # Turns the MCP server on in every chat, and leaves out Open WebUI's own tools so the model only sees ours. DEFAULT_MODEL_METADATA: '{"toolIds":["server:mcp:mcp"],"capabilities":{"builtin_tools":false}}' # These two skip the embedding model download from Hugging Face on first start. diff --git a/docs/RUN.md b/docs/RUN.md index c8055b8..9f84bf0 100644 --- a/docs/RUN.md +++ b/docs/RUN.md @@ -23,16 +23,16 @@ dotnet run --project ./src/Presentation ## Docker Compose -`docker-compose.yml` starts the app, the [MCP Inspector](https://github.com/modelcontextprotocol/inspector) already pointed at it, and the [Aspire dashboard](https://learn.microsoft.com/dotnet/aspire/fundamentals/dashboard/standalone), which shows its logs, traces and metrics. The app sends everything to an [OpenTelemetry Collector](https://opentelemetry.io/docs/collector/), and the collector forwards it to the dashboard, the same shape as a production setup. The dashboard is only a container that receives OTLP; nothing in the solution depends on Aspire. The dashboard has no login and the collector accepts OTLP from anyone on ports 8723 and 8724. That's on purpose for a developer's machine, so don't run this compose file anywhere other people can reach. +`docker-compose.yml` starts the app, the [MCP Inspector](https://github.com/modelcontextprotocol/inspector) already pointed at it, and the [Aspire dashboard](https://learn.microsoft.com/dotnet/aspire/fundamentals/dashboard/standalone), which shows its logs, traces and metrics. The app sends everything to an [OpenTelemetry Collector](https://opentelemetry.io/docs/collector/), and the collector forwards it to the dashboard, the same shape as a production setup. The dashboard is only a container that receives OTLP; nothing in the solution depends on Aspire. The dashboard has no login and the collector accepts OTLP from anyone on ports 7323 and 7324. That's on purpose for a developer's machine, so don't run this compose file anywhere other people can reach. ``` bash docker compose up -d --build ``` -- App: (MCP endpoint, Streamable HTTP) -- MCP Inspector: . Flip the switch on the server card to connect, then open **Tools** to see every tool with its description and input schema and run it. -- Aspire dashboard: -- Health checks: (the process is up) and (the database can be reached). Both answer without a token. The image's `HEALTHCHECK` uses the live one. +- App: (MCP endpoint, Streamable HTTP) +- MCP Inspector: . Flip the switch on the server card to connect, then open **Tools** to see every tool with its description and input schema and run it. +- Aspire dashboard: +- Health checks: (the process is up) and (the database can be reached). Both answer without a token. The image's `HEALTHCHECK` uses the live one. The container runs as the image's non-root `app` user. @@ -51,23 +51,23 @@ Want to ask for things in plain English? The optional `demo` profile adds [Open docker compose --profile demo up -d --build ``` -Open , dismiss the release notes and ask it to list the movies. It uses Kilo's free gateway as the model, so there's no account or key, but it only works online, and your prompts and tool results go to Kilo, a third party. Keep it to sample data. Solutions created with `--include-auth` leave it out, because Open WebUI has no token to send. Stop it with `docker compose --profile demo down`. +Open , dismiss the release notes and ask it to list the movies. It uses Kilo's free gateway as the model, so there's no account or key, but it only works online, and your prompts and tool results go to Kilo, a third party. Keep it to sample data. Solutions created with `--include-auth` leave it out, because Open WebUI has no token to send. Stop it with `docker compose --profile demo down`. ## Ports -Every Lean Mean Clean template has its own block of ten ports on your machine, so this solution runs beside one made from another template without a clash. This one uses 8720 to 8729: +Every Lean Mean Clean template has its own block of ten ports on your machine, so this solution runs beside one made from another template without a clash. This one uses 7320 to 7329: | Port | What listens | | --- | --- | -| 8720 | The app over HTTP, from Docker Compose or `dotnet run` | -| 8721 | The app over HTTPS, `dotnet run` only | -| 8722 | The Aspire dashboard | -| 8723 | The OpenTelemetry Collector, OTLP over gRPC | -| 8724 | The OpenTelemetry Collector, OTLP over HTTP | -| 8725 | The MCP Inspector | -| 8726 | The chat demo | +| 7320 | The app over HTTP, from Docker Compose or `dotnet run` | +| 7321 | The app over HTTPS, `dotnet run` only | +| 7322 | The Aspire dashboard | +| 7323 | The OpenTelemetry Collector, OTLP over gRPC | +| 7324 | The OpenTelemetry Collector, OTLP over HTTP | +| 7325 | The MCP Inspector | +| 7326 | The chat demo | -Inside the containers nothing moves: the app listens on 8080, the .NET default, so the image runs anywhere without changes, and the dashboard and collector keep their usual ports. To move a port, change the left-hand side of its mapping in `docker-compose.yml`, or `applicationUrl` in `launchSettings.json`. The Inspector is the one exception: it only accepts calls from the origin it listens on, so it listens on 8725 inside its container too. 8727 to 8729 are spare for anything you add. +The app listens on 7320 everywhere: under `dotnet run`, inside its container and on your machine, so `docker run -p 7320:7320` works the same way. The dashboard and collector keep their usual ports inside their containers. To move the app, change `applicationUrl` in `launchSettings.json`, `ASPNETCORE_HTTP_PORTS`, `EXPOSE` and the `HEALTHCHECK` in the `Dockerfile`, and its mapping in `docker-compose.yml`. When you deploy the image, point the platform's target port at 7320. The Inspector also listens on 7325 inside its container, because it only accepts calls from the origin it listens on. 7327 to 7329 are spare for anything you add. ## Telemetry @@ -79,7 +79,7 @@ To see a `dotnet run` in the dashboard, start the collector and the dashboard, a ``` bash docker compose up -d otel-collector -OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:8723 dotnet run --project ./src/Presentation +OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:7323 dotnet run --project ./src/Presentation ``` Every span, metric and log carries `service.version`, so you can always tell which build sent it. diff --git a/src/Presentation/Properties/launchSettings.json b/src/Presentation/Properties/launchSettings.json index 193db27..02dc77b 100644 --- a/src/Presentation/Properties/launchSettings.json +++ b/src/Presentation/Properties/launchSettings.json @@ -5,7 +5,7 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": false, - "applicationUrl": "https://localhost:8721;http://localhost:8720", + "applicationUrl": "https://localhost:7321;http://localhost:7320", "environmentVariables": { "DOTNET_ENVIRONMENT": "local" } From 2c363efe772bd018c9eca65de384ee3bcf16da42 Mon Sep 17 00:00:00 2001 From: Stephen Walsh <868017+stphnwlsh@users.noreply.github.com> Date: Sat, 7 Nov 2026 02:59:42 +0000 Subject: [PATCH 3/3] Use the block ports in the dotnet run HTTPS lines --- docs/RUN.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/RUN.md b/docs/RUN.md index 290bf0a..556ff37 100644 --- a/docs/RUN.md +++ b/docs/RUN.md @@ -21,7 +21,7 @@ dotnet ef migrations add --project ./src/Infrastructure --output-dir Data dotnet run --project ./src/Presentation ``` -It listens on , and on , which redirects there. If your browser or `curl` doesn't trust the .NET development certificate yet, run `dotnet dev-certs https --trust` once. +It listens on , and on , which redirects there. If your browser or `curl` doesn't trust the .NET development certificate yet, run `dotnet dev-certs https --trust` once. ## Docker Compose @@ -88,7 +88,7 @@ Every span, metric and log carries `service.version`, so you can always tell whi ## HTTPS -The app redirects HTTP to HTTPS whenever it has an HTTPS port, which is why `dotnet run` sends on to . Outside the `local` and `Development` environments it also sends an HSTS header. The container only listens on HTTP, so in production put it behind a proxy or load balancer that ends TLS, and set `ASPNETCORE_FORWARDEDHEADERS_ENABLED=true` so the app knows the original request was HTTPS. +The app redirects HTTP to HTTPS whenever it has an HTTPS port, which is why `dotnet run` sends on to . Outside the `local` and `Development` environments it also sends an HSTS header. The container only listens on HTTP, so in production put it behind a proxy or load balancer that ends TLS, and set `ASPNETCORE_FORWARDEDHEADERS_ENABLED=true` so the app knows the original request was HTTPS. ## Tests