Self-Hosted, Non-Azure Docker
Everything so far assumed Azure: Azure OpenAI, Azure Key Vault, Microsoft Entra sign-in. None of that is required. This page runs the harness headless — API and real-time messaging only, no web dashboard — in a container on a plain Linux box, an x64 workstation, or an Apple Silicon Mac, with zero Azure configuration anywhere. This is the exact path proven while adopting the harness as the runtime for a Discord-based AI companion running on a Mac Mini with no Azure tenant at all (issue #591).
Presentation.AgentHub — the same SignalR-based service you'd otherwise
run with dotnet run. Nothing new was built to make this work; the
composition root
(The Big Picture) already builds cleanly with zero
Azure configuration. This page is about packaging that fact into a container and
closing the two real gaps that only show up once Azure identity genuinely isn't
there.
What you get, and what you give up
Read this before you deploy — the trade-offs are deliberate, not oversights, but you should make them with eyes open.
Build and run
One Dockerfile builds images for both Intel/AMD (x64) and Apple
Silicon/ARM servers (arm64) machines — the underlying publish is
framework-dependent, so no per-architecture build step is needed.
src/
Presentation.AgentHub pulls its skill and agent definitions from two
folders that live above src/. Building from src/
alone (the pattern the Azure/Foundry container uses) builds and boots without error,
then fails every agent-turn request because no agents were ever packaged into the
image. The commands below already account for this.
-
Configure your provider
From the repository root:
bashcp deploy/agenthub/.env.example deploy/agenthub/.env # edit deploy/agenthub/.env: set your provider API key, endpoint, and modeldeploy/agenthub/.envis already excluded from version control. Never put real credentials in any trackedappsettings*.jsonfile — see Configuration Reference for the full secrets story. -
Build and runbash
docker compose -f deploy/agenthub/docker-compose.yml --env-file deploy/agenthub/.env up --buildFor a multi-architecture image outside compose (e.g. to push to a registry both an x64 CI runner and an arm64 device will pull from):
bashdocker buildx build --platform linux/amd64,linux/arm64 \ -f src/Content/Presentation/Presentation.AgentHub/Dockerfile \ -t agenthub:latest . -
Verifybash
curl http://localhost:8080/health/ai curl http://localhost:8080/health/subsystems/health/aiconfirms the model provider is reachable./health/subsystemsis new to this deployment path — see below for what it reports and why it exists.
The two container-specific gotchas
Both were found the hard way, on real hardware, during the harness's own adoption as a Discord companion's runtime. Both are already fixed in the shipped Dockerfile and container settings — this section explains why they exist, in case you hit a variant of either one.
1 · Kestrel needs both endpoints overridden, not just one
The default configuration binds an HTTP and an HTTPS endpoint. A fresh container has no TLS certificate, so if only the HTTP endpoint is redirected to a plain port, the host still fails outright at startup trying to configure HTTPS. The Dockerfile sets both:
ENV Kestrel__Endpoints__Http__Url=http://+:8080 \
Kestrel__Endpoints__Https__Url=http://+:8081
Also note that ASPNETCORE_URLS is silently ignored whenever
Kestrel:Endpoints is set in configuration — which it is here — so that
environment variable is not a working alternative.
2 · Prompt caching and the storage paths need Linux-shaped values
The default configuration ships with a Windows-style conversation storage path, which only matters on Linux the moment a conversation is actually written. The container's own settings file redirects every persistent path — conversation history, planner state, governance state, logs — to one directory, mounted as a single Docker volume so nothing is lost on restart.
Authentication: off by default, and why
The harness normally requires signing in through Microsoft Entra. A self-hosted deployment usually has no Entra tenant at all, so the container ships with two settings that together turn sign-in off entirely:
"Auth": {
"Disabled": true,
"AllowOutsideDevelopment": true
}
Both flags are required together, deliberately. Auth:Disabled alone only takes
effect in the Development environment — it does nothing in a production-configured
container. AllowOutsideDevelopment is the explicit, reviewable decision to
accept that trade-off outside Development, instead of the alternative of mislabeling a real
deployment as "Development" (which also silently turns off unrelated hardening, like forced
HTTPS).
Treat network placement as your access control here: put this behind a private
network, a VPN, or a reverse proxy that enforces its own authentication before
traffic reaches the container. If you do have an Entra tenant, set both flags back
to false and configure Entra normally — everything else on this page
still applies.
"Use it" means ordinary agent turns only — the auto-authenticated identity this mode grants deliberately holds no elevated roles. It cannot approve escalations, decide change proposals, or trigger drift/registry operate actions. Someone who reaches the port can talk to the agent; they cannot administer the deployment.
Verifying the no-Azure guarantee
Two independent layers guarantee this container never reaches out to Azure for configuration, and you can check both yourself rather than take it on faith:
-
An explicit
DisableAzureConfigSourcesflag, set both as a container environment variable and in the shipped settings file, forces Azure Key Vault and Azure App Configuration off regardless of anything else present in the environment. -
GET /health/subsystemsreports, among other things, whether either Azure configuration provider actually loaded at startup — not merely whether the flag is set, but whether it worked:
{
"aiProvider": "OpenAI",
"aiProviderConfigured": true,
"authMode": "disabled",
"azureKeyVaultConfigSourceLoaded": false,
"azureAppConfigurationSourceLoaded": false,
"cacheBackend": "None",
"sandboxEnabled": true,
"governanceEnabled": true,
"otlpEnabled": false
}
This endpoint reports names, enum values, and booleans only — the same convention
/health/ai and GET /api/config/status already follow. It never
returns a secret, a connection string, or a file path.
What just happened
Nothing about the harness's core composition changed for this deployment mode — every Azure service registration was already conditional on Azure configuration actually being present. What this page added was the missing packaging and two pieces of hardening that only matter once that configuration is genuinely, permanently absent: a real sign-in off-switch that doesn't require lying about the environment, and an explicit, verifiable guarantee that no Azure configuration source will ever be attempted.