#!/usr/bin/env bash # setup-runner-tls.sh # # One-time host setup for per-runner TLS via path-routing on # mcp.buildmymcpserver.com. Run as root on the BMM host AFTER you've done # the prereqs below. Idempotent — safe to re-run. # # Why path-routing instead of per-subdomain: Cloudflare's free Universal # SSL only covers ONE-level wildcards (*.buildmymcpserver.com). A # two-level wildcard (*.mcp.buildmymcpserver.com) needs CF Advanced # Certificate Manager ($10/mo) OR a Let's Encrypt wildcard via certbot # + DNS-01. To keep recurring cost at zero, we serve every runner under # the single subdomain mcp.buildmymcpserver.com — which IS covered by # Universal SSL — and route by path: https://mcp..//mcp. # # What it does: # 1. Creates /opt/buildmymcpserver/runner-map/ (volume-mounted into bmm-api # and bmm-generator — they drop one .conf fragment per live runner) # 2. Installs an nginx vhost that catches *.mcp.buildmymcpserver.com, # reads slug→port from a combined map file, and reverse-proxies to the # runner on localhost # 3. Installs a systemd service that inotify-watches the map dir, combines # all fragments into a single map file, and reloads nginx on any change # # After this script: # - In docker-compose.prod.yml, add a volume mount to BOTH api and generator: # volumes: # - /opt/buildmymcpserver/runner-map:/var/runner-map # - In .env.production, add: # MCP_DOMAIN=mcp.buildmymcpserver.com # - docker compose up -d --force-recreate api generator # - From now on every deployed runner gets https://.mcp.buildmymcpserver.com # # ─── PREREQS (do these in Cloudflare dashboard before running) ─────────── # A. DNS: Add an A-record 'mcp.buildmymcpserver.com' → 213.239.213.217 # Proxy status: Proxied (orange cloud) # (The wildcard *.mcp record from earlier setup is no longer # needed for path-routing — safe to leave or delete.) # B. SSL: Cloudflare → SSL/TLS → Origin Server → Create Certificate # (likely already done — the same Origin cert from the prior # setup attempt covers mcp.buildmymcpserver.com too, since # you generated it with all four hostnames in the SAN list). # Path: /etc/ssl/buildmymcpserver/mcp-runners.crt (644) # /etc/ssl/buildmymcpserver/mcp-runners.key (600 root:root) # C. SSL mode: Cloudflare → SSL/TLS → Overview → "Full (strict)" # # Run: sudo bash scripts/setup-runner-tls.sh set -euo pipefail if [[ "${EUID}" -ne 0 ]]; then echo "Run as root (sudo bash $0)." exit 1 fi MAP_DIR="/opt/buildmymcpserver/runner-map" COMBINED="/opt/buildmymcpserver/runner-map.combined" VHOST_DST="/etc/nginx/sites-available/bmm-mcp-runners" VHOST_LNK="/etc/nginx/sites-enabled/bmm-mcp-runners" CERT="/etc/ssl/buildmymcpserver/mcp-runners.crt" KEY="/etc/ssl/buildmymcpserver/mcp-runners.key" echo "─── checking prereqs ───────────────────────────────────────" for f in "$CERT" "$KEY"; do if [[ ! -f "$f" ]]; then echo "MISSING: $f — see PREREQS at the top of this script." exit 1 fi done if ! command -v inotifywait >/dev/null; then echo "Installing inotify-tools…" apt-get update -qq apt-get install -y -qq inotify-tools fi echo "─── creating map dir + initial combined file ──────────────" mkdir -p "$MAP_DIR" chmod 755 "$MAP_DIR" touch "$COMBINED" chmod 644 "$COMBINED" echo "─── writing nginx vhost ──────────────────────────────────" cat > "$VHOST_DST" <<'NGINX' # BMM per-runner proxy via PATH routing on mcp.buildmymcpserver.com. # The combined snippet file (regenerated by the bmm-api and bmm-generator # containers + reloaded by the systemd inotify watcher) is a sequence of # `if ($bmm_slug = "") { set $bmm_port ; }` lines that map the # slug captured from the URL path to the local runner port. server { listen 80; listen [::]:80; listen 443 ssl http2; listen [::]:443 ssl http2; server_name mcp.buildmymcpserver.com; ssl_certificate /etc/ssl/buildmymcpserver/mcp-runners.crt; ssl_certificate_key /etc/ssl/buildmymcpserver/mcp-runners.key; client_max_body_size 4M; # Cheap health probe for monitoring — doesn't go through the slug router. location = /health { return 200 "ok\n"; add_header Content-Type text/plain; } # // → 127.0.0.1:/ location ~ ^/(?[a-z0-9][a-z0-9-]*)(?/.*)?$ { set $bmm_port ""; include /opt/buildmymcpserver/runner-map.combined; # Unknown slug — return 404 instead of a confusing default. if ($bmm_port = "") { return 404; } # Default sub-path to / when client asked for / with no trailing slash. set $bmm_target_path $bmm_path; if ($bmm_target_path = "") { set $bmm_target_path "/"; } proxy_pass http://127.0.0.1:$bmm_port$bmm_target_path; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto https; # MCP uses Streamable HTTP — disable buffering so response chunks flow. proxy_buffering off; proxy_cache off; proxy_read_timeout 600s; } # Root of mcp.buildmymcpserver.com — nothing to serve here. location = / { return 404; } } NGINX ln -sf "$VHOST_DST" "$VHOST_LNK" echo "─── writing systemd watcher service ──────────────────────" cat > /etc/systemd/system/bmm-runner-map.service </dev/null > ${COMBINED} || true; /usr/sbin/nginx -t && /usr/sbin/nginx -s reload || true' ExecStart=/bin/bash -c 'while inotifywait -q -e create,modify,delete,moved_to,moved_from ${MAP_DIR}; do cat ${MAP_DIR}/*.conf 2>/dev/null > ${COMBINED} || true; /usr/sbin/nginx -t && /usr/sbin/nginx -s reload; done' Restart=always RestartSec=2 [Install] WantedBy=multi-user.target EOF systemctl daemon-reload systemctl enable --now bmm-runner-map echo "─── verifying nginx config + reload ──────────────────────" nginx -t nginx -s reload || true echo "" echo "─── DONE ─────────────────────────────────────────────────" echo "" echo "Next steps (one-time):" echo "" echo "1) Edit /opt/buildmymcpserver/docker-compose.prod.yml — add to BOTH" echo " the 'api' and 'generator' services:" echo "" echo " volumes:" echo " - /opt/buildmymcpserver/runner-map:/var/runner-map" echo "" echo "2) Edit /opt/buildmymcpserver/.env.production — add:" echo "" echo " MCP_DOMAIN=mcp.buildmymcpserver.com" echo "" echo "3) Restart api + generator so they pick up the env + volume:" echo "" echo " cd /opt/buildmymcpserver" echo " docker compose --env-file .env.production -f docker-compose.prod.yml \\" echo " up -d --force-recreate api generator" echo "" echo "Test (after at least one runner has been deployed):" echo " curl -I https://.mcp.buildmymcpserver.com/health" echo "" echo "If you ever need to verify the map state:" echo " cat ${COMBINED}" echo " systemctl status bmm-runner-map"