fix(tls): pivot per-runner TLS to path-routing on single subdomain
All checks were successful
Deploy to Production / deploy (push) Successful in 54s
All checks were successful
Deploy to Production / deploy (push) Successful in 54s
The per-subdomain approach (*.mcp.buildmymcpserver.com) failed at the Cloudflare edge — Universal SSL only covers ONE-level wildcards, so the TLS handshake on slug.mcp.buildmymcpserver.com hits SSL alert 40 handshake_failure. The two paths to fix that (CF Advanced Cert Manager at $10/mo, or a Let's-Encrypt wildcard via DNS-01 with certbot) both trade either money or ops for the URL aesthetic. Pivot to path-routing on the single subdomain mcp.buildmymcpserver.com, which IS covered by free Universal SSL. publicUrl format changes from https://<slug>.mcp.buildmymcpserver.com → https://mcp.buildmymcpserver.com/<slug> No recurring cost, works with the existing CF setup, MCP clients don't care about the URL shape (it comes from the wizard's install snippet). Code changes: - generator/lib/deploy.ts: * publicUrl computed as `${MCP_DOMAIN}/${slug}` instead of `${slug}.${MCP_DOMAIN}` * writeRunnerMapEntry writes one-line nginx snippet: if ($bmm_slug = "<slug>") { set $bmm_port <port>; } (was: a map-entry pair "<slug>.<MCP_DOMAIN> <port>;") - setup-runner-tls.sh: * nginx vhost is now single server_name mcp.buildmymcpserver.com * regex location captures (?<bmm_slug>...)(?<bmm_path>/.*)? * includes runner-map.combined inside the location block so the generated if-snippets set $bmm_port; unknown slug → 404 * proxy_pass strips the slug prefix: /<slug>/foo → 127.0.0.1:port/foo * Prereq docs updated: just A-record for mcp (no wildcard needed), same Origin CA cert reused * Added /health endpoint at vhost root for monitoring Systemd watcher + map dir + volume mounts unchanged — same file paths, just different snippet content. Re-running setup-runner-tls.sh on the host overwrites the wildcard vhost with the new path-based one. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,11 +5,19 @@ import { createDb, eq, isNotNull, mcpServers } from '@bmm/db';
|
||||
import { config } from '../config.js';
|
||||
|
||||
/**
|
||||
* Per-runner subdomain TLS support. When MCP_DOMAIN is set, the generator
|
||||
* publishes each container under https://<slug>.<MCP_DOMAIN> via a host-side
|
||||
* nginx that reads a slug→port map. The generator writes a tiny config
|
||||
* fragment per server; a systemd inotify watcher combines them and reloads
|
||||
* nginx. See scripts/setup-runner-tls.sh for the one-time host setup.
|
||||
* Per-runner TLS via path-routing on mcp.buildmymcpserver.com. When
|
||||
* MCP_DOMAIN is set, the generator publishes each container at
|
||||
* https://<MCP_DOMAIN>/<slug>
|
||||
* and writes a one-line nginx snippet per server into RUNNER_MAP_DIR.
|
||||
* A host-side systemd inotify watcher combines the snippets into a single
|
||||
* file that the nginx vhost includes inside its location block, mapping
|
||||
* the captured slug to its local runner port.
|
||||
*
|
||||
* Path-routing (instead of per-subdomain) is the bootstrap-friendly choice:
|
||||
* mcp.buildmymcpserver.com is covered by Cloudflare's free Universal SSL,
|
||||
* whereas *.mcp.buildmymcpserver.com would need CF Advanced Cert Manager
|
||||
* ($10/mo) or a custom Let's-Encrypt wildcard via DNS-01 (free but more
|
||||
* ops). See scripts/setup-runner-tls.sh for the one-time host setup.
|
||||
*
|
||||
* If MCP_DOMAIN is unset, both the URL formatter and the map writer no-op
|
||||
* and we fall back to the legacy http://host:port URL — zero behaviour
|
||||
@@ -21,7 +29,10 @@ function runnerMapPath(slug: string): string {
|
||||
|
||||
async function writeRunnerMapEntry(slug: string, port: number): Promise<void> {
|
||||
if (!config.MCP_DOMAIN) return;
|
||||
const line = `${slug}.${config.MCP_DOMAIN} ${port};\n`;
|
||||
// nginx snippet — included inside a `location ~` block that captures
|
||||
// $bmm_slug. Each runner contributes one line; the systemd watcher
|
||||
// concatenates them into /opt/buildmymcpserver/runner-map.combined.
|
||||
const line = `if ($bmm_slug = "${slug}") { set $bmm_port ${port}; }\n`;
|
||||
try {
|
||||
await fs.mkdir(config.RUNNER_MAP_DIR, { recursive: true });
|
||||
await fs.writeFile(runnerMapPath(slug), line, 'utf8');
|
||||
@@ -42,7 +53,7 @@ async function removeRunnerMapEntry(slug: string): Promise<void> {
|
||||
}
|
||||
|
||||
function computePublicUrl(slug: string, port: number): string {
|
||||
if (config.MCP_DOMAIN) return `https://${slug}.${config.MCP_DOMAIN}`;
|
||||
if (config.MCP_DOMAIN) return `https://${config.MCP_DOMAIN}/${slug}`;
|
||||
return `http://${config.RUNNER_HOST}:${port}`;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user