Migration — Phase 16b: Docker host companion (`ToolUp.Hosts.Docker`)
Migration — Phase 16b: Docker host companion (ToolUp.Hosts.Docker)
Adds a maintained Dockerfile + .dockerignore + healthcheck script + sample compose.yml so deployments do not have to hand-roll Docker artefacts.
What changes
- New companion package
ToolUp.Hosts.Docker. Ships no.fsbridge code — the SDK already speaks HTTP via Kestrel; the container just runs the published binary. The deliverable is four template files (Dockerfile.template,.dockerignore.template,healthcheck.sh,compose.yml.template) undercontentFiles/in the nupkg. - New
dotnet new platformsdk-dockertemplate — emits the same four files at the consumer's solution root with{{server-project}}/{{server-dll}}/{{image-name}}/{{host-port}}tokens substituted. - New TECHNICAL_GUIDE chapter
14. Docker Hosting. Image layout, signal handling, non-root convention, healthcheck,ProcessProfileinteraction, per-platform deployment entry points.
The companion is purely additive. Existing deployments that ship their own hand-rolled Dockerfile keep working without change. No SDK-side behaviour or API moves.
How to adopt
Option A — dotnet new platformsdk-docker (recommended)
dotnet new platformsdk-docker \
--server-project MyApp-Server \
--server-dll MyApp-Server \
--image-name myapp \
--host-port 8080
Emits Dockerfile, .dockerignore, healthcheck.sh, compose.yml at the solution root. Token-substitutes for your project names.
Option B — <PackageReference> + manual rename
<PackageReference Include="ToolUp.Hosts.Docker" />
NuGet restore drops the four template files into the consumer's contentFiles/ cache. Copy / rename them at the solution root and edit the {{...}} tokens by hand:
| Template name | Final name |
|---|---|
Dockerfile.template |
Dockerfile |
.dockerignore.template |
.dockerignore |
healthcheck.sh |
healthcheck.sh (use as-is) |
compose.yml.template |
compose.yml |
Verification
After scaffolding the artefacts:
# 1. Build the image (cold; warm cache after the first run is much faster)
docker build -t myapp:dev .
# 2. Run with the default ProcessProfile=AllInOne
docker run --rm -d -p 8080:5000 --name myapp-dev myapp:dev
# 3. Confirm Liveness probe is green within ~30 seconds
curl -fsS http://localhost:8080/health # → 2xx
docker inspect --format '{{.State.Health.Status}}' myapp-dev # → "healthy"
# 4. Confirm graceful shutdown (no SIGKILL fallthrough)
docker stop myapp-dev
# Expect: "myapp-dev" printed within the grace period (default 10s).
# If docker stop takes the full grace period and then prints SIGKILL,
# tini is not forwarding signals — check the Dockerfile's ENTRYPOINT.
# 5. Confirm non-root execution
docker run --rm myapp:dev id # → uid=10001(app) gid=10001(app) groups=10001(app)
Profile-shape verification (one image, env-var-driven role):
# WebOnly — Kestrel binds, no background services tick
docker run --rm -d -p 8080:5000 -e TOOLUP_PROCESS_PROFILE=WebOnly --name myapp-web myapp:dev
curl -fsS http://localhost:8080/health # → 2xx
docker logs myapp-web 2>&1 | grep -E '(scheduler|webhook|dispatcher)' | head
# Expect: zero or near-zero matches — every IHostedService is gated off.
docker stop myapp-web
# WorkerOnly — silo runs background services, HTTP pipeline not configured
docker run --rm -d -e TOOLUP_PROCESS_PROFILE=WorkerOnly --name myapp-worker myapp:dev
docker logs myapp-worker 2>&1 | grep -i 'scheduler\|tick' | head
# Expect: scheduler tick lines appearing within ~30 seconds.
docker stop myapp-worker
Rollback
The companion is purely additive — there is nothing to revert in your SDK composition.
- Delete the four files (
Dockerfile,.dockerignore,healthcheck.sh,compose.yml) and the<PackageReference Include="ToolUp.Hosts.Docker" />line from your solution. - The deployment falls back to whatever hand-rolled Docker config you were using before (or to a non-containerised deployment if there was none).
Caveats
WorkerOnly+ multi-replica. Phase 9i (IDistributedLock) is unshipped; aWorkerOnly(orDispatcherOnly) silo withReplicaCount > 1duplicate-fires scheduler ticks. PinReplicaCount = 1on those silos. Web silos scale freely.- CLI command
dotnet toolup docker emitdeferred. Phase 16b's task list includes atoolup-adminCLI command for re-emitting a customised Dockerfile after changing the deployment's companion set. The CLI substrate (toolup-admin) is unshipped today; the command lands in a follow-up phase once the CLI is in place. Until then, re-emit viadotnet new platformsdk-docker --force(which overwrites at the solution root). - No image signing / SBOM step. The Dockerfile produces an OCI image; signing (cosign / Notary v2) and SBOM generation (
syft,dotnet sbom-tool) are consumer responsibilities. Long-term forge phase. - Streaming. SSE works through the standard Kestrel-on-Docker stack with no special config — pass-through behaviour, unlike
ToolUp.Hosts.AwsLambdawhere buffered-response is the default and streaming is a separate code path.
Forwarded-headers interaction
Phase 16d (forwarded-headers default-on, shipped 0.4.4) made TrustForwardedHeaders = true the SDK default. This companion's Dockerfile.template does not set TOOLUP_TRUST_FORWARDED_HEADERS=1 — it would be redundant. Reverse-proxy deployments (App Service ingress / Cloud Run front door / ALB / nginx / Traefik) work out of the box.
Set TOOLUP_TRUST_FORWARDED_HEADERS=0 explicitly only when you intend to opt out of the default trust (e.g. Kestrel exposed directly to the public internet without a proxy).