CDP access in aiod splits by design into two roles: the daemon connects to Chromium as a client, and a reverse proxy in front of it exposes Chromium's debugging port to the outside.
The daemon implements only the client role. This page describes that split and what a deployment has to provide for the other half.
BROWSER_REMOTE_DEBUGGING_HOST:PORT (default 127.0.0.1:9222) for its own browser tools, for capability probing, and for the browser MCP tools.GET /v1/browser/info with cdp_url, built from the request's X-Forwarded-Proto, X-Forwarded-Host, and X-Forwarded-Prefix headers. Host is the fallback. The shape: <scheme>://<host><prefix>/cdp/devtools/browser/<id>.The daemon does not serve /cdp/*. The shape is fixed — Chromium reachable under the sandbox's single public origin, at the /cdp/ prefix — but not the component behind it: serving that path is the deployment's job.
/cdp/*Serving /cdp/json/* and relaying /cdp/devtools/* WebSockets are both a reverse proxy's job:
/json/version and /json/list return URLs starting with ws://127.0.0.1:9222/. The proxy replaces that prefix with the public origin plus /cdp/, and the ws= form inside devtoolsFrontendUrl the same way./cdp/devtools/<rest> becomes /devtools/<rest> on port 9222. The Upgrade headers pass through, with a long read timeout.Two details make the rewrite work:
/json/* requests whose Host is not an IP address or localhost. The proxy sends Host: 127.0.0.1:9222 upstream.Accept-Encoding upstream.nginx on the AIO and Computer images implements both locations. Here is the config:
$cdp_scheme, $cdp_host, and $cdp_prefix are derived from the X-Forwarded-* headers. cdp_url stays correct behind a second proxy or an ingress that adds a path prefix.
For example, set these request headers:
info then answers wss://sandbox.example.com/s/abc/cdp/devtools/browser/<id>.
On a host without the image's gateway, the deployment's own reverse proxy does the same two things. On Windows this is the reverse proxy in front of the host. It rewrites the discovery JSON and forwards /cdp/devtools/* to port 9222, with the same header rules.
Any proxy that can forward WebSockets and rewrite a response body works. The daemon needs only the X-Forwarded-* headers from it.
cdp_url is path-shaped: wss://sandbox.example.com/cdp/devtools/browser/<id>. Playwright's connect_over_cdp and Puppeteer's browserWSEndpoint accept it as is. Playwright also accepts {base_url}/cdp as an HTTP endpoint: it appends json/version/ and keeps the query string.
Some clients accept only http://host:port with no path:
browserURL--browserUrlThey need port-shaped access, a hostname that terminates at Chromium's port 9222. A deployment that gives each sandbox its own hostname can expose the port as a subdomain or a dedicated listener. The same proxy forwards it to 9222.
The daemon is not involved in either shape. Its browser tools keep connecting to BROWSER_REMOTE_DEBUGGING_PORT, whether Chromium or a forwarder answers there.
A browser-side WebSocket cannot set headers. When the gateway requires a key, append ?api_key=<key> to cdp_url. info echoes its own query string onto both URLs it returns, so GET /v1/browser/info?api_key=<key> hands back a cdp_url that connects as is. The rule is the same as for every other WebSocket route.
Never expose port 9222 outside a trusted network: Chromium's debugging port has no authentication of its own.