Two overlapping bugs were killing OAuth discovery for every external
MCP client (Claude Desktop, Cursor, etc.):
1. worker.ts injected PUBLIC_URL=http://<RUNNER_HOST>:<port> into the
runner container even when MCP_DOMAIN was set. Result: the runner's
/.well-known/oauth-protected-resource advertised an unreachable URL
and the WWW-Authenticate header pointed at a non-HTTPS loopback
address. Claude Desktop refused to follow the discovery chain.
Now derives PUBLIC_URL from the same computePublicUrl() helper that
builds the user-visible URL stored in mcp_servers.public_url, so the
container's self-reported resource matches its actual route.
2. docker-compose.prod.yml never mounted /opt/buildmymcpserver/runner-map
into the api / generator containers. The .conf snippet written by
the generator landed in an ephemeral container path; the host
inotify watcher saw an empty directory and produced an empty
runner-map.combined. Result: nginx 404'd every /<slug>/* request,
the runner was unreachable from the public domain, and OAuth
discovery couldn't even begin. Mount added to both services.
Existing weather server has the wrong PUBLIC_URL baked in and must be
recreated after deploy. No customers yet.
export computePublicUrl from deploy.ts so worker.ts can call it.
Server recon (read-only SSH) showed the box already runs ~8 apps behind a
host-level nginx, with Gitea + an Actions runner. The host-networking
design collided with contentra on port 3001.
- docker-compose.prod.yml: bridge networking + per-app network, house
style; api/web/postgres/redis publish to 127.0.0.1 on verified-free
ports (4000/4001/5440/6390); only the generator keeps host networking
(no listening port, needs the host namespace for runner-port probing).
- Drop the Traefik config; the box uses a host nginx. Add a ready nginx
vhost in infra/nginx/buildmymcpserver.conf (listen 80, Cloudflare TLS).
- Add .gitea/workflows/deploy.yml mirroring the buildmydiscord pipeline.
- Narrow the generated-MCP port range to 4400-4900 (clear of screencraft
on 4321).
- .env.production.example + DEPLOY.md rewritten for buildmymcpserver.com
and the real topology.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Multi-stage Dockerfiles for web/api/generator (pnpm workspace install,
tsx runtime — workspace packages are raw TS, same model as runner-template).
- docker-compose.prod.yml: postgres + redis + the three app services.
api/generator/web use host networking so the generator's host-port probe
is correct and every service shares one address space; api + generator
mount the Docker socket. Binds nothing on 80/443 — safe beside other apps.
- Optional Traefik reverse proxy in infra/traefik/ (heavily gated — only if
the box has no existing proxy).
- .env.production.example, .dockerignore, DEPLOY.md (Cloudflare zone, GoDaddy
nameserver switch, server deploy, Google Cloud Console OAuth app).
- api/generator `start` now runs via tsx; `node dist/index.js` could never
resolve the raw-TS workspace imports.
All three images verified building clean; the API container boots under tsx.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>