HTTPS and domains
Serve the Instance on a real domain — bundled Caddy or your own proxy
The stack serves plain HTTP — published on all host interfaces by default
(SPECBENCH_APP_BIND narrows that). To put it on a domain, behind TLS, you
need a reverse proxy in front — either the bundled Caddy (nothing else to
install, certificates handled for you) or a proxy you already run.
Either way, only one route matters: the web service already folds the API into the same origin, so the proxy forwards a single host to the web port. The MCP server is the one extra — its own port, usually its own hostname.
Option 1: the bundled Caddy
The compose file ships a proxy profile with Caddy,
which obtains and renews Let's Encrypt certificates automatically.
-
Point DNS at the host: an
A/AAAArecord for your domain and one formcp.<domain>. If your DNS provider can proxy traffic (Cloudflare's orange cloud, for instance), turn that off for these records — a proxy in front takes over TLS, and Let's Encrypt must reach this Caddy directly. -
Open ports 80 and 443 to the internet (Let's Encrypt needs both).
-
In the
.envnext to the compose file:SPECBENCH_DOMAIN=specs.example.com SPECBENCH_WEB_ORIGIN=https://specs.example.com -
Start with the profile:
docker compose --profile proxy up -d
That's it — the app is on https://specs.example.com, the MCP server on
https://mcp.specs.example.com (set SPECBENCH_MCP_DOMAIN to serve it on a
different hostname). Certificates live on the caddy-data volume;
leave it in place so renewals don't start from scratch.
One Compose quirk to know: the domain variables are baked into Caddy's config
at container creation, and a plain up -d does not recreate Caddy when
they change. After editing SPECBENCH_DOMAIN or SPECBENCH_MCP_DOMAIN on a
running stack, force it:
docker compose --profile proxy up -d --force-recreate caddyWith SPECBENCH_DOMAIN unset, Caddy serves https://localhost signed by its
own local CA — a way to smoke-test the TLS path on a laptop.
If the browser reports an SSL protocol error on your domain right after
starting, that unset-domain fallback is almost always what's happening:
SPECBENCH_DOMAIN never reached Compose (a # left on the line in the
.env is the classic cause), so Caddy is serving localhost and has no
certificate for your hostname. Verify with
docker compose --profile proxy config | grep <your-domain>, fix the .env,
and up -d again — and if the passwords were also still commented on the
first start, Postgres initialised with the defaults baked in:
docker compose --profile proxy down -v for a clean start. Certificate
issuance itself is watchable with docker compose logs -f caddy.
Option 2: your own proxy
Already running Traefik, nginx, or anything else on the host? Skip the profile and add two routes:
specs.example.com→localhost:5173(the web service). The route must allow WebSocket upgrades — realtime runs over SignalR.mcp.specs.example.com→localhost:8081(the MCP server). Agents authenticate with a bearer token, so TLS matters here too. Forward theHostandX-Forwarded-Protoheaders: the MCP server names itself to agents that sign in from what it sees there.
Then set the origin the API should trust:
SPECBENCH_WEB_ORIGIN=https://specs.example.comThe web service passes X-Forwarded-For and X-Forwarded-Proto through to
the API, so send those from your proxy (Caddy and Traefik do by default).
After the proxy is up
The direct app ports (5173, 5174, 8081) are still published on all interfaces by default. Once traffic comes through the proxy, close them to the network with:
SPECBENCH_APP_BIND=127.0.0.1Nothing else changes: the bundled Caddy reaches the services over the compose network, and a proxy of your own still reaches them on the loopback ports — which also stay available from the host for debugging. Postgres and Redis already bind to localhost only.