diff --git a/.github/workflows/nuget-publish.yml b/.github/workflows/nuget-publish.yml
new file mode 100644
index 0000000..37599d2
--- /dev/null
+++ b/.github/workflows/nuget-publish.yml
@@ -0,0 +1,69 @@
+name: NuGet Publish
+
+on:
+ push:
+ tags: [ 'v*' ]
+ workflow_dispatch:
+
+permissions:
+ contents: read
+ packages: write
+
+env:
+ DOTNET_VERSION: '10.0.x'
+ DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
+ DOTNET_CLI_TELEMETRY_OPTOUT: true
+ DOTNET_NOLOGO: true
+
+jobs:
+ publish:
+ name: Pack & publish SCDMS.Aspire.Hosting
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
+
+ - name: Setup .NET 10
+ uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6
+ with:
+ dotnet-version: ${{ env.DOTNET_VERSION }}
+
+ - name: Compute version from git tag
+ shell: pwsh
+ id: version
+ run: |
+ $version = if ("${{ github.ref_type }}" -eq 'tag') { "${{ github.ref_name }}".TrimStart('v') } else { "1.0.0.0" }
+ Write-Host "Version: $version"
+ "value=$version" >> $env:GITHUB_OUTPUT
+
+ - name: Pack SCDMS.Aspire.Hosting
+ run: >
+ dotnet pack src/SCDMS.Aspire.Hosting/SCDMS.Aspire.Hosting.csproj
+ --configuration Release
+ --output ./artifacts
+ --configfile NuGet.Config
+ /p:ContinuousIntegrationBuild=true
+ /p:Version=${{ steps.version.outputs.value }}
+ /p:PackageReleaseNotes="SCDMS.Aspire.Hosting ${{ steps.version.outputs.value }} - see https://github.com/MPCoreDeveloper/SCDMS/blob/main/docs/aspire.md"
+
+ - name: Push to NuGet.org
+ shell: bash
+ env:
+ NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
+ run: |
+ set -euo pipefail
+ ls -1 ./artifacts/*.nupkg
+ dotnet nuget push ./artifacts/*.nupkg \
+ --api-key "$NUGET_API_KEY" \
+ --source https://api.nuget.org/v3/index.json \
+ --skip-duplicate
+
+ - name: Create release summary
+ run: |
+ echo "## π¦ NuGet Publish Completed" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "Published **SCDMS.Aspire.Hosting ${{ steps.version.outputs.value }}** to NuGet.org." >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "**Triggered by**: @${{ github.actor }} β commit \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY
diff --git a/Directory.Packages.props b/Directory.Packages.props
index a14794d..c0f714e 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -9,7 +9,15 @@
+
+
+
+
+
+
+
diff --git a/README.md b/README.md
index cf0c44a..f6682a8 100644
--- a/README.md
+++ b/README.md
@@ -72,6 +72,7 @@ Dev launchers: `scripts/launch.ps1` (Windows), `scripts/launch.sh` (Linux/macOS)
1. Tag: `git tag v1.0.0 && git push origin v1.0.0`
2. The [release workflow](.github/workflows/release.yml) publishes self-contained single-file binaries for `win-x64`, `linux-x64`, `linux-arm64`, `osx-x64`, `osx-arm64` plus `SHA256SUMS.txt` to GitHub Releases.
3. The [Docker workflow](.github/workflows/docker-publish.yml) builds the container image (`ghcr.io/mpcoredeveloper/scdms`) for `linux/amd64` + `linux/arm64`.
+4. The [NuGet workflow](.github/workflows/nuget-publish.yml) packs `SCDMS.Aspire.Hosting` and publishes it to NuGet.org.
## Docker & gRPC deployments
@@ -93,11 +94,25 @@ docker run --rm -p 8080:8080 \
ghcr.io/mpcoredeveloper/scdms:local
```
-A full example (SCDMS + SharpCoreDB server + Caddy reverse proxy with Let's Encrypt) lives in [`samples/docker/`](samples/docker/). Configuration reference for container deployments (env variables, HTTP mode, default gRPC server, data volumes) is in [docs/usage.md](docs/usage.md).
+A full example (SCDMS + SharpCoreDB server + Caddy reverse proxy with Let's Encrypt) lives in [`samples/docker/`](samples/docker/). Drop-in proxy alternatives with the same topology are [`samples/yarp/`](samples/yarp/) (all-.NET) and [`samples/haproxy/`](samples/haproxy/) (industry-standard HAProxy). Configuration reference for container deployments (env variables, HTTP mode, default gRPC server, data volumes) is in [docs/usage.md](docs/usage.md).
+
+## .NET Aspire
+
+SCDMS ships a `SCDMS.Aspire.Hosting` NuGet package (plus a runnable [AppHost example](examples/Aspire/SCDMS.AppHost/)) that runs SharpCoreDB server + SCDMS as one Aspire application β like pgweb/pgAdmin next to PostgreSQL:
+
+```csharp
+var db = builder.AddSharpCoreDB("db").WithServerContainer(); // SharpCoreDB server container
+builder.AddSCDMS("admin", db); // SCDMS container auto-wired over gRPC
+```
+
+Requires the published images `ghcr.io/mpcoredeveloper/sharpcoredb-server` (published) and `ghcr.io/mpcoredeveloper/scdms` (built on `v*` tags; until then `docker build -t ghcr.io/mpcoredeveloper/scdms:latest .`). See [docs/aspire.md](docs/aspire.md) for the design, status and the TLS notes. A step-by-step run & test guide for **both** options (Compose and Aspire) is in [docs/container-and-aspire-guide.md](docs/container-and-aspire-guide.md).
## Documentation
+- [Run & test guide β Docker Compose + .NET Aspire](docs/container-and-aspire-guide.md)
+
- [Usage & configuration](docs/usage.md)
+- [.NET Aspire integration (design & status)](docs/aspire.md)
- [Standalone/migration plan](https://github.com/MPCoreDeveloper/SharpCoreDB/blob/master/docs/viewer/scdms-standalone-plan.md) (in the SharpCoreDB repo)
- [SharpCoreDB documentation](https://github.com/MPCoreDeveloper/SharpCoreDB)
diff --git a/SCDMS.slnx b/SCDMS.slnx
index 42aeb4d..152ad9a 100644
--- a/SCDMS.slnx
+++ b/SCDMS.slnx
@@ -1,6 +1,10 @@
+
+
+
+
diff --git a/docs/aspire.md b/docs/aspire.md
index 9b4e715..ab78838 100644
--- a/docs/aspire.md
+++ b/docs/aspire.md
@@ -1,8 +1,9 @@
# .NET Aspire integration (issue #10)
-> **Status: design doc.** Fase 4 van de container/gRPC roadmap. Implementatie vereist eerst
-> twee zaken in de **SharpCoreDB**-repo (zie "Prerequisites"), daarna kan dit repo een
-> `SCDMS.Aspire.Hosting`-pakket toevoegen.
+> **Status: geΓ―mplementeerd (2026-09-05).** Fase 4 van de container/gRPC-roadmap is afgerond:
+> beide SharpCoreDB-prerequisites (server-image + `SharpCoreDB.Aspire.Hosting`) zijn gepubliceerd,
+> en dit repo levert nu het `SCDMS.Aspire.Hosting`-pakket, een voorbeeld-AppHost, een
+> NuGet-publish-workflow en documentatie.
## Goal
@@ -14,111 +15,77 @@ var builder = DistributedApplication.CreateBuilder(args);
var sharpCoreDb = builder.AddSharpCoreDB("db")
.WithServerContainer(); // SharpCoreDB server container
-builder.AddSCDMS("admin")
- .WithGrpcReference(sharpCoreDb) // SCDMS container linked via gRPC
- .WithHttpEndpoint(port: 8080, name: "http");
+// SCDMS web studio, gekoppeld aan de server over gRPC. AddSCDMS registreert meteen het
+// HTTP-endpoint (containerpoort 8080) en de SCDMS__* container-voorwaarden.
+var scdms = builder.AddSCDMS("admin", sharpCoreDb);
builder.Build().Run();
```
-Al het SCDMS β SharpCoreDB-dataverkeer loopt over **gRPC**.
+Al het SCDMS β SharpCoreDB-dataverkeer loopt over **gRPC**. Browser-URL in de AppHost:
+`scdms.GetEndpoint("http")`.
-## Prerequisites (SharpCoreDB repo β volgorde)
+## Prerequisites (SharpCoreDB-repo) β klaar
-1. **Server-image publiceren** naar `ghcr.io/mpcoredeveloper/sharpcoredb-server`
- (de `Dockerfile` in `src/SharpCoreDB.Server/` bestaat al; voeg een
- `docker/build-push-action`-workflow toe op `v*`-tags, `linux/amd64`+`linux/arm64`).
-2. **`SharpCoreDB.Aspire.Hosting`-pakket** publiceren met:
+1. β
**Server-image gepubliceerd** β `ghcr.io/mpcoredeveloper/sharpcoredb-server`
+ (`linux/amd64` + `linux/arm64`, getagd op elke `v*`-tag).
+2. β
**`SharpCoreDB.Aspire.Hosting`-pakket gepubliceerd** β versie `2.0.0.2`
+ (dependency: `Aspire.Hosting` 13.5.3, net10.0).
-```csharp
-// SharpCoreDB.Aspire.Hosting / SharpCoreDbServerResource.cs
-public sealed class SharpCoreDbServerResource(string name)
- : ContainerResource(name), IResourceWithConnectionString
-{
- public string? JwtSecretKey { get; set; }
- // ReferenceExpression voor de gRPC-connection string ("Host=...;Port=...;SSL=true")
-}
-```
+Publieke API die dit repo gebruikt:
-```csharp
-public static class SharpCoreDbAspireExtensions
-{
- // Container-gebaseerd (gepubliceerde image)
- public static IResourceBuilder AddSharpCoreDB(
- this IDistributedApplicationBuilder builder, string name) =>
- builder.AddResource(new SharpCoreDbServerResource(name))
- .WithImage("ghcr.io/mpcoredeveloper/sharpcoredb-server")
- .WithImageTag("latest")
- .WithHttpEndpoint(port: 5001, name: "grpc");
-
- // Convenience-alias voor het issue-snippet
- public static IResourceBuilder WithServerContainer(
- this IResourceBuilder resource) => resource;
-}
-```
+| Lid | Betekenis |
+|---|---|
+| `AddSharpCoreDB(name, imageTag = null, grpcPort = null, httpsApiPort = null)` | Registreert de servercontainer |
+| `.WithServerContainer()` | Documentatie-alias voor container-hosting |
+| `.WithImageTag(...)` | Image-tag pinnen |
+| `.WithJwtSecret(secret)` | Zet `Server__Security__JwtSecretKey` (min. 32 tekens) |
+| `SharpCoreDbServerResource.GrpcEndpointName` (= `"grpc"`) | HTTPS-gRPC-endpoint, containerpoort 5001 |
+| `SharpCoreDbServerResource.HttpsApiEndpointName` (= `"https"`) | HTTPS REST API, containerpoort 8443 |
-## SCDMS-repo implementatie (zodra prerequisites klaar zijn)
+Referentie (SharpCoreDB-kant): [`docs/server/ASPIRE_INTEGRATION.md`](https://github.com/MPCoreDeveloper/SharpCoreDB/blob/master/docs/server/ASPIRE_INTEGRATION.md)
-### Nieuw project `src/SCDMS.Aspire.Hosting/SCDMS.Aspire.Hosting.csproj`
+## Implementatie in dit repo (SCDMS)
-- `TargetFramework: net10.0`
-- PackageReference: `Aspire.Hosting` (zelfde versie als SharpCoreDB.AppHost gebruikt,
- i.c. 13.x) + project/package-ref naar `SharpCoreDB.Aspire.Hosting`.
-- `SCDMSResource : ContainerResource, IResourceWithConnectionString` (web-URL als
- connection string).
+### `src/SCDMS.Aspire.Hosting` β NuGet: `SCDMS.Aspire.Hosting`
-### `SCDMSAspireExtensions.cs` (API-schets)
+- **`ScdmsResource`** (`SCDMSResource.cs`) β `ContainerResource` + `IResourceWithConnectionString`
+ (web-URL als connection string). Endpoint-naam `http`, containerpoort 8080.
+- **`ScdmsAspireExtensions`** (`ScdmsAspireExtensions.cs`):
+ - `AddSCDMS(builder, name, sharpCoreDb = null, imageTag = null, port = null)` β registreert de
+ SCDMS-container op `ghcr.io/mpcoredeveloper/scdms` met de container-voorwaarden
+ (`SCDMS__EnableHttps=false`, `SCDMS__BindAddress=0.0.0.0`, `SCDMS__DataDirectory=/app/data`,
+ update-check uit). Wordt `sharpCoreDb` meegegeven, dan volgt automatisch `WithGrpcReference`.
+ - `WithGrpcReference(scdms, sharpCoreDb)` β koppelt het `grpc`-endpoint van de server via
+ `SCDMS__DefaultServerHost` / `SCDMS__DefaultServerPort`, zet `SCDMS__DefaultServerUseSsl=true`
+ en `SCDMS__DefaultServerAutoConnect=true`.
-```csharp
-public static class ScdmsAspireExtensions
-{
- public static IResourceBuilder AddSCDMS(
- this IDistributedApplicationBuilder builder,
- string name,
- IResourceBuilder? sharpCoreDb = null)
- {
- var scdms = builder.AddResource(new SCDMSResource(name))
- .WithImage("ghcr.io/mpcoredeveloper/scdms")
- .WithImageTag("latest")
- .WithHttpEndpoint(targetPort: 8080, name: "http")
- .WithEnvironment("SCDMS__EnableHttps", "false")
- .WithEnvironment("SCDMS__BindAddress", "0.0.0.0")
- .WithEnvironment("SCDMS__DataDirectory", "/app/data")
- .WithEnvironment("SCDMS__DefaultServerAutoConnect", "true");
-
- return sharpCoreDb is null ? scdms : scdms.WithGrpcReference(sharpCoreDb);
- }
-
- // Koppelt de gRPC-server aan SCDMS via SCDMS__DefaultServer* omgevingsvariabelen.
- public static IResourceBuilder WithGrpcReference(
- this IResourceBuilder scdms,
- IResourceBuilder sharpCoreDb)
- {
- var grpcEndpoint = sharpCoreDb.GetEndpoint("grpc");
- return scdms
- .WithEnvironment("SCDMS__DefaultServerHost", grpcEndpoint)
- .WithEnvironment("SCDMS__DefaultServerPort", grpcEndpoint.Property(EndpointProperty.Port))
- .WithEnvironment("SCDMS__DefaultServerUseSsl", "true")
- .WithEnvironment("SCDMS__DefaultServerAutoConnect", "true");
- }
-}
-```
+### Voorbeeld-AppHost: `examples/Aspire/SCDMS.AppHost`
-### Voorbeeld-AppHost (nieuw project, bijv. `examples/Aspire/SCDMS.AppHost`)
+Volledig draaibaar voorbeeld. Starten:
+
+```bash
+dotnet run --project examples/Aspire/SCDMS.AppHost/SCDMS.AppHost.csproj
+```
-- `` +
- `SharpCoreDB.Aspire.Hosting` (NuGet).
-- `Program.cs` met het snippet bovenaan dit document; browser-URL via
- `scdms.GetEndpoint("http")`.
+Lees `examples/Aspire/SCDMS.AppHost/README.md` voor de lokale-dev-/TLS-opmerkingen.
-### CI
+### CI/CD
-- Bouw/pack `SCDMS.Aspire.Hosting` en publiceer naar NuGet.org bij releases.
+- `ci.yml` bouwt de volledige oplossing (`SCDMS.slnx`, inclusief de nieuwe projecten) op
+ ubuntu/windows/macos.
+- `nuget-publish.yml` packt `SCDMS.Aspire.Hosting` en publiceert naar NuGet.org bij elke `v*`-tag
+ (vereist het `NUGET_API_KEY`-secret).
+- De SCDMS-image (`ghcr.io/mpcoredeveloper/scdms`) wordt gepubliceerd door `docker-publish.yml`
+ bij een `v*`-tag (deel 1 van het issue).
## Opmerkingen
-- De Aspire-local-run kan de server als container draaien (`WithServerContainer`). Voor
- TLS: in de Aspire-dev-omgeving is een publiek certificaat niet beschikbaar β gebruik de
- publiek-vertrouwde-proxy-aanpak in productie (zie `samples/docker/`) en voor lokale dev
- een dev-certificaat + `tls_insecure_skip_verify`-achtige optie in de hosting-extensie
- (of rechtstreeks container-intern over het Aspire-netwerk met de server-`/health`-check).
+- De SharpCoreDB-servercontainer spreekt **uitsluitend TLS** en heeft een certificaat nodig
+ (`Server__Security__TlsCertificatePath`); SCDMS valideert het certificaat van het gRPC-endpoint.
+- **Productie:** beΓ«indig TLS op een publiek vertrouwde reverse proxy (patroon in
+ `samples/docker/`), precies zoals de compose-sample.
+- **Lokale dev:** de servercontainer heeft nog steeds een (dev-)certificaat nodig; lees de
+ server-side certificaatopties in de SharpCoreDB `ASPIRE_INTEGRATION.md`. Voor een volledig
+ vertrouwde lokale run zonder extra CA-mounts blijft de proxy-topologie van `samples/docker/`
+ de aanbevolen route.
diff --git a/docs/container-and-aspire-guide.md b/docs/container-and-aspire-guide.md
new file mode 100644
index 0000000..3ae5c5c
--- /dev/null
+++ b/docs/container-and-aspire-guide.md
@@ -0,0 +1,368 @@
+# SCDMS + SharpCoreDB in containers β run & test guide
+
+This guide explains how to run **SCDMS** (the web database studio) together with a
+**SharpCoreDB server** using the two container-based options the project ships, and how to
+**test the whole thing yourself**:
+
+| Option | What you get | Where it lives | Best for |
+|---|---|---|---|
+| **A. Docker Compose** | SharpCoreDB server + SCDMS + TLS-terminating reverse proxy | [`samples/docker/`](../samples/docker/) (Caddy, auto-TLS) β alternatives: [`samples/yarp/`](../samples/yarp/) (.NET) and [`samples/haproxy/`](../samples/haproxy/) | Production-style deployments; teams with real domain names |
+| **B. .NET Aspire** | SharpCoreDB server + SCDMS as one Aspire app (Aspire dashboard, resources, endpoints) | `SCDMS.Aspire.Hosting` package + [`examples/Aspire/SCDMS.AppHost`](../examples/Aspire/SCDMS.AppHost/) | Local development & cloud-native orchestration |
+
+Both topologies share the same data path:
+
+```text
+Browser βββΊ SCDMS (web UI) ββgRPC/TLSβββΊ SharpCoreDB server
+ β β
+ plain HTTP :8080 gRPC :5001 (TLS only)
+ (TLS at reverse proxy + HTTPS API :8443
+ in production)
+```
+
+All SCDMS β SharpCoreDB data traffic flows over **gRPC**. SCDMS never talks to the server's
+HTTPS management API.
+
+---
+
+## 1. Read this first: how TLS works (it decides what you can test)
+
+- The SharpCoreDB server **only speaks TLS 1.2+** and refuses to start without a certificate
+ and a JWT secret (`Server__Security__*` settings).
+- SCDMS runs **plain HTTP on port 8080** inside a container. In production a reverse proxy
+ terminates TLS for the browser.
+- When SCDMS connects to the server over gRPC it **validates the server's certificate against
+ the OS trust store**. No "skip certificate verification" switch exists.
+
+Consequences for testing:
+
+| Scenario | Full end-to-end (SCDMS auto-connect) | Why |
+|---|---|---|
+| Compose with **public domain** | β
Works | Caddy holds a publicly trusted (Let's Encrypt) cert; SCDMS validates it |
+| Compose/Aspire with `localhost` + self-signed dev cert | β οΈ Partial | Browser warns; SCDMS cannot validate the self-signed server cert |
+| Server container + SCDMS **run on the host** with a **trusted** dev cert | β
Works | `dotnet dev-certs https --trust` adds the dev CA to the OS store; SCDMS on the host validates it |
+
+So: use a **public domain** for a fully green Compose test, or use a **trusted local dev
+certificate** with SCDMS running on the host for a green data-path test. Both recipes are below.
+
+---
+
+## 2. Prerequisites
+
+- **Docker** (Docker Desktop or a Linux engine) with the `docker compose` plugin, engine running.
+- **.NET 10 SDK** (the repo's `global.json` pins `10.0.400`).
+- Ports free: `80`/`443` (Compose/Caddy), `5001`/`8443` (server), `8080` (SCDMS),
+ plus dynamically allocated Aspire ports.
+- Container images:
+
+ - `ghcr.io/mpcoredeveloper/sharpcoredb-server` β **published** (tag `2.0.0.2` / `latest`).
+ - `ghcr.io/mpcoredeveloper/scdms` β built on `v*` tags. **Not published yet** β build locally:
+
+ ```bash
+ cd /SCDMS
+ docker build -t ghcr.io/mpcoredeveloper/scdms:latest .
+ ```
+
+---
+
+## 3. Option A β Docker Compose (production sample)
+
+The primary sample in [`samples/docker/`](../samples/docker/) runs SharpCoreDB server, SCDMS and
+**Caddy** (automatic Let's Encrypt TLS). Two drop-in alternatives with the identical topology and
+wiring β but manual certificate provisioning instead of automatic TLS β are in
+[`samples/yarp/`](../samples/yarp/) (all-.NET) and [`samples/haproxy/`](../samples/haproxy/)
+(industry-standard HAProxy).
+
+```text
+Browser ββHTTPS (public cert)βββΊ Caddy ββHTTPβββΊ scdms:8080
+SCDMS ββgRPC https://βββΊ Caddy ββgRPC/TLSβββΊ sharpcoredb:5001
+```
+
+### 3.1 Configure
+
+```bash
+cd samples/docker
+cp .env.example .env # PowerShell: Copy-Item .env.example .env
+```
+
+Edit `.env`:
+
+| Variable | Value |
+|---|---|
+| `SCDMS_DOMAIN` | public hostname for the SCDMS UI, e.g. `scdms.example.com` |
+| `GRPC_DOMAIN` | public hostname for the gRPC endpoint, e.g. `scdb.example.com` |
+| `ACME_EMAIL` | your e-mail (Let's Encrypt notifications) |
+| `SERVER_TLS_CERT_PATH` | `/app/certs/server.pfx` (keep default) |
+| `SERVER_JWT_SECRET` | **random secret β₯ 32 characters** |
+| `SDB_USERNAME` / `SDB_PASSWORD` | login SCDMS uses against the server |
+
+### 3.2 Provide the server certificate
+
+Place the server TLS certificate in `./server-certs/`. For testing you can create a development
+PFX (see the SharpCoreDB
+[`QUICKSTART.md`](https://github.com/MPCoreDeveloper/SharpCoreDB/blob/master/docs/server/QUICKSTART.md)
+for the authoritative dev-cert instructions):
+
+```bash
+mkdir -p server-certs
+dotnet dev-certs https -ep server-certs/server.pfx -p devonly --trust
+```
+
+> A self-signed dev certificate only works for **local** testing. For the fully working Compose
+> experience you need a real public domain so Let's Encrypt can issue a publicly trusted
+> certificate β then SCDMS can validate it.
+
+### 3.3 Start & verify
+
+```bash
+docker compose up -d
+docker compose ps # all three services "healthy"
+```
+
+Health checks (run from `samples/docker/`):
+
+```bash
+# SCDMS internal health (inside its container)
+docker compose exec scdms curl -fs http://localhost:8080/health
+
+# SharpCoreDB server health (inside its container, self-signed = -k)
+docker compose exec sharpcoredb curl -fsk https://localhost:8443/api/v1/health
+
+# Logs
+docker compose logs -f scdms
+```
+
+Expected results:
+
+- `docker compose ps` shows `caddy`, `scdms`, `sharpcoredb` all `healthy` (give it ~30-60 s).
+- Open `https://` β SCDMS UI loads and, with public domains, auto-connects to the
+ server (left sidebar shows the `master` database and/or the databases list).
+- `sharpcoredb` log line: `π Primary protocol (flagship): gRPC β¦ Endpoint: https://0.0.0.0:5001`.
+
+### 3.4 Local-only smoke test (no public domain)
+
+Point the browser at `https://localhost` instead and accept the browser warning. The
+**containers** will still be healthy and the SCDMS UI reachable; SCDMS's automatic gRPC connect
+may fail certificate validation β that is expected and is exactly the TLS caveat above. Use the
+full end-to-end recipe (Β§5) for a green data-path test.
+
+### 3.5 Stop
+
+```bash
+docker compose down # add -v to also delete the data volumes
+```
+
+### 3.6 Proxy alternatives: YARP & HAProxy
+
+Prefer a different proxy than Caddy? Two drop-in samples with the identical wiring are included:
+
+**YARP β all-.NET** ([`samples/yarp/`](../samples/yarp/)): builds a small `Yarp.ReverseProxy`
+2.3.0 proxy on Kestrel (~150 lines of C#, host-based routes). TLS is terminated with a mounted
+PFX; the proxy skips certificate validation only towards the *internal* server
+(`DangerousAcceptAnyServerCertificate`, mirroring Caddy's `tls_insecure_skip_verify`).
+
+```bash
+cd samples/yarp
+cp .env.example .env # set SCDMS_DOMAIN/GRPC_DOMAIN (localhost for local tests)
+mkdir -p server-certs
+dotnet dev-certs https -ep server-certs/server.pfx -p devonly
+docker compose up -d --build # builds the yarp proxy image
+```
+
+**HAProxy β industry standard** ([`samples/haproxy/`](../samples/haproxy/)): the most widely
+deployed proxy/load balancer; SNI-based routing in `haproxy.cfg`. TLS is terminated with a
+mounted **PEM** (private key + certificate):
+
+```bash
+cd samples/haproxy
+cp .env.example .env # set SCDMS_DOMAIN/GRPC_DOMAIN (localhost for local tests)
+mkdir -p server-certs
+dotnet dev-certs https -ep server-certs/server.pfx -p devonly
+openssl pkcs12 -in server-certs/server.pfx -out server-certs/haproxy.pem -nodes -passin pass:devonly
+docker compose up -d
+```
+
+For **fully automatic** Let's Encrypt certificates the same folder ships an ACME variant
+(`docker-compose.acme.yml`): HAProxy answers `http-01` challenges via the janeczku Lua plugin, a
+certbot companion issues/renews the certificate, and the entrypoint reloads gracefully when the
+PEM changes. Requires publicly reachable domains + ports 80/443 β see
+[`samples/haproxy/README.md`](../samples/haproxy/README.md).
+
+Caddy is the only proxy with certificate management built in; the HAProxy ACME variant automates
+it as well. SCDMS always validates the proxy's *public* certificate β for a fully green local
+data-path test use the trusted-dev recipe (Β§5). Full details:
+[`samples/yarp/README.md`](../samples/yarp/README.md) and
+[`samples/haproxy/README.md`](../samples/haproxy/README.md).
+
+---
+
+## 4. Option B β .NET Aspire (SCDMS.Aspire.Hosting)
+
+The AppHost example in
+[`examples/Aspire/SCDMS.AppHost`](../examples/Aspire/SCDMS.AppHost/) is backed by the
+**`SCDMS.Aspire.Hosting`** NuGet package (`AddSharpCoreDB` + `AddSCDMS` + `WithGrpcReference`).
+You can use the same package in your own Aspire app.
+
+### 4.1 Prepare images & certificate
+
+```bash
+cd /SCDMS
+
+# 1) SCDMS image (until the official image is published)
+docker build -t ghcr.io/mpcoredeveloper/scdms:latest .
+
+# 2) Pull the published server image
+docker pull ghcr.io/mpcoredeveloper/sharpcoredb-server:2.0.0.2
+
+# 3) Development certificate for the server container
+cd examples/Aspire/SCDMS.AppHost
+mkdir -p certs
+dotnet dev-certs https -ep certs/server.pfx -p devonly
+```
+
+The AppHost automatically mounts `certs/server.pfx` when present and points
+`Server__Security__TlsCertificatePath` at it. It also uses a dev JWT secret β override with the
+`SCDMS_SERVER_JWT_SECRET` environment variable if you want your own (β₯ 32 chars).
+
+### 4.2 Run the AppHost
+
+```bash
+dotnet run # from examples/Aspire/SCDMS.AppHost
+```
+
+The Aspire dashboard opens in the browser. Expect:
+
+- Two resources: **`db`** (SharpCoreDB server; endpoints `grpc`, `https`) and **`admin`**
+ (SCDMS; endpoint `http`), both **Running**.
+- Click `admin` β open the **http** endpoint to see the SCDMS web UI.
+- SCDMS has received the gRPC link as environment variables. Verify:
+
+```bash
+# find the SCDMS container created by Aspire
+docker ps --format '{{.Names}}\t{{.Image}}' | grep scdms
+docker inspect --format '{{range .Config.Env}}{{println .}}{{end}}' | grep '^SCDMS__'
+```
+
+You should see `SCDMS__DefaultServerHost`, `SCDMS__DefaultServerPort`,
+`SCDMS__DefaultServerUseSsl=true`, `SCDMS__DefaultServerAutoConnect=true` plus the container
+defaults (`SCDMS__EnableHttps=false`, `SCDMS__DataDirectory=/app/data`, β¦).
+
+> With the self-signed dev certificate, SCDMS inside the container cannot validate the server
+> certificate, so the *automatic* gRPC connect may fail in this pure container dev setup. The
+> wiring is correct (see the environment variables above); use Β§5 for a green data-path test.
+
+### 4.3 Stop
+
+Stop the AppHost with `Ctrl+C` in the terminal. Containers are removed automatically
+(`docker ps` afterwards should no longer list them).
+
+---
+
+## 5. Verify the wiring end-to-end (recommended local recipe)
+
+This gives a **green** data-path test without needing a public domain: the server runs in the
+published container, SCDMS runs on your machine and validates the server's dev certificate
+because you trust it on the host.
+
+> Run the commands below from `samples/docker/` (where Β§3.2 created `server-certs/server.pfx`),
+> or change the `-v` source so it points at whichever folder holds your PFX (e.g. the AppHost
+> `certs/` directory from Β§4.1).
+
+```bash
+# 1) Server container on host ports, using the dev PFX from Β§3.2/Β§4.1
+docker run -d --name scdb-test \
+ -p 5001:5001 -p 8443:8443 \
+ -e Server__Security__JwtSecretKey="some-random-secret-of-at-least-32-chars!" \
+ -e Server__Security__TlsCertificatePath=/certs/server.pfx \
+ -e Server__SystemDatabases__Enabled=true \
+ -v "$(pwd)/server-certs:/certs:ro" \
+ ghcr.io/mpcoredeveloper/sharpcoredb-server:2.0.0.2
+
+# 2) Health check the server
+curl -fsk https://localhost:8443/api/v1/health
+
+# 3) Trust the dev certificate on the host (once)
+dotnet dev-certs https --trust
+```
+
+Then start SCDMS from source with the default-server settings pointing at the container:
+
+```bash
+# PowerShell
+$env:SCDMS__DefaultServerHost="localhost"
+$env:SCDMS__DefaultServerPort="5001"
+$env:SCDMS__DefaultServerAutoConnect="true"
+dotnet run --project src/SCDMS/SCDMS.csproj
+```
+
+```bash
+# bash
+export SCDMS__DefaultServerHost=localhost
+export SCDMS__DefaultServerPort=5001
+export SCDMS__DefaultServerAutoConnect=true
+dotnet run --project src/SCDMS/SCDMS.csproj
+```
+
+Open `https://localhost:5443` (accept SCDMS's own self-signed UI certificate once):
+
+- The status bar shows a **connected** server session (`localhost:5001/master`).
+- The sidebar lists the **`master`** database.
+- Run `SELECT * FROM ...` in the SQL editor to confirm gRPC query execution.
+
+Clean up when done: `docker rm -f scdb-test`.
+
+---
+
+## 6. Use `SCDMS.Aspire.Hosting` in your own AppHost
+
+Once published, add the package to your Aspire host:
+
+```bash
+dotnet add package SCDMS.Aspire.Hosting
+```
+
+```csharp
+using Scdms.Aspire.Hosting;
+using SharpCoreDB.Aspire.Hosting;
+
+var builder = DistributedApplication.CreateBuilder(args);
+
+var db = builder.AddSharpCoreDB("db")
+ .WithServerContainer()
+ .WithJwtSecret("your-random-secret-of-at-least-32-chars");
+
+builder.AddSCDMS("admin", db); // SCDMS container auto-wired to the server over gRPC
+
+builder.Build().Run();
+```
+
+`AddSCDMS` without the `db` argument starts SCDMS standalone (no default server). Both overloads
+accept an optional `imageTag` and a fixed host `port` for the HTTP endpoint.
+
+---
+
+## 7. Troubleshooting
+
+| Symptom | Likely cause | Fix |
+|---|---|---|
+| `sharpcoredb` restarts / never healthy | No certificate or no JWT secret | Check `docker compose logs sharpcoredb`. Generate the PFX (`dotnet dev-certs https -ep server-certs/server.pfx -p devonly`), keep `SERVER_JWT_SECRET` β₯ 32 chars. If the PFX cannot be loaded, see the server certificate docs (password/format). |
+| `scdms` unhealthy | Wrong container env or data volume permissions | Check `docker compose logs scdms`. Required: `SCDMS__EnableHttps=false`, `SCDMS__BindAddress=0.0.0.0`, writable `/app/data` (container runs as uid 1000). |
+| Caddy logs certificate errors | Domain does not resolve publicly, or ports 80/443 blocked | Use real public DNS + open ports for Let's Encrypt, or switch to the local `localhost` smoke test (Β§3.4). |
+| SCDMS UI opens but shows a server connection error | SCDMS cannot validate a self-signed server certificate | Expected with self-signed dev certs. Use a public-domain proxy (Option A) or the trusted local recipe (Β§5). |
+| Aspire resources run but SCDMS does not connect | Same certificate validation + wrong `SCDMS__DefaultServer*` | Verify env via `docker inspect` (Β§4.2); for a green connect use Β§5. |
+| `dotnet` uses the wrong SDK version | Machine default is not .NET 10 | The repo's `global.json` requires a .NET 10 SDK (10.0.400). Install it; VS Code/`dotnet` pick it up automatically. |
+| `docker compose`/Aspire says image not found for `scdms` | Official SCDMS image not published yet | Build it locally: `docker build -t ghcr.io/mpcoredeveloper/scdms:latest .` |
+
+---
+
+## 8. Related documentation
+
+- [`docs/aspire.md`](aspire.md) β .NET Aspire design & status (issue #10)
+- [`docs/usage.md`](usage.md) β SCDMS configuration, environment variables, connection modes
+- [`samples/docker/README.md`](../samples/docker/README.md) β Compose sample notes (Caddy)
+- [`samples/yarp/README.md`](../samples/yarp/README.md) β Compose sample notes (YARP, all-.NET)
+- [`samples/haproxy/README.md`](../samples/haproxy/README.md) β Compose sample notes (HAProxy, industry standard)
+- [`examples/Aspire/SCDMS.AppHost/README.md`](../examples/Aspire/SCDMS.AppHost/README.md) β AppHost example notes
+- SharpCoreDB side: [`ASPIRE_INTEGRATION.md`](https://github.com/MPCoreDeveloper/SharpCoreDB/blob/master/docs/server/ASPIRE_INTEGRATION.md) and [`QUICKSTART.md`](https://github.com/MPCoreDeveloper/SharpCoreDB/blob/master/docs/server/QUICKSTART.md)
+
+
+
diff --git a/docs/usage.md b/docs/usage.md
index f9237b1..0cacf6b 100644
--- a/docs/usage.md
+++ b/docs/usage.md
@@ -83,6 +83,10 @@ SCDMS validates the public certificate, so no custom CA or certificate-skip logi
Mount a volume at the `SCDMS__DataDirectory` location (`/app/data` in the official image) to persist settings, saved queries/history and built-in databases across container restarts.
+### .NET Aspire integration
+
+A `SCDMS.Aspire.Hosting` NuGet package plus a runnable [AppHost example](../examples/Aspire/SCDMS.AppHost/) run SharpCoreDB server + SCDMS as one .NET Aspire application (all SCDMS β SharpCoreDB traffic over gRPC). Design, status and the TLS-in-development notes: [docs/aspire.md](aspire.md).
+
## Security posture
SCDMS is configured secure-by-default:
diff --git a/examples/Aspire/SCDMS.AppHost/Program.cs b/examples/Aspire/SCDMS.AppHost/Program.cs
new file mode 100644
index 0000000..4aace16
--- /dev/null
+++ b/examples/Aspire/SCDMS.AppHost/Program.cs
@@ -0,0 +1,47 @@
+using System.Security.Cryptography;
+using Aspire.Hosting;
+using Scdms.Aspire.Hosting;
+using SharpCoreDB.Aspire.Hosting;
+
+var builder = DistributedApplication.CreateBuilder(args);
+
+// JWT secret for the dev server (min. 32 chars). Override with SCDMS_SERVER_JWT_SECRET to keep
+// it stable across runs; otherwise a random per-run secret is generated (dev only - JWT tokens
+// do not survive restarts).
+var jwtSecret = builder.Configuration["SCDMS_SERVER_JWT_SECRET"];
+if (string.IsNullOrWhiteSpace(jwtSecret))
+{
+ jwtSecret = Convert.ToHexString(RandomNumberGenerator.GetBytes(32));
+}
+
+// SharpCoreDB network server container (HTTPS gRPC on container port 5001, HTTPS REST API on
+// container port 8443) from ghcr.io/mpcoredeveloper/sharpcoredb-server. Pin a specific image
+// tag with the SCDB_IMAGE_TAG environment variable; defaults to the published "latest" tag.
+var sharpCoreDb = builder.AddSharpCoreDB("db")
+ .WithServerContainer()
+ .WithJwtSecret(jwtSecret);
+
+if (builder.Configuration["SCDB_IMAGE_TAG"] is { Length: > 0 } serverImageTag)
+{
+ sharpCoreDb = sharpCoreDb.WithImageTag(serverImageTag);
+}
+
+// Optional local development certificate. Drop certs/server.pfx next to this project
+// (generate with: dotnet dev-certs https -ep certs/server.pfx -p devonly) and the server
+// container mounts and uses it. The server only speaks TLS and will not start without a
+// certificate. See README.md for the TLS notes.
+var certDirectory = Path.Combine(Environment.CurrentDirectory, "certs");
+var certFile = Path.Combine(certDirectory, "server.pfx");
+if (Directory.Exists(certDirectory) && File.Exists(certFile))
+{
+ sharpCoreDb
+ .WithBindMount(certDirectory, "/app/certs", isReadOnly: true)
+ .WithEnvironment("Server__Security__TlsCertificatePath", "/app/certs/server.pfx");
+}
+
+// SCDMS web studio container (plain HTTP on container port 8080), linked to the server over gRPC
+// through the SCDMS__DefaultServer* environment variables (see ScdmsAspireExtensions).
+builder.AddSCDMS("admin", sharpCoreDb);
+
+await builder.Build().RunAsync();
+
diff --git a/examples/Aspire/SCDMS.AppHost/README.md b/examples/Aspire/SCDMS.AppHost/README.md
new file mode 100644
index 0000000..f0a9888
--- /dev/null
+++ b/examples/Aspire/SCDMS.AppHost/README.md
@@ -0,0 +1,70 @@
+# SCDMS + SharpCoreDB β .NET Aspire sample (gRPC)
+
+This Aspire host runs SharpCoreDB server and SCDMS as one application, the same topology as the
+production Docker Compose sample in [`samples/docker/`](../../../samples/docker/README.md):
+
+- **SharpCoreDB server** container (`ghcr.io/mpcoredeveloper/sharpcoredb-server`) β HTTPS gRPC
+ on container port 5001, HTTPS REST API on container port 8443.
+- **SCDMS** web studio container (`ghcr.io/mpcoredeveloper/scdms`) β plain HTTP on container
+ port 8080, auto-wired to the server over **gRPC**.
+
+All SCDMS β SharpCoreDB data traffic flows over gRPC.
+
+## Prerequisites
+
+- .NET 10 SDK and Docker (with a running Docker engine).
+- SharpCoreDB server image: published (`ghcr.io/mpcoredeveloper/sharpcoredb-server`).
+- SCDMS image: defaults to `ghcr.io/mpcoredeveloper/scdms:latest`. Until that image is
+ published, build it locally from the repository root:
+
+ ```bash
+ docker build -t ghcr.io/mpcoredeveloper/scdms:latest .
+ ```
+
+## Run
+
+The SharpCoreDB server only speaks TLS and requires a certificate. For a local run, generate a
+development certificate and drop it next to this project (`certs/server.pfx`); the AppHost
+mounts it automatically when present:
+
+```bash
+# from the SCDMS.AppHost folder so ./certs resolves:
+cd examples/Aspire/SCDMS.AppHost
+dotnet dev-certs https -ep certs/server.pfx -p devonly
+dotnet run
+```
+
+Optional overrides:
+
+```bash
+# different JWT secret for the dev server (min. 32 characters)
+SCDMS_SERVER_JWT_SECRET="some-random-32-char-secret!" dotnet run
+
+# pin a specific SharpCoreDB server image tag instead of "latest"
+SCDB_IMAGE_TAG="2.0.0.2" dotnet run
+```
+
+The Aspire dashboard opens automatically; the resources (`db`, `admin`) and their endpoints are
+listed there. Open the SCDMS **http** endpoint to reach the web studio.
+
+> Note: with the self-signed `dotnet dev-certs` certificate, the *browser* will warn about the
+> SCDMS endpoint certificate **only if** you open the dashboard/SCDMS over TLS. SCDMS itself
+> serves plain HTTP inside the container. Because SCDMS validates the server certificate, an
+> end-to-end auto-connect over gRPC in this container-only dev setup requires a certificate that
+> is trusted by the SCDMS container β see the TLS section below.
+
+## TLS & production notes
+
+- In **production**, terminate TLS at a reverse proxy holding a publicly trusted certificate
+ (Let's Encrypt) and let SCDMS reach the gRPC endpoint through that proxy β exactly what the
+ [`samples/docker/`](../../../samples/docker/README.md) compose sample does.
+- The SharpCoreDB server always enforces TLS 1.2+ and refuses to start without a certificate
+ and a JWT secret (`WithJwtSecret`). For local development you can mount a `dotnet dev-certs`
+ PFX as shown above; see the SharpCoreDB
+ [`ASPIRE_INTEGRATION.md`](https://github.com/MPCoreDeveloper/SharpCoreDB/blob/master/docs/server/ASPIRE_INTEGRATION.md)
+ for the full server-side development-certificate story.
+- Persist SCDMS state by mounting a volume on `/app/data`
+ (`scdms.WithVolume(...)`/`WithBindMount(...)`); by default state is ephemeral.
+
+A full end-to-end test guide (both Docker Compose and Aspire) lives in
+[`docs/container-and-aspire-guide.md`](../../../docs/container-and-aspire-guide.md).
diff --git a/examples/Aspire/SCDMS.AppHost/SCDMS.AppHost.csproj b/examples/Aspire/SCDMS.AppHost/SCDMS.AppHost.csproj
new file mode 100644
index 0000000..801d75a
--- /dev/null
+++ b/examples/Aspire/SCDMS.AppHost/SCDMS.AppHost.csproj
@@ -0,0 +1,39 @@
+
+
+
+ false
+
+
+
+
+
+ Exe
+ net10.0
+ 14.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $(NoWarn);ASPIRE010
+
+
+
diff --git a/global.json b/global.json
new file mode 100644
index 0000000..5269c28
--- /dev/null
+++ b/global.json
@@ -0,0 +1,7 @@
+{
+ "sdk": {
+ "version": "10.0.400",
+ "rollForward": "latestFeature",
+ "allowPrerelease": false
+ }
+}
diff --git a/samples/docker/README.md b/samples/docker/README.md
index 1345bce..552632d 100644
--- a/samples/docker/README.md
+++ b/samples/docker/README.md
@@ -23,11 +23,11 @@ to the server's internal certificate, so no custom CA is needed inside SCDMS.
The compose file references published images by default:
-- `ghcr.io/mpcoredeveloper/scdms:latest`
-- `ghcr.io/mpcoredeveloper/sharpcoredb-server:latest`
+- `ghcr.io/mpcoredeveloper/scdms:latest` β built by the `docker-publish.yml` workflow on every
+ `v*` tag. Until it is published, build it locally (see below).
+- `ghcr.io/mpcoredeveloper/sharpcoredb-server:latest` β **published** (built on every `v*` tag).
-Until those images are published (the `docker-publish.yml` workflow builds SCDMS on a
-`v*` tag), build them locally by uncommenting the `build:` blocks in `docker-compose.yml`:
+To build the SCDMS image locally, uncomment the SCDMS `build:` block in `docker-compose.yml`:
```yaml
# for scdms
@@ -35,7 +35,7 @@ build:
context: ../..
dockerfile: Dockerfile
-# for sharpcoredb
+# for sharpcoredb (optional: only needed when you want to build the server image yourself)
build:
context: /path/to/SharpCoreDB
dockerfile: src/SharpCoreDB.Server/Dockerfile
diff --git a/samples/docker/docker-compose.yml b/samples/docker/docker-compose.yml
index 506a5b3..bb0a006 100644
--- a/samples/docker/docker-compose.yml
+++ b/samples/docker/docker-compose.yml
@@ -13,9 +13,9 @@
# docker compose up -d
#
# Prerequisites:
-# - Published images ghcr.io/mpcoredeveloper/scdms and
-# ghcr.io/mpcoredeveloper/sharpcoredb-server. Until they are published you can
-# build them locally (see the commented `build:` blocks below).
+# - Published image ghcr.io/mpcoredeveloper/sharpcoredb-server (available).
+# - SCDMS image ghcr.io/mpcoredeveloper/scdms: published on v* tags; until then build it
+# locally by uncommenting the scdms `build:` block below.
services:
caddy:
@@ -66,7 +66,8 @@ services:
sharpcoredb:
image: ghcr.io/mpcoredeveloper/sharpcoredb-server:latest
- # Until the image is published, build it from a local SharpCoreDB checkout:
+ # The image is published on every v* tag. To build it from a local SharpCoreDB checkout,
+ # uncomment:
# build:
# context: /path/to/SharpCoreDB
# dockerfile: src/SharpCoreDB.Server/Dockerfile
diff --git a/samples/haproxy/.env.example b/samples/haproxy/.env.example
new file mode 100644
index 0000000..900ec4d
--- /dev/null
+++ b/samples/haproxy/.env.example
@@ -0,0 +1,31 @@
+# ββ Proxy hostnames ββ
+# For local testing use localhost for both (one dev certificate covers both).
+# For production use the real public hostnames; the PEM must cover both names.
+SCDMS_DOMAIN=scdms.example.com
+GRPC_DOMAIN=scdb.example.com
+
+# ββ Automatic certificates (ACME variant only) ββ
+# docker-compose.acme.yml obtains/renews a Let's Encrypt certificate automatically. The domains
+# above must resolve publicly to this host and ports 80/443 must be reachable.
+# e.g. ACME_EMAIL=admin@example.com
+ACME_EMAIL=
+
+# ββ HAProxy TLS ββ
+# HAProxy terminates TLS for the browser UI and the SCDMS gRPC client and needs a PEM file
+# (private key + certificate [+ chain]). For local testing, generate the dev PFX and convert
+# it to PEM:
+# mkdir -p server-certs
+# dotnet dev-certs https -ep server-certs/server.pfx -p devonly
+# openssl pkcs12 -in server-certs/server.pfx -out server-certs/haproxy.pem -nodes -passin pass:devonly
+# (Windows without openssl: use Git Bash, WSL, or any openssl binary.)
+# Production: place your public certificate (e.g. certbot fullchain.pem + privkey.pem) at
+# server-certs/haproxy.pem β or use docker-compose.acme.yml for fully automatic issuance.
+
+# ββ SharpCoreDB server credentials ββ
+# The SharpCoreDB server uses its own (internal) certificate from the same server-certs folder.
+SERVER_TLS_CERT_PATH=/app/certs/server.pfx
+SERVER_JWT_SECRET=replace-with-a-random-32-char-secret
+
+# ββ SCDMS default-server login used against the SharpCoreDB server ββ
+SDB_USERNAME=anonymous
+SDB_PASSWORD=
diff --git a/samples/haproxy/.gitignore b/samples/haproxy/.gitignore
new file mode 100644
index 0000000..af24271
--- /dev/null
+++ b/samples/haproxy/.gitignore
@@ -0,0 +1,4 @@
+# Runtime state for the ACME variant (certificates, webroot tokens, certbot account state).
+# Never commit these.
+acme-state/
+server-certs/
diff --git a/samples/haproxy/README.md b/samples/haproxy/README.md
new file mode 100644
index 0000000..98240a4
--- /dev/null
+++ b/samples/haproxy/README.md
@@ -0,0 +1,136 @@
+# SCDMS + SharpCoreDB β HAProxy reverse proxy sample
+
+HAProxy is the battle-tested, industry-standard load balancer / reverse proxy. This sample
+runs the same topology as [`samples/docker/`](../docker/) and [`samples/yarp/`](../yarp/) but
+terminates TLS with **HAProxy** instead of Caddy/YARP:
+
+```text
+Browser ββHTTPS (cert on HAProxy)βββΊ haproxy ββHTTPβββΊ scdms:8080 (SCDMS web UI)
+SCDMS ββgRPC https://βββΊ haproxy ββgRPC/TLSβββΊ sharpcoredb:5001
+```
+
+All SCDMS β SharpCoreDB data traffic flows over **gRPC**. The proxy terminates TLS for both the
+SCDMS web UI and the SCDMSβserver gRPC channel, then re-encrypts to the SharpCoreDB server's
+internal certificate (`ssl verify none`, mirroring Caddy's `tls_insecure_skip_verify`).
+
+## Caddy vs. YARP vs. HAProxy
+
+| | Caddy (`samples/docker/`) | YARP (`samples/yarp/`) | HAProxy (this folder) |
+|---|---|---|---|
+| Proxy | `caddy:2.9` official image | Your .NET image (`Yarp.ReverseProxy`) | `haproxy:3.0` official image |
+| TLS certificates | **Automatic** Let's Encrypt | Manual PFX | Manual **PEM** |
+| Config | Caddyfile | C# code | `haproxy.cfg` (templated) |
+| Language | Go | 100% .NET/C# | C |
+| Sweet spot | Zero-maintenance auto-TLS | Everything in .NET | Industry-standard proxy/LB, huge ecosystem |
+
+Choose HAProxy when you (or your ops team) already standardize on it β it is the most widely
+deployed option and supports advanced LB features (health checks, stickiness, routing policies).
+
+## Quick start
+
+1. Make the images available:
+
+ ```bash
+ cd /SCDMS
+ docker build -t ghcr.io/mpcoredeveloper/scdms:latest . # until the official image is published
+ docker pull ghcr.io/mpcoredeveloper/sharpcoredb-server:2.0.0.2
+ ```
+
+2. Configure and create the TLS certificate. HAProxy needs a **PEM** (key + certificate);
+ for local testing convert the dev PFX:
+
+ ```bash
+ cd samples/haproxy
+ cp .env.example .env # PowerShell: Copy-Item .env.example .env
+ mkdir -p server-certs
+ dotnet dev-certs https -ep server-certs/server.pfx -p devonly
+ openssl pkcs12 -in server-certs/server.pfx -out server-certs/haproxy.pem -nodes -passin pass:devonly
+ ```
+
+ Edit `.env`: set `SCDMS_DOMAIN=localhost` and `GRPC_DOMAIN=localhost` for local testing, or
+ your real public hostnames in production (the PEM must cover both hostnames).
+
+3. Start everything:
+
+ ```bash
+ docker compose up -d
+ docker compose ps # haproxy, scdms, sharpcoredb all "healthy"
+ ```
+
+## Verify
+
+```bash
+# HAProxy (stats socket, socat)
+docker compose exec haproxy sh -c "echo 'show info' | socat unix-connect:/var/lib/haproxy/admin.sock stdio" | head
+
+# SCDMS health (through the proxy)
+docker compose exec scdms curl -fs http://localhost:8080/health
+
+# SharpCoreDB server health (internal self-signed cert = -k)
+docker compose exec sharpcoredb curl -fsk https://localhost:8443/api/v1/health
+
+# Logs
+docker compose logs -f haproxy scdms
+```
+
+- Browser: `https://` (accept the self-signed warning when using `localhost`).
+- The HAProxy log lines show the SNI-based backend selection (`scdms_ui` / `scdb_grpc`).
+
+## TLS notes
+
+- **SCDMS validates the certificate of the gRPC endpoint it connects to.** In production the
+ mounted PEM must be publicly trusted (as with any proxy). With a self-signed `localhost` cert
+ the browser warns and SCDMS's automatic gRPC connect cannot validate the proxy cert β the same
+ TLS caveat as the other samples (see
+ [`docs/container-and-aspire-guide.md`](../../docs/container-and-aspire-guide.md), Β§5 for a
+ green local data-path test).
+- The proxy skips certificate validation only towards the *internal* SharpCoreDB server
+ (`ssl verify none`). That switch never affects what SCDMS validates.
+- The container runs as the non-root `haproxy` user; binding 80/443 needs the
+ `NET_BIND_SERVICE` capability (added in `docker-compose.yml`).
+
+## Fully automatic certificates (ACME variant)
+
+`docker-compose.acme.yml` runs the same stack but manages the HAProxy certificate
+**automatically** with Let's Encrypt β no manual PEM, no manual renewal:
+
+- **HAProxy** (custom image `./acme`, based on `haproxy:3.0` + `openssl`) loads the
+ [janeczku HAProxy ACME Lua plugin](https://github.com/janeczku/haproxy-acme-validation-plugin)
+ (`./acme/acme-http01-webroot.lua`) which answers `http-01` challenges on port 80 from the
+ shared `/webroot` folder. Its entrypoint creates a temporary self-signed bootstrap cert when
+ none exists yet and **gracefully reloads (SIGUSR2)** whenever the PEM file changes.
+- **Certbot companion** (`certbot/certbot`) issues/renews the certificate every 12 h for
+ `SCDMS_DOMAIN` + `GRPC_DOMAIN` (webroot method). After every successful issue/renewal its
+ deploy hook (`./acme/certbot-deploy.sh`) combines `privkey.pem` + `fullchain.pem` into
+ `/certs/haproxy.pem`.
+
+Requirements: `SCDMS_DOMAIN` and `GRPC_DOMAIN` must **resolve publicly** to this host and ports
+`80`/`443` must be reachable (that is how Let's Encrypt validates ownership). Set `ACME_EMAIL`
+in `.env`. Local `localhost` testing still uses the manual compose file above.
+
+```bash
+cd samples/haproxy
+cp .env.example .env # set real domains + ACME_EMAIL + secrets
+mkdir -p server-certs # dev PFX for the SharpCoreDB server's internal TLS
+dotnet dev-certs https -ep server-certs/server.pfx -p devonly
+docker compose -f docker-compose.acme.yml up -d --build
+```
+
+Check the flow:
+
+```bash
+docker compose -f docker-compose.acme.yml ps # haproxy, certbot, scdms, sharpcoredb
+docker compose -f docker-compose.acme.yml logs -f certbot # issuance/renewal activity
+docker compose -f docker-compose.acme.yml logs haproxy # "[entrypoint] certificate changed - graceful reload"
+# after issuance, the real certificate is live on https:// and https://
+```
+
+State (live `haproxy.pem`, challenge webroot, certbot account) lives in the Docker named volumes
+`haproxy-certs`, `haproxy-webroot` and `letsencrypt`. Reset with
+`docker compose -f docker-compose.acme.yml down -v`.
+
+## Stop
+
+```bash
+docker compose down # add -v to also delete the data volumes
+```
diff --git a/samples/haproxy/acme/Dockerfile b/samples/haproxy/acme/Dockerfile
new file mode 100644
index 0000000..9c280ad
--- /dev/null
+++ b/samples/haproxy/acme/Dockerfile
@@ -0,0 +1,19 @@
+# ββ HAProxy ACME variant runtime image ββ
+# Same as the official haproxy:3.0 image plus the openssl CLI used by entrypoint.sh to create
+# the temporary bootstrap certificate. The Lua plugin, config template and scripts are mounted
+# read-only by docker-compose.acme.yml.
+#
+# The container keeps the official image's non-root 'haproxy' user. /certs and /webroot are
+# pre-created here so that Docker named volumes (see docker-compose.acme.yml) inherit their
+# ownership. Binding 80/443 requires the NET_BIND_SERVICE capability (cap_add in compose).
+
+FROM haproxy:3.0
+
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends openssl \
+ && rm -rf /var/lib/apt/lists/* \
+ && mkdir -p /certs /webroot \
+ && chown haproxy:haproxy /certs /webroot
+
+ENTRYPOINT ["/bin/sh", "/usr/local/etc/haproxy/acme-entrypoint.sh"]
+
diff --git a/samples/haproxy/acme/acme-http01-webroot.lua b/samples/haproxy/acme/acme-http01-webroot.lua
new file mode 100644
index 0000000..71ba424
--- /dev/null
+++ b/samples/haproxy/acme/acme-http01-webroot.lua
@@ -0,0 +1,91 @@
+-- ACME http-01 domain validation plugin for HAProxy 1.6+
+-- Upstream: https://github.com/janeczku/haproxy-acme-validation-plugin
+-- copyright (C) 2015 Jan Broer (MIT License)
+--
+-- SCDMS sample: vendored copy with the webroot path pinned to /webroot (shared volume with the
+-- certbot companion container). The upstream file is configured by setting
+-- acme.conf.non_chroot_webroot below; no other changes were made.
+
+acme = {}
+acme.version = "0.1.1"
+
+--
+-- Configuration
+--
+-- When HAProxy is *not* configured with the 'chroot' option you must set an absolute path here and pass
+-- that as 'webroot-path' to the letsencrypt client
+
+acme.conf = {
+ ["non_chroot_webroot"] = "/webroot"
+}
+
+--
+-- Startup
+--
+acme.startup = function()
+ core.Info("[acme] http-01 plugin v" .. acme.version);
+end
+
+--
+-- ACME http-01 validation endpoint
+--
+acme.http01 = function(applet)
+ local response = ""
+ local reqPath = applet.path
+ local src = applet.sf:src()
+ local token = reqPath:match( ".+/(.*)$" )
+
+ if token then
+ token = sanitizeToken(token)
+ end
+
+ if (token == nil or token == '') then
+ response = "bad request\n"
+ applet:set_status(400)
+ core.Warning("[acme] malformed request (client-ip: " .. tostring(src) .. ")")
+ else
+ auth = getKeyAuth(token)
+ if (auth:len() >= 1) then
+ response = auth .. "\n"
+ applet:set_status(200)
+ core.Info("[acme] served http-01 token: " .. token .. " (client-ip: " .. tostring(src) .. ")")
+ else
+ response = "resource not found\n"
+ applet:set_status(404)
+ core.Warning("[acme] http-01 token not found: " .. token .. " (client-ip: " .. tostring(src) .. ")")
+ end
+ end
+
+ applet:add_header("Server", "haproxy/acme-http01-authenticator")
+ applet:add_header("Content-Length", string.len(response))
+ applet:add_header("Content-Type", "text/plain")
+ applet:start_response()
+ applet:send(response)
+end
+
+--
+-- strip chars that are not in the URL-safe Base64 alphabet
+-- see https://github.com/letsencrypt/acme-spec/blob/master/draft-barnes-acme.md
+--
+function sanitizeToken(token)
+ _strip="[^%a%d%+%-%_=]"
+ token = token:gsub(_strip,'')
+ return token
+end
+
+--
+-- get key auth from token file
+--
+function getKeyAuth(token)
+ local keyAuth = ""
+ local path = acme.conf.non_chroot_webroot .. "/.well-known/acme-challenge/" .. token
+ local f = io.open(path, "rb")
+ if f ~= nil then
+ keyAuth = f:read("*all")
+ f:close()
+ end
+ return keyAuth
+end
+
+core.register_init(acme.startup)
+core.register_service("acme-http01", "http", acme.http01)
diff --git a/samples/haproxy/acme/certbot-deploy.sh b/samples/haproxy/acme/certbot-deploy.sh
new file mode 100644
index 0000000..83f5a1e
--- /dev/null
+++ b/samples/haproxy/acme/certbot-deploy.sh
@@ -0,0 +1,11 @@
+#!/bin/sh
+# Certbot deploy hook (called after every successful issue/renewal).
+# Combines the private key + full chain into the PEM that HAProxy uses (/certs/haproxy.pem).
+# HAProxy does not need a signal from here: its entrypoint watches the PEM and triggers a
+# graceful reload (SIGUSR2) when the file changes.
+set -eu
+
+: "${RENEWED_LINEAGE:?RENEWED_LINEAGE is not set (certbot deploy hook context)}"
+
+cat "$RENEWED_LINEAGE/privkey.pem" "$RENEWED_LINEAGE/fullchain.pem" > /certs/haproxy.pem
+echo "[certbot deploy] wrote /certs/haproxy.pem from $RENEWED_LINEAGE"
diff --git a/samples/haproxy/acme/certbot-entrypoint.sh b/samples/haproxy/acme/certbot-entrypoint.sh
new file mode 100644
index 0000000..55aca1a
--- /dev/null
+++ b/samples/haproxy/acme/certbot-entrypoint.sh
@@ -0,0 +1,32 @@
+#!/bin/sh
+# Certbot companion loop for the HAProxy ACME variant:
+# - issues/renews certificates for SCDMS_DOMAIN and GRPC_DOMAIN via http-01 (webroot),
+# - runs the deploy hook (/bin/sh /hooks/certbot-deploy.sh) after every successful
+# issue/renewal; the hook writes the combined HAProxy PEM,
+# - retries every 12 hours (harmless while certificates are still valid thanks to
+# --keep-until-expiring).
+set -eu
+
+: "${ACME_EMAIL:?ACME_EMAIL is required (set it in samples/haproxy/.env)}"
+: "${SCDMS_DOMAIN:?SCDMS_DOMAIN is required}"
+: "${GRPC_DOMAIN:?GRPC_DOMAIN is required}"
+
+WEBROOT=/webroot
+mkdir -p "$WEBROOT"
+
+echo "[certbot] starting companion for ${SCDMS_DOMAIN} + ${GRPC_DOMAIN}"
+
+while true; do
+ echo "[certbot] requesting certificates (http-01 via haproxy on :80)"
+ certbot certonly \
+ --webroot -w "$WEBROOT" \
+ -d "$SCDMS_DOMAIN" -d "$GRPC_DOMAIN" \
+ --non-interactive \
+ --agree-tos \
+ --email "$ACME_EMAIL" \
+ --keep-until-expiring \
+ --deploy-hook "/bin/sh /hooks/certbot-deploy.sh" \
+ || echo "[certbot] attempt failed - retrying in 12h (domains must resolve to this host and :80 must be reachable)"
+
+ sleep 12h
+done
diff --git a/samples/haproxy/acme/entrypoint.sh b/samples/haproxy/acme/entrypoint.sh
new file mode 100644
index 0000000..2029152
--- /dev/null
+++ b/samples/haproxy/acme/entrypoint.sh
@@ -0,0 +1,57 @@
+#!/bin/sh
+# HAProxy entrypoint for the ACME variant:
+# 1) renders haproxy.cfg.tmpl (__SCDMS_DOMAIN__ / __GRPC_DOMAIN__) to /tmp/haproxy.cfg
+# 2) creates a temporary self-signed certificate if /certs/haproxy.pem does not exist yet
+# (HAProxy needs a cert to boot; the certbot companion replaces it with a real one)
+# 3) starts HAProxy in master-worker mode (-W -db) and watches the PEM file: on change it
+# sends SIGUSR2 (graceful reload re-reading the certificate) β no downtime.
+set -eu
+
+SCDMS_DOMAIN="${SCDMS_DOMAIN:-scdms.example.com}"
+GRPC_DOMAIN="${GRPC_DOMAIN:-scdb.example.com}"
+CERT_PATH="${HAPROXY_PEM_PATH:-/certs/haproxy.pem}"
+CHECK_INTERVAL="${HAPROXY_RELOAD_INTERVAL:-10}"
+
+# 1) render configuration
+sed -e "s/__SCDMS_DOMAIN__/${SCDMS_DOMAIN}/g" \
+ -e "s/__GRPC_DOMAIN__/${GRPC_DOMAIN}/g" \
+ /usr/local/etc/haproxy/haproxy.cfg.tmpl > /tmp/haproxy.cfg
+
+# 2) bootstrap certificate (only when the certbot companion has not written one yet)
+if [ ! -f "$CERT_PATH" ]; then
+ echo "[entrypoint] no certificate at $CERT_PATH - creating a temporary self-signed one (certbot will replace it)."
+ tmpdir="$(mktemp -d)"
+ openssl req -x509 -newkey rsa:2048 -nodes \
+ -keyout "$tmpdir/key.pem" -out "$tmpdir/cert.pem" \
+ -days 30 \
+ -subj "/CN=${SCDMS_DOMAIN}" \
+ -addext "subjectAltName=DNS:${SCDMS_DOMAIN},DNS:${GRPC_DOMAIN}" 2>/dev/null
+ cat "$tmpdir/key.pem" "$tmpdir/cert.pem" > "$CERT_PATH"
+ rm -rf "$tmpdir"
+fi
+
+# 3) start HAProxy (master-worker: SIGUSR2 = graceful reload)
+haproxy -W -db -f /tmp/haproxy.cfg &
+HAPROXY_PID=$!
+
+last_mtime="$(stat -c %Y "$CERT_PATH")"
+
+forward_shutdown() {
+ echo "[entrypoint] shutting down (signal to haproxy pid $HAPROXY_PID)"
+ kill -TERM "$HAPROXY_PID" 2>/dev/null || true
+ exit 0
+}
+trap forward_shutdown TERM INT
+
+while kill -0 "$HAPROXY_PID" 2>/dev/null; do
+ current_mtime="$(stat -c %Y "$CERT_PATH")"
+ if [ "$current_mtime" != "$last_mtime" ]; then
+ echo "[entrypoint] certificate changed - graceful reload (SIGUSR2)"
+ kill -USR2 "$HAPROXY_PID" 2>/dev/null || true
+ last_mtime="$current_mtime"
+ fi
+ sleep "$CHECK_INTERVAL"
+done
+
+echo "[entrypoint] haproxy exited unexpectedly"
+exit 1
diff --git a/samples/haproxy/acme/haproxy.cfg.tmpl b/samples/haproxy/acme/haproxy.cfg.tmpl
new file mode 100644
index 0000000..ce32a2d
--- /dev/null
+++ b/samples/haproxy/acme/haproxy.cfg.tmpl
@@ -0,0 +1,63 @@
+# ββ HAProxy sample (ACME variant): SCDMS web UI + SharpCoreDB gRPC ββββββββββββββ
+# TLS is terminated here. Placeholders __SCDMS_DOMAIN__ / __GRPC_DOMAIN__ are replaced by
+# entrypoint.sh from the SCDMS_DOMAIN / GRPC_DOMAIN environment variables.
+#
+# ACME additions vs. samples/haproxy/haproxy.cfg.tmpl:
+# - global: lua-load of the http-01 validation plugin
+# - http-in: /.well-known/acme-challenge/* is answered by the Lua service so the certbot
+# companion can prove domain ownership on port 80 (zero downtime)
+
+global
+ log stdout format raw local0 notice
+ maxconn 4096
+ # Used by the container HEALTHCHECK (socat "show info").
+ stats socket /var/lib/haproxy/admin.sock level admin mode 600
+ ssl-default-bind-options no-sslv3 no-tlsv10 no-tlsv11
+ ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384
+ ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384
+ # ACME http-01 validation (janeczku/haproxy-acme-validation-plugin, webroot=/webroot).
+ lua-load /usr/local/etc/haproxy/acme-http01-webroot.lua
+
+defaults
+ mode http
+ log global
+ option httplog
+ option dontlognull
+ option http-server-close
+ timeout connect 5s
+ timeout client 60s
+ timeout server 60s
+ timeout tunnel 15m
+
+# ββ HTTP frontend: ACME challenges + container /health + redirect to HTTPS βββββββ
+frontend http-in
+ bind *:80
+ acl url_acme_http01 path_beg /.well-known/acme-challenge/
+ http-request use-service lua.acme-http01 if METH_GET url_acme_http01
+ http-request return status 200 content-type text/plain string "OK" if { path /health }
+ http-request redirect scheme https unless { path /health }
+
+# ββ HTTPS frontend (TLS terminated): HTTP/1.1 (UI) + HTTP/2 (gRPC) ββββββββββββββ
+frontend https-in
+ bind *:443 ssl crt /certs/haproxy.pem alpn h2,http/1.1
+ http-request set-header X-Forwarded-Proto https
+ http-request set-header X-Forwarded-Port 443
+ # Long timeout for long-lived gRPC streams.
+ timeout client 1h
+
+ # Route by Server Name Indication (browser sends SNI for the UI host, the SCDMS gRPC
+ # client sends SNI for the gRPC host).
+ use_backend scdms_ui if { ssl_fc_sni -i __SCDMS_DOMAIN__ }
+ use_backend scdb_grpc if { ssl_fc_sni -i __GRPC_DOMAIN__ }
+ default_backend scdms_ui
+
+# SCDMS web UI β plain HTTP on scdms:8080.
+backend scdms_ui
+ server scdms scdms:8080 check inter 30s fall 3 rise 2
+
+# SharpCoreDB gRPC β HTTPS/HTTP2 upstream. 'ssl verify none' skips the server's internal
+# (self-signed) certificate β the public client still validates HAProxy's certificate
+# (mirror of Caddy's tls_insecure_skip_verify / the YARP sample).
+backend scdb_grpc
+ timeout server 1h
+ server sharpcoredb sharpcoredb:5001 ssl verify none alpn h2 check inter 30s fall 3 rise 2
diff --git a/samples/haproxy/docker-compose.acme.yml b/samples/haproxy/docker-compose.acme.yml
new file mode 100644
index 0000000..06df7ac
--- /dev/null
+++ b/samples/haproxy/docker-compose.acme.yml
@@ -0,0 +1,132 @@
+# SCDMS + SharpCoreDB Server + HAProxy (ACME variant) β fully automatic Let's Encrypt.
+#
+# Same topology as samples/haproxy/docker-compose.yml (manual certificates) but with:
+# - the janeczku HAProxy ACME Lua plugin serving http-01 challenges on :80,
+# - a certbot companion container that issues/renews certificates and writes the combined PEM,
+# - the HAProxy entrypoint reloading gracefully (SIGUSR2) whenever the PEM changes.
+#
+# Usage (requires SCDMS_DOMAIN + GRPC_DOMAIN to resolve publicly to this host, :80/:443 open):
+# cp ../.env.example .env # or set the variables below
+# docker compose -f docker-compose.acme.yml up -d --build
+#
+# TLS flow:
+# Let's Encrypt ββhttp-01 on :80βββΊ haproxy (Lua) ββtoken from /webrootβββΊ certbot writes token
+# certbot issues ββdeploy-hookβββΊ combines key+chain into the haproxy-certs volume
+# haproxy entrypoint detects change ββSIGUSR2βββΊ graceful reload (new cert live)
+#
+# State (certificates, webroot, certbot account) lives in Docker named volumes:
+# haproxy-certs, haproxy-webroot, letsencrypt (see bottom). Reset with `down -v`.
+
+services:
+ haproxy:
+ build: ./acme
+ image: scdms-haproxy-acme:local
+ restart: unless-stopped
+ # The container runs as the non-root 'haproxy' user; binding 80/443 needs this capability.
+ cap_add:
+ - NET_BIND_SERVICE
+ ports:
+ - "80:80"
+ - "443:443"
+ environment:
+ - SCDMS_DOMAIN=${SCDMS_DOMAIN:-scdms.example.com}
+ - GRPC_DOMAIN=${GRPC_DOMAIN:-scdb.example.com}
+ volumes:
+ - ./acme/haproxy.cfg.tmpl:/usr/local/etc/haproxy/haproxy.cfg.tmpl:ro
+ - ./acme/acme-http01-webroot.lua:/usr/local/etc/haproxy/acme-http01-webroot.lua:ro
+ - ./acme/entrypoint.sh:/usr/local/etc/haproxy/acme-entrypoint.sh:ro
+ # Named volumes: ownership (haproxy) is copied from the image into a fresh volume, so the
+ # non-root haproxy user can write the bootstrap PEM and the challenge webroot.
+ - haproxy-certs:/certs
+ - haproxy-webroot:/webroot
+ depends_on:
+ - scdms
+ - sharpcoredb
+ healthcheck:
+ test: ["CMD-SHELL", "echo 'show info' | socat unix-connect:/var/lib/haproxy/admin.sock stdio >/dev/null 2>&1 || exit 1"]
+ interval: 30s
+ timeout: 5s
+ retries: 3
+ start_period: 10s
+
+ certbot:
+ image: certbot/certbot
+ restart: unless-stopped
+ entrypoint: ["/bin/sh", "/hooks/certbot-entrypoint.sh"]
+ environment:
+ - ACME_EMAIL=${ACME_EMAIL:-}
+ - SCDMS_DOMAIN=${SCDMS_DOMAIN:-scdms.example.com}
+ - GRPC_DOMAIN=${GRPC_DOMAIN:-scdb.example.com}
+ volumes:
+ - ./acme/certbot-entrypoint.sh:/hooks/certbot-entrypoint.sh:ro
+ - ./acme/certbot-deploy.sh:/hooks/certbot-deploy.sh:ro
+ - letsencrypt:/etc/letsencrypt
+ - haproxy-webroot:/webroot
+ - haproxy-certs:/certs
+ depends_on:
+ - haproxy
+
+ scdms:
+ image: ghcr.io/mpcoredeveloper/scdms:latest
+ # Until the official image is published you can build it locally from this repository:
+ # build:
+ # context: ../..
+ # dockerfile: Dockerfile
+ restart: unless-stopped
+ environment:
+ - SCDMS__EnableHttps=false
+ - SCDMS__BindAddress=0.0.0.0
+ - SCDMS__DataDirectory=/app/data
+ - SCDMS__UseForwardedHeaders=true
+ # Default gRPC server, reached through the HAProxy proxy (public cert on the proxy):
+ - SCDMS__DefaultServerHost=${GRPC_DOMAIN:-scdb.example.com}
+ - SCDMS__DefaultServerPort=443
+ - SCDMS__DefaultServerDatabase=master
+ - SCDMS__DefaultServerUsername=${SDB_USERNAME:-anonymous}
+ - SCDMS__DefaultServerPassword=${SDB_PASSWORD:-}
+ - SCDMS__DefaultServerUseSsl=true
+ - SCDMS__DefaultServerAutoConnect=true
+ # Optional: disable the in-app update check inside a container
+ - SCDMS__UpdateCheckEnabled=false
+ volumes:
+ - scdms-data:/app/data
+ healthcheck:
+ test: ["CMD", "curl", "-fs", "http://localhost:8080/health"]
+ interval: 30s
+ timeout: 5s
+ retries: 3
+ start_period: 15s
+
+ sharpcoredb:
+ image: ghcr.io/mpcoredeveloper/sharpcoredb-server:latest
+ # The image is published on every v* tag. To build it from a local SharpCoreDB checkout,
+ # uncomment:
+ # build:
+ # context: /path/to/SharpCoreDB
+ # dockerfile: src/SharpCoreDB.Server/Dockerfile
+ restart: unless-stopped
+ environment:
+ # The server uses its own (internal) dev PFX from ./server-certs. Only the HAProxy
+ # certificate (public) is managed by ACME; the proxy skips validation to this internal
+ # certificate, so it never needs to be publicly trusted.
+ - Server__Security__TlsCertificatePath=${SERVER_TLS_CERT_PATH:-/app/certs/server.pfx}
+ - Server__Security__JwtSecretKey=${SERVER_JWT_SECRET:-change-me-to-a-random-32-char-secret!}
+ - Server__SystemDatabases__Enabled=true
+ volumes:
+ - sharpcoredb-data:/app/data
+ - ./server-certs:/app/certs:ro
+ healthcheck:
+ test: ["CMD", "curl", "-fsk", "https://localhost:5001/health"]
+ interval: 30s
+ timeout: 5s
+ retries: 3
+ start_period: 20s
+
+volumes:
+ scdms-data:
+ sharpcoredb-data:
+ # ACME state (named volumes: ownership is seeded from the haproxy image, so the non-root
+ # 'haproxy' user can write; certbot runs as root and writes the real certificate).
+ haproxy-certs:
+ haproxy-webroot:
+ letsencrypt:
diff --git a/samples/haproxy/docker-compose.yml b/samples/haproxy/docker-compose.yml
new file mode 100644
index 0000000..c3d1d22
--- /dev/null
+++ b/samples/haproxy/docker-compose.yml
@@ -0,0 +1,102 @@
+# SCDMS + SharpCoreDB Server + HAProxy β industry-standard reverse proxy sample.
+#
+# Topology (identical wiring to samples/docker and samples/yarp):
+# Browser ββHTTPS (cert on HAProxy)βββΊ haproxy ββHTTPβββΊ scdms:8080 (SCDMS web UI)
+# SCDMS ββgRPC https://βββΊ haproxy ββgRPC/TLSβββΊ sharpcoredb:5001
+#
+# All SCDMS β SharpCoreDB data traffic flows over gRPC. HAProxy does NOT provision TLS
+# certificates automatically (unlike Caddy): mount a PEM (private key + certificate) that
+# covers the proxy hostnames (dev: localhost; prod: your public certificate).
+#
+# Usage:
+# cp .env.example .env # edit domain names + credentials
+# docker compose up -d
+#
+# haproxy.cfg.tmpl is rendered to /tmp/haproxy.cfg by entrypoint.sh from the
+# SCDMS_DOMAIN / GRPC_DOMAIN environment variables.
+
+services:
+ haproxy:
+ image: haproxy:3.0
+ restart: unless-stopped
+ # Binding 80/443 as the non-root 'haproxy' user requires the NET_BIND_SERVICE capability.
+ cap_add:
+ - NET_BIND_SERVICE
+ ports:
+ - "80:80"
+ - "443:443"
+ entrypoint: ["/bin/sh", "/usr/local/etc/haproxy/entrypoint.sh"]
+ environment:
+ - SCDMS_DOMAIN=${SCDMS_DOMAIN:-scdms.example.com}
+ - GRPC_DOMAIN=${GRPC_DOMAIN:-scdb.example.com}
+ volumes:
+ - ./haproxy.cfg.tmpl:/usr/local/etc/haproxy/haproxy.cfg.tmpl:ro
+ - ./entrypoint.sh:/usr/local/etc/haproxy/entrypoint.sh:ro
+ - ./server-certs:/certs:ro
+ depends_on:
+ - scdms
+ - sharpcoredb
+ healthcheck:
+ # socat is included in the official image; queries the HAProxy stats socket.
+ test: ["CMD-SHELL", "echo 'show info' | socat unix-connect:/var/lib/haproxy/admin.sock stdio >/dev/null 2>&1 || exit 1"]
+ interval: 30s
+ timeout: 5s
+ retries: 3
+ start_period: 10s
+
+ scdms:
+ image: ghcr.io/mpcoredeveloper/scdms:latest
+ # Until the official image is published you can build it locally from this repository:
+ # build:
+ # context: ../..
+ # dockerfile: Dockerfile
+ restart: unless-stopped
+ environment:
+ - SCDMS__EnableHttps=false
+ - SCDMS__BindAddress=0.0.0.0
+ - SCDMS__DataDirectory=/app/data
+ - SCDMS__UseForwardedHeaders=true
+ # Default gRPC server, reached through the HAProxy proxy (public cert on the proxy):
+ - SCDMS__DefaultServerHost=${GRPC_DOMAIN:-scdb.example.com}
+ - SCDMS__DefaultServerPort=443
+ - SCDMS__DefaultServerDatabase=master
+ - SCDMS__DefaultServerUsername=${SDB_USERNAME:-anonymous}
+ - SCDMS__DefaultServerPassword=${SDB_PASSWORD:-}
+ - SCDMS__DefaultServerUseSsl=true
+ - SCDMS__DefaultServerAutoConnect=true
+ # Optional: disable the in-app update check inside a container
+ - SCDMS__UpdateCheckEnabled=false
+ volumes:
+ - scdms-data:/app/data
+ healthcheck:
+ test: ["CMD", "curl", "-fs", "http://localhost:8080/health"]
+ interval: 30s
+ timeout: 5s
+ retries: 3
+ start_period: 15s
+
+ sharpcoredb:
+ image: ghcr.io/mpcoredeveloper/sharpcoredb-server:latest
+ # The image is published on every v* tag. To build it from a local SharpCoreDB checkout,
+ # uncomment:
+ # build:
+ # context: /path/to/SharpCoreDB
+ # dockerfile: src/SharpCoreDB.Server/Dockerfile
+ restart: unless-stopped
+ environment:
+ - Server__Security__TlsCertificatePath=${SERVER_TLS_CERT_PATH:-/app/certs/server.pfx}
+ - Server__Security__JwtSecretKey=${SERVER_JWT_SECRET:-change-me-to-a-random-32-char-secret!}
+ - Server__SystemDatabases__Enabled=true
+ volumes:
+ - sharpcoredb-data:/app/data
+ - ./server-certs:/app/certs:ro
+ healthcheck:
+ test: ["CMD", "curl", "-fsk", "https://localhost:5001/health"]
+ interval: 30s
+ timeout: 5s
+ retries: 3
+ start_period: 20s
+
+volumes:
+ scdms-data:
+ sharpcoredb-data:
diff --git a/samples/haproxy/entrypoint.sh b/samples/haproxy/entrypoint.sh
new file mode 100644
index 0000000..eaf413e
--- /dev/null
+++ b/samples/haproxy/entrypoint.sh
@@ -0,0 +1,13 @@
+#!/bin/sh
+# Renders haproxy.cfg.tmpl (placeholders __SCDMS_DOMAIN__ / __GRPC_DOMAIN__) into
+# /tmp/haproxy.cfg and starts HAProxy in foreground (-W -db, like the official entrypoint).
+set -eu
+
+SCDMS_DOMAIN="${SCDMS_DOMAIN:-scdms.example.com}"
+GRPC_DOMAIN="${GRPC_DOMAIN:-scdb.example.com}"
+
+sed -e "s/__SCDMS_DOMAIN__/${SCDMS_DOMAIN}/g" \
+ -e "s/__GRPC_DOMAIN__/${GRPC_DOMAIN}/g" \
+ /usr/local/etc/haproxy/haproxy.cfg.tmpl > /tmp/haproxy.cfg
+
+exec haproxy -W -db -f /tmp/haproxy.cfg "$@"
diff --git a/samples/haproxy/haproxy.cfg.tmpl b/samples/haproxy/haproxy.cfg.tmpl
new file mode 100644
index 0000000..6260ed6
--- /dev/null
+++ b/samples/haproxy/haproxy.cfg.tmpl
@@ -0,0 +1,54 @@
+# ββ HAProxy sample: SCDMS web UI + SharpCoreDB gRPC βββββββββββββββββββββββββββββ
+# TLS is terminated here. Placeholders __SCDMS_DOMAIN__ / __GRPC_DOMAIN__ are replaced by
+# entrypoint.sh from the SCDMS_DOMAIN / GRPC_DOMAIN environment variables.
+
+global
+ log stdout format raw local0 notice
+ maxconn 4096
+ # Used by the container HEALTHCHECK (socat "show info").
+ stats socket /var/lib/haproxy/admin.sock level admin mode 600
+ ssl-default-bind-options no-sslv3 no-tlsv10 no-tlsv11
+ ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384
+ ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384
+
+defaults
+ mode http
+ log global
+ option httplog
+ option dontlognull
+ option http-server-close
+ timeout connect 5s
+ timeout client 60s
+ timeout server 60s
+ timeout tunnel 15m
+
+# ββ HTTP frontend: container /health + redirect everything else to HTTPS ββββββββ
+frontend http-in
+ bind *:80
+ http-request return status 200 content-type text/plain string "OK" if { path /health }
+ http-request redirect scheme https unless { path /health }
+
+# ββ HTTPS frontend (TLS terminated): HTTP/1.1 (UI) + HTTP/2 (gRPC) ββββββββββββββ
+frontend https-in
+ bind *:443 ssl crt /certs/haproxy.pem alpn h2,http/1.1
+ http-request set-header X-Forwarded-Proto https
+ http-request set-header X-Forwarded-Port 443
+ # Long timeout for long-lived gRPC streams.
+ timeout client 1h
+
+ # Route by Server Name Indication (browser sends SNI for the UI host, the SCDMS gRPC
+ # client sends SNI for the gRPC host).
+ use_backend scdms_ui if { ssl_fc_sni -i __SCDMS_DOMAIN__ }
+ use_backend scdb_grpc if { ssl_fc_sni -i __GRPC_DOMAIN__ }
+ default_backend scdms_ui
+
+# SCDMS web UI β plain HTTP on scdms:8080.
+backend scdms_ui
+ server scdms scdms:8080 check inter 30s fall 3 rise 2
+
+# SharpCoreDB gRPC β HTTPS/HTTP2 upstream. 'ssl verify none' skips the server's internal
+# (self-signed) certificate β the public client still validates HAProxy's certificate
+# (mirror of Caddy's tls_insecure_skip_verify / the YARP sample).
+backend scdb_grpc
+ timeout server 1h
+ server sharpcoredb sharpcoredb:5001 ssl verify none alpn h2 check inter 30s fall 3 rise 2
diff --git a/samples/yarp/.env.example b/samples/yarp/.env.example
new file mode 100644
index 0000000..b682e93
--- /dev/null
+++ b/samples/yarp/.env.example
@@ -0,0 +1,23 @@
+# ββ Proxy hostnames ββ
+# For local testing use localhost for both (single dev certificate covers both).
+# For production use the real public hostnames; the PFX must cover both names.
+SCDMS_DOMAIN=scdms.example.com
+GRPC_DOMAIN=scdb.example.com
+
+# ββ YARP proxy TLS ββ
+# YARP terminates TLS for the browser UI and the SCDMS gRPC client. Provide a PFX covering
+# SCDMS_DOMAIN and GRPC_DOMAIN, e.g. for local testing:
+# mkdir -p server-certs
+# dotnet dev-certs https -ep server-certs/server.pfx -p devonly
+# (leave YARP_TLS_CERT_PASSWORD empty when the PFX has no password)
+YARP_TLS_CERT_PATH=/certs/server.pfx
+YARP_TLS_CERT_PASSWORD=devonly
+
+# ββ SharpCoreDB server credentials ββ
+# The SharpCoreDB server uses its own (internal) certificate from the same server-certs folder.
+SERVER_TLS_CERT_PATH=/app/certs/server.pfx
+SERVER_JWT_SECRET=replace-with-a-random-32-char-secret
+
+# ββ SCDMS default-server login used against the SharpCoreDB server ββ
+SDB_USERNAME=anonymous
+SDB_PASSWORD=
diff --git a/samples/yarp/README.md b/samples/yarp/README.md
new file mode 100644
index 0000000..29ed550
--- /dev/null
+++ b/samples/yarp/README.md
@@ -0,0 +1,95 @@
+# SCDMS + SharpCoreDB β YARP reverse proxy sample (all-.NET)
+
+This is an **all-.NET** alternative to the Caddy-based sample in
+[`samples/docker/`](../docker/). It runs the same three-part topology but replaces Caddy with a
+small **YARP** (`Yarp.ReverseProxy`) proxy that you build from `./proxy`:
+
+```text
+Browser ββHTTPS (cert on YARP)βββΊ yarp ββHTTPβββΊ scdms:8080 (SCDMS web UI)
+SCDMS ββgRPC https://βββΊ yarp ββgRPC/TLSβββΊ sharpcoredb:5001
+```
+
+All SCDMS β SharpCoreDB data traffic flows over **gRPC**. The proxy terminates TLS for both
+the SCDMS web UI and the SCDMSβserver gRPC channel, then re-encrypts to the SharpCoreDB
+server's internal certificate (the server only speaks TLS).
+
+## Caddy vs. YARP β what changes
+
+| | Caddy sample (`samples/docker/`) | YARP sample (this folder) |
+|---|---|---|
+| Proxy | Official `caddy:2.9` image | Your own .NET image built from `./proxy` |
+| TLS certificates | **Automatic** Let's Encrypt per domain | **Manual**: you mount a PFX; cert renewal is your job |
+| Language | Go binary + Caddyfile | 100% C# (`Yarp.ReverseProxy` on Kestrel) |
+| Customization | Config file | Full programmatic control (routes/clusters/transforms in code) |
+
+Choose YARP when you want everything in .NET and/or deep programmatic proxy control; choose the
+Caddy sample when you want zero-maintenance automatic TLS.
+
+## Quick start
+
+1. Make the images available:
+
+ ```bash
+ cd /SCDMS
+ docker build -t ghcr.io/mpcoredeveloper/scdms:latest . # until the official image is published
+ docker pull ghcr.io/mpcoredeveloper/sharpcoredb-server:2.0.0.2
+ ```
+
+2. Configure and create the TLS certificate (local test β use `localhost` in `.env`):
+
+ ```bash
+ cd samples/yarp
+ cp .env.example .env # PowerShell: Copy-Item .env.example .env
+ mkdir -p server-certs
+ dotnet dev-certs https -ep server-certs/server.pfx -p devonly
+ ```
+
+ Edit `.env`: set `SCDMS_DOMAIN=localhost` and `GRPC_DOMAIN=localhost` for local testing,
+ or your real public hostnames in production (the PFX must then cover both hostnames).
+
+3. Start everything:
+
+ ```bash
+ docker compose up -d --build
+ docker compose ps # yarp, scdms, sharpcoredb all "healthy"
+ ```
+
+## Verify
+
+```bash
+# YARP proxy health
+docker compose exec yarp curl -fs http://localhost:80/health
+
+# SCDMS health (via the proxy, https)
+docker compose exec scdms curl -fs http://localhost:8080/health
+
+# SharpCoreDB server health (internal self-signed cert = -k)
+docker compose exec sharpcoredb curl -fsk https://localhost:8443/api/v1/health
+
+# Logs
+docker compose logs -f yarp scdms
+```
+
+- Browser: `https://` (accept the self-signed warning when using `localhost`).
+- The YARP proxy log shows the routes it registered at startup.
+
+## TLS notes
+
+- **SCDMS validates the certificate of the gRPC endpoint it connects to.** In production the
+ mounted PFX must be publicly trusted (as with any proxy). With a self-signed `localhost` cert
+ the browser warns and SCDMS's automatic gRPC connect cannot validate the proxy cert β that is
+ the same TLS caveat as the Caddy sample (see
+ [`docs/container-and-aspire-guide.md`](../../docs/container-and-aspire-guide.md), Β§5 for a
+ green local data-path test).
+- The proxy deliberately **skips** certificate validation towards the internal SharpCoreDB
+ server (`DangerousAcceptAnyServerCertificate`), exactly like the Caddy sample's
+ `tls_insecure_skip_verify`. That switch only affects the proxyβserver leg.
+- The proxy runs as a **non-root** user inside the container; binding ports `80`/`443` needs the
+ `NET_BIND_SERVICE` capability (added via `cap_add` in `docker-compose.yml`). Map higher ports
+ (`YARP_HTTP_PORT`/`YARP_HTTPS_PORT`) when capabilities cannot be granted.
+
+## Stop
+
+```bash
+docker compose down # add -v to also delete the data volumes
+```
diff --git a/samples/yarp/docker-compose.yml b/samples/yarp/docker-compose.yml
new file mode 100644
index 0000000..a5b6107
--- /dev/null
+++ b/samples/yarp/docker-compose.yml
@@ -0,0 +1,103 @@
+# SCDMS + SharpCoreDB Server + YARP β all-.NET reverse proxy sample.
+#
+# Topology (identical wiring to samples/docker, but with a YARP proxy instead of Caddy):
+# Browser ββHTTPS (cert on YARP)βββΊ yarp ββHTTPβββΊ scdms:8080 (SCDMS web UI)
+# SCDMS ββgRPC https://βββΊ yarp ββgRPC/TLSβββΊ sharpcoredb:5001
+#
+# All SCDMS β SharpCoreDB data traffic flows over gRPC. YARP does NOT provision TLS
+# certificates automatically (unlike Caddy): mount a PFX that covers the proxy hostnames
+# (dev: localhost; prod: your public certificate).
+#
+# Usage:
+# cp .env.example .env # edit domain names + credentials
+# docker compose up -d --build # builds the yarp proxy image locally
+#
+# Prerequisites:
+# - Published image ghcr.io/mpcoredeveloper/sharpcoredb-server (available).
+# - SCDMS image ghcr.io/mpcoredeveloper/scdms: published on v* tags; until then build it
+# locally by uncommenting the scdms `build:` block below.
+
+services:
+ yarp:
+ build: ./proxy
+ image: yarp-proxy:local
+ restart: unless-stopped
+ # The container runs as a non-root user; binding 80/443 needs this capability.
+ cap_add:
+ - NET_BIND_SERVICE
+ ports:
+ - "80:80"
+ - "443:443"
+ environment:
+ - SCDMS_DOMAIN=${SCDMS_DOMAIN:-scdms.example.com}
+ - GRPC_DOMAIN=${GRPC_DOMAIN:-scdb.example.com}
+ - YARP_TLS_CERT_PATH=${YARP_TLS_CERT_PATH:-/certs/server.pfx}
+ - YARP_TLS_CERT_PASSWORD=${YARP_TLS_CERT_PASSWORD:-devonly}
+ volumes:
+ - ./server-certs:/certs:ro
+ depends_on:
+ - scdms
+ - sharpcoredb
+ healthcheck:
+ test: ["CMD", "curl", "-fs", "http://localhost:80/health"]
+ interval: 30s
+ timeout: 5s
+ retries: 3
+ start_period: 10s
+
+ scdms:
+ image: ghcr.io/mpcoredeveloper/scdms:latest
+ # Until the official image is published you can build it locally from this repository:
+ # build:
+ # context: ../..
+ # dockerfile: Dockerfile
+ restart: unless-stopped
+ environment:
+ - SCDMS__EnableHttps=false
+ - SCDMS__BindAddress=0.0.0.0
+ - SCDMS__DataDirectory=/app/data
+ - SCDMS__UseForwardedHeaders=true
+ # Default gRPC server, reached through the YARP proxy (public cert on the proxy):
+ - SCDMS__DefaultServerHost=${GRPC_DOMAIN:-scdb.example.com}
+ - SCDMS__DefaultServerPort=443
+ - SCDMS__DefaultServerDatabase=master
+ - SCDMS__DefaultServerUsername=${SDB_USERNAME:-anonymous}
+ - SCDMS__DefaultServerPassword=${SDB_PASSWORD:-}
+ - SCDMS__DefaultServerUseSsl=true
+ - SCDMS__DefaultServerAutoConnect=true
+ # Optional: disable the in-app update check inside a container
+ - SCDMS__UpdateCheckEnabled=false
+ volumes:
+ - scdms-data:/app/data
+ healthcheck:
+ test: ["CMD", "curl", "-fs", "http://localhost:8080/health"]
+ interval: 30s
+ timeout: 5s
+ retries: 3
+ start_period: 15s
+
+ sharpcoredb:
+ image: ghcr.io/mpcoredeveloper/sharpcoredb-server:latest
+ # The image is published on every v* tag. To build it from a local SharpCoreDB checkout,
+ # uncomment:
+ # build:
+ # context: /path/to/SharpCoreDB
+ # dockerfile: src/SharpCoreDB.Server/Dockerfile
+ restart: unless-stopped
+ environment:
+ - Server__Security__TlsCertificatePath=${SERVER_TLS_CERT_PATH:-/app/certs/server.pfx}
+ - Server__Security__JwtSecretKey=${SERVER_JWT_SECRET:-change-me-to-a-random-32-char-secret!}
+ - Server__SystemDatabases__Enabled=true
+ volumes:
+ - sharpcoredb-data:/app/data
+ - ./server-certs:/app/certs:ro
+ healthcheck:
+ test: ["CMD", "curl", "-fsk", "https://localhost:5001/health"]
+ interval: 30s
+ timeout: 5s
+ retries: 3
+ start_period: 20s
+
+volumes:
+ scdms-data:
+ sharpcoredb-data:
diff --git a/samples/yarp/proxy/.dockerignore b/samples/yarp/proxy/.dockerignore
new file mode 100644
index 0000000..9f8ccca
--- /dev/null
+++ b/samples/yarp/proxy/.dockerignore
@@ -0,0 +1,9 @@
+# Build context exclusions for the YARP proxy sample image
+.git
+.github
+**/bin
+**/obj
+*.md
+*.user
+.dockerignore
+Dockerfile
diff --git a/samples/yarp/proxy/Dockerfile b/samples/yarp/proxy/Dockerfile
new file mode 100644
index 0000000..a521290
--- /dev/null
+++ b/samples/yarp/proxy/Dockerfile
@@ -0,0 +1,43 @@
+# ββ YARP reverse proxy (SCDMS + SharpCoreDB) ββ
+# All-.NET TLS-terminating proxy sample.
+# Image: yarp-proxy (local build from samples/yarp/proxy)
+
+# ββ Stage 1: Build ββ
+FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
+WORKDIR /src
+
+COPY YarpProxy.csproj .
+RUN dotnet restore YarpProxy.csproj
+
+COPY . .
+RUN dotnet publish YarpProxy.csproj \
+ -c Release \
+ --no-restore \
+ -o /app/publish
+
+# ββ Stage 2: Runtime ββ
+FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
+WORKDIR /app
+
+# curl for the container HEALTHCHECK
+RUN apt-get update && \
+ apt-get install -y --no-install-recommends curl && \
+ rm -rf /var/lib/apt/lists/*
+
+COPY --from=build /app/publish .
+
+# Kestrel endpoints are configured in Program.cs from environment variables (YARP_HTTP_PORT /
+# YARP_HTTPS_PORT). Exposed for documentation; the HTTPS listener terminates TLS for both the
+# SCDMS web UI and the SharpCoreDB gRPC endpoint.
+EXPOSE 80 443
+
+# Health check (http listener; /health is excluded from the https redirect).
+HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
+ CMD curl -fs http://localhost:80/health || exit 1
+
+# The proxy runs as a non-root user; binding 80/443 needs the NET_BIND_SERVICE capability
+# (added via cap_add in docker-compose.yml). Map higher ports (YARP_HTTP_PORT/YARP_HTTPS_PORT)
+# when capabilities cannot be granted.
+USER 1000:1000
+
+ENTRYPOINT ["dotnet", "YarpProxy.dll"]
diff --git a/samples/yarp/proxy/Program.cs b/samples/yarp/proxy/Program.cs
new file mode 100644
index 0000000..80f167c
--- /dev/null
+++ b/samples/yarp/proxy/Program.cs
@@ -0,0 +1,145 @@
+using System.Net;
+using System.Net.Http;
+using System.Security.Authentication;
+using System.Security.Cryptography.X509Certificates;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Server.Kestrel.Core;
+using Yarp.ReverseProxy;
+using Yarp.ReverseProxy.Configuration;
+using Yarp.ReverseProxy.Forwarder;
+
+// ββ YARP reverse proxy sample βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+// All-.NET, TLS-terminating reverse proxy in front of:
+// β’ SCDMS β http://scdms:8080 (web UI, HTTP/1.1)
+// β’ SharpCoreDB β https://sharpcoredb:5001 (gRPC over HTTP/2)
+//
+// The proxy is fully driven by environment variables (see docker-compose.yml), mirroring the
+// samples/docker Caddyfile topology. Unlike Caddy, YARP does not provision certificates
+// automatically: mount a PFX that covers the proxy hostname(s) (dev: localhost via
+// `dotnet dev-certs https -ep server-certs/server.pfx -p devonly`; prod: your public cert).
+
+static string GetEnv(string name, string fallback) =>
+ Environment.GetEnvironmentVariable(name) ?? fallback;
+
+var scdmsDomain = GetEnv("SCDMS_DOMAIN", "scdms.example.com");
+var grpcDomain = GetEnv("GRPC_DOMAIN", "scdb.example.com");
+var httpPort = int.Parse(GetEnv("YARP_HTTP_PORT", "80"));
+var httpsPort = int.Parse(GetEnv("YARP_HTTPS_PORT", "443"));
+var certPath = GetEnv("YARP_TLS_CERT_PATH", "/certs/server.pfx");
+var certPassword = GetEnv("YARP_TLS_CERT_PASSWORD", "");
+// Plain HTTP is intentional on this leg: SCDMS serves HTTP inside its container and TLS is
+// terminated by this proxy (see README). Suppress S5332 accordingly.
+var scdmsUpstream = GetEnv("SCDMS_UPSTREAM", "http://scdms:8080"); // NOSONAR
+var grpcUpstream = GetEnv("SCDB_UPSTREAM", "https://sharpcoredb:5001");
+
+var builder = WebApplication.CreateBuilder(args);
+
+// Kestrel endpoints: HTTP for redirects + container healthcheck, HTTPS (HTTP/1.1 + HTTP/2 for
+// gRPC) with the mounted TLS certificate.
+builder.WebHost.ConfigureKestrel(kestrel =>
+{
+ kestrel.Listen(IPAddress.Any, httpPort);
+
+ kestrel.Listen(IPAddress.Any, httpsPort, listen =>
+ {
+ listen.Protocols = HttpProtocols.Http1AndHttp2;
+
+ if (!File.Exists(certPath))
+ {
+ throw new FileNotFoundException(
+ $"TLS certificate not found at '{certPath}'. Generate one and mount it, e.g. " +
+ "'dotnet dev-certs https -ep server-certs/server.pfx -p devonly' (see README).");
+ }
+
+ listen.UseHttps(https => https.ServerCertificate =
+ X509CertificateLoader.LoadPkcs12FromFile(certPath, certPassword));
+ });
+});
+
+// Host-based routing (mirror of samples/docker/caddy/Caddyfile):
+// β SCDMS web UI (plain HTTP upstream)
+// β SharpCoreDB gRPC (HTTPS/HTTP2 upstream, internal certificate skipped,
+// exactly like Caddy's tls_insecure_skip_verify)
+var routes = new[]
+{
+ new RouteConfig
+ {
+ RouteId = "scdms-ui",
+ ClusterId = "scdms-cluster",
+ Match = new RouteMatch { Hosts = new[] { scdmsDomain } },
+ Transforms = new[]
+ {
+ new Dictionary
+ {
+ ["RequestHeader"] = "X-Forwarded-Proto",
+ ["Set"] = "https"
+ }
+ }
+ },
+ new RouteConfig
+ {
+ RouteId = "scdb-grpc",
+ ClusterId = "scdb-grpc-cluster",
+ Match = new RouteMatch { Hosts = new[] { grpcDomain } }
+ }
+};
+
+var clusters = new[]
+{
+ new ClusterConfig
+ {
+ ClusterId = "scdms-cluster",
+ Destinations = new Dictionary
+ {
+ ["scdms"] = new DestinationConfig { Address = scdmsUpstream }
+ }
+ },
+ new ClusterConfig
+ {
+ ClusterId = "scdb-grpc-cluster",
+ Destinations = new Dictionary
+ {
+ ["sharpcoredb"] = new DestinationConfig { Address = grpcUpstream }
+ },
+ HttpClient = new HttpClientConfig
+ {
+ // The SharpCoreDB container uses an internal/self-signed certificate; the public
+ // client still validates the proxy's certificate. Dev only (Caddy equivalent:
+ // tls_insecure_skip_verify).
+ DangerousAcceptAnyServerCertificate = true,
+ SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13,
+ EnableMultipleHttp2Connections = true
+ },
+ HttpRequest = new ForwarderRequestConfig
+ {
+ Version = HttpVersion.Version20, // gRPC requires HTTP/2 upstream
+ VersionPolicy = HttpVersionPolicy.RequestVersionOrLower
+ }
+ }
+};
+
+builder.Services.AddReverseProxy().LoadFromMemory(routes, clusters);
+
+var app = builder.Build();
+
+// http β https redirect for the browser UI (never for /health).
+app.Use(async (context, next) =>
+{
+ if (!context.Request.IsHttps && context.Request.Path != "/health")
+ {
+ var host = httpsPort == 443 ? context.Request.Host.Host : $"{context.Request.Host.Host}:{httpsPort}";
+ context.Response.Redirect(
+ $"https://{host}{context.Request.PathBase}{context.Request.Path}{context.Request.QueryString}",
+ permanent: true);
+ return;
+ }
+
+ await next(context);
+});
+
+// Proxy-container health endpoint (used by the Docker HEALTHCHECK).
+app.MapGet("/health", () => Results.Text("OK"));
+
+app.MapReverseProxy();
+
+await app.RunAsync();
diff --git a/samples/yarp/proxy/YarpProxy.csproj b/samples/yarp/proxy/YarpProxy.csproj
new file mode 100644
index 0000000..4c28dec
--- /dev/null
+++ b/samples/yarp/proxy/YarpProxy.csproj
@@ -0,0 +1,18 @@
+
+
+
+ net10.0
+ 14.0
+ enable
+ enable
+ YarpProxy
+ YarpProxy
+
+ false
+
+
+
+
+
+
+
diff --git a/src/SCDMS.Aspire.Hosting/NuGet.README.md b/src/SCDMS.Aspire.Hosting/NuGet.README.md
new file mode 100644
index 0000000..1848f17
--- /dev/null
+++ b/src/SCDMS.Aspire.Hosting/NuGet.README.md
@@ -0,0 +1,38 @@
+# SCDMS.Aspire.Hosting
+
+.NET Aspire hosting integration for the [SCDMS](https://github.com/MPCoreDeveloper/SCDMS)
+web studio β the database studio for [SharpCoreDB](https://github.com/MPCoreDeveloper/SharpCoreDB).
+
+Spin up a **SharpCoreDB server container** and an **SCDMS container** as one Aspire application
+(pgweb/pgAdmin-style). All SCDMS β SharpCoreDB data traffic flows over **gRPC**.
+
+## Getting started
+
+```csharp
+using Scdms.Aspire.Hosting;
+
+var builder = DistributedApplication.CreateBuilder(args);
+
+// SharpCoreDB network server container (HTTPS gRPC on 5001, HTTPS REST API on 8443).
+var db = builder.AddSharpCoreDB("db")
+ .WithServerContainer()
+ .WithJwtSecret("a-random-secret-of-at-least-32-characters");
+
+// SCDMS web studio container, auto-wired to the server over gRPC.
+builder.AddSCDMS("admin", db);
+
+builder.Build().Run();
+```
+
+`AddSCDMS` uses the published image `ghcr.io/mpcoredeveloper/scdms:latest` and configures the
+container defaults (plain HTTP on 8080, bind `0.0.0.0`, data in `/app/data`). When the optional
+SharpCoreDB resource is passed, `WithGrpcReference` forwards the server's gRPC endpoint through
+the `SCDMS__DefaultServerHost`/`SCDMS__DefaultServerPort` environment variables and enables
+auto-connect.
+
+## TLS & production notes
+
+SCDMS validates the public certificate of the gRPC endpoint. In production terminate TLS at a
+reverse proxy holding a publicly trusted certificate (see the Docker Compose sample in the
+SCDMS repository, `samples/docker/`). The SharpCoreDB server container always exposes TLS
+endpoints and requires a JWT secret (use `WithJwtSecret`).
diff --git a/src/SCDMS.Aspire.Hosting/SCDMS.Aspire.Hosting.csproj b/src/SCDMS.Aspire.Hosting/SCDMS.Aspire.Hosting.csproj
new file mode 100644
index 0000000..a4778cb
--- /dev/null
+++ b/src/SCDMS.Aspire.Hosting/SCDMS.Aspire.Hosting.csproj
@@ -0,0 +1,37 @@
+
+
+
+ net10.0
+ 14.0
+ enable
+ enable
+ true
+
+ SCDMS.Aspire.Hosting
+ Scdms.Aspire.Hosting
+ .NET Aspire hosting integration for the SCDMS web studio - registers the SCDMS container and links it to a SharpCoreDB server container over gRPC
+
+
+ SCDMS.Aspire.Hosting
+ MPCoreDeveloper
+ database;aspire;hosting;container;sharpcoredb;scdms;net10
+ MIT
+ https://github.com/MPCoreDeveloper/SCDMS
+ https://github.com/MPCoreDeveloper/SCDMS
+ git
+ NuGet.README.md
+ true
+ snupkg
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/SCDMS.Aspire.Hosting/SCDMSResource.cs b/src/SCDMS.Aspire.Hosting/SCDMSResource.cs
new file mode 100644
index 0000000..5d72e08
--- /dev/null
+++ b/src/SCDMS.Aspire.Hosting/SCDMSResource.cs
@@ -0,0 +1,23 @@
+using Aspire.Hosting.ApplicationModel;
+
+namespace Scdms.Aspire.Hosting;
+
+///
+/// A container resource representing the SCDMS web studio image
+/// (ghcr.io/mpcoredeveloper/scdms). Inside the container SCDMS serves plain HTTP on
+/// port 8080; in production a reverse proxy terminates TLS and forwards the browser traffic
+/// to this endpoint. All SCDMS β SharpCoreDB data traffic flows over gRPC (server mode).
+///
+public sealed class ScdmsResource(string name) : ContainerResource(name), IResourceWithConnectionString
+{
+ /// Name of the HTTP endpoint exposed by the SCDMS container.
+ public const string HttpEndpointName = "http";
+
+ /// Gets a reference to the HTTP endpoint of the container.
+ public EndpointReference HttpEndpoint => new(this, HttpEndpointName);
+
+ ///
+ public ReferenceExpression ConnectionStringExpression =>
+ ReferenceExpression.Create(
+ $"{HttpEndpoint.Property(EndpointProperty.Scheme)}://{HttpEndpoint.Property(EndpointProperty.Host)}:{HttpEndpoint.Property(EndpointProperty.Port)}");
+}
diff --git a/src/SCDMS.Aspire.Hosting/ScdmsAspireExtensions.cs b/src/SCDMS.Aspire.Hosting/ScdmsAspireExtensions.cs
new file mode 100644
index 0000000..bef1bb2
--- /dev/null
+++ b/src/SCDMS.Aspire.Hosting/ScdmsAspireExtensions.cs
@@ -0,0 +1,80 @@
+using Aspire.Hosting;
+using Aspire.Hosting.ApplicationModel;
+using SharpCoreDB.Aspire.Hosting;
+
+namespace Scdms.Aspire.Hosting;
+
+///
+/// .NET Aspire extension methods that register a SCDMS web studio container and link it to a
+/// SharpCoreDB server container. SCDMS talks to the server exclusively over gRPC; the link is
+/// configured through the SCDMS__DefaultServer* environment variables that the SCDMS
+/// image reads at startup (see docs/usage.md).
+///
+public static class ScdmsAspireExtensions
+{
+ /// Published OCI image for SCDMS.
+ public const string ScdmsImage = "ghcr.io/mpcoredeveloper/scdms";
+
+ /// Default image tag used when no explicit tag is supplied.
+ public const string DefaultImageTag = "latest";
+
+ /// Default HTTP port inside the SCDMS container (see the repository Dockerfile).
+ public const int DefaultHttpTargetPort = 8080;
+
+ ///
+ /// Adds a SCDMS web studio container to the Aspire application. The resource exposes an HTTP
+ /// endpoint (named , container port 8080). When
+ /// is supplied the SCDMS container is linked to the SharpCoreDB
+ /// server over gRPC via .
+ ///
+ /// The distributed application builder.
+ /// The resource name.
+ /// Optional SharpCoreDB server resource to auto-connect to.
+ /// Optional container image tag (defaults to latest).
+ /// Optional fixed host port for the HTTP endpoint (default: allocated by Aspire).
+ /// The SCDMS resource builder.
+ public static IResourceBuilder AddSCDMS(
+ this IDistributedApplicationBuilder builder,
+ string name,
+ IResourceBuilder? sharpCoreDb = null,
+ string? imageTag = null,
+ int? port = null)
+ {
+ var scdms = builder.AddResource(new ScdmsResource(name))
+ .WithImage(ScdmsImage)
+ .WithImageTag(imageTag ?? DefaultImageTag)
+ .WithHttpEndpoint(targetPort: DefaultHttpTargetPort, port: port, name: ScdmsResource.HttpEndpointName)
+ .WithEnvironment("SCDMS__EnableHttps", "false") // reverse proxy terminates TLS in production
+ .WithEnvironment("SCDMS__BindAddress", "0.0.0.0") // bind all container interfaces
+ .WithEnvironment("SCDMS__DataDirectory", "/app/data") // volume mount point for persistence
+ .WithEnvironment("SCDMS__UpdateCheckEnabled", "false"); // no GitHub reachability needed in containers
+
+ return sharpCoreDb is null ? scdms : scdms.WithGrpcReference(sharpCoreDb);
+ }
+
+ ///
+ /// Links a SCDMS container to a SharpCoreDB server container. The gRPC endpoint of the server
+ /// (container port 5001) is resolved and forwarded as SCDMS__DefaultServerHost and
+ /// SCDMS__DefaultServerPort; TLS stays enabled and SCDMS auto-connects when the UI
+ /// opens, mirroring the production compose sample in samples/docker/.
+ ///
+ /// The SCDMS resource builder.
+ /// The SharpCoreDB server resource builder.
+ /// The SCDMS resource builder.
+ /// Thrown when either argument is null.
+ public static IResourceBuilder WithGrpcReference(
+ this IResourceBuilder scdms,
+ IResourceBuilder sharpCoreDb)
+ {
+ ArgumentNullException.ThrowIfNull(scdms);
+ ArgumentNullException.ThrowIfNull(sharpCoreDb);
+
+ var grpcEndpoint = sharpCoreDb.GetEndpoint(SharpCoreDbServerResource.GrpcEndpointName);
+
+ return scdms
+ .WithEnvironment("SCDMS__DefaultServerHost", grpcEndpoint.Property(EndpointProperty.Host))
+ .WithEnvironment("SCDMS__DefaultServerPort", grpcEndpoint.Property(EndpointProperty.Port))
+ .WithEnvironment("SCDMS__DefaultServerUseSsl", "true")
+ .WithEnvironment("SCDMS__DefaultServerAutoConnect", "true");
+ }
+}