HAProxy is the battle-tested, industry-standard load balancer / reverse proxy. This sample
runs the same topology as samples/docker/ and samples/yarp/ but
terminates TLS with HAProxy instead of Caddy/YARP:
Browser ──HTTPS (cert on HAProxy)──► haproxy ──HTTP──► scdms:8080 (SCDMS web UI)
SCDMS ──gRPC https://<GRPC_HOST>──► 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 (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).
-
Make the images available:
cd <repo-root>/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
-
Configure and create the TLS certificate. HAProxy needs a PEM (key + certificate); for local testing convert the dev PFX:
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: setSCDMS_DOMAIN=localhostandGRPC_DOMAIN=localhostfor local testing, or your real public hostnames in production (the PEM must cover both hostnames). -
Start everything:
docker compose up -d docker compose ps # haproxy, scdms, sharpcoredb all "healthy"
# 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://<SCDMS_DOMAIN>(accept the self-signed warning when usinglocalhost). - The HAProxy log lines show the SNI-based backend selection (
scdms_ui/scdb_grpc).
- 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
localhostcert the browser warns and SCDMS's automatic gRPC connect cannot validate the proxy cert — the same TLS caveat as the other samples (seedocs/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
haproxyuser; binding 80/443 needs theNET_BIND_SERVICEcapability (added indocker-compose.yml).
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 onhaproxy:3.0+openssl) loads the janeczku HAProxy ACME Lua plugin (./acme/acme-http01-webroot.lua) which answershttp-01challenges on port 80 from the shared/webrootfolder. 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 forSCDMS_DOMAIN+GRPC_DOMAIN(webroot method). After every successful issue/renewal its deploy hook (./acme/certbot-deploy.sh) combinesprivkey.pem+fullchain.peminto/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.
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 --buildCheck the flow:
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://<SCDMS_DOMAIN> and https://<GRPC_DOMAIN>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.
docker compose down # add -v to also delete the data volumes