OpenClaw dashboard — browser sessions, media files, and future log viewer. Served through the Cloudflare Tunnel on a fixed port.
User browser → Cloudflare Edge → Tunnel → cloudflared (VPS host)
→ localhost:6090 → gateway:6090 (dashboard.mjs)
→ reads browsers.json → 127.0.0.1:<noVncPort> (browser container)
Browser sandbox containers run inside the gateway's nested Docker (Sysbox DinD). Each browser container serves noVNC on port 6080, Docker-mapped to a random host port inside the gateway container. The dashboard server (deploy/openclaw-stack/dashboard/server.mjs) runs inside the gateway and routes requests to the correct browser container based on dynamic port mappings.
The gateway tracks browser containers in ~/.openclaw/sandbox/browsers.json:
{
"entries": [
{
"containerName": "openclaw-sbx-browser-agent-main-...",
"sessionKey": "agent:main",
"cdpPort": 32768,
"noVncPort": 32769
},
{
"containerName": "openclaw-sbx-browser-agent-skills-...",
"sessionKey": "agent:skills",
"cdpPort": 32770,
"noVncPort": 32771
}
]
}The proxy reads this file on every request (no caching needed — the file is tiny). New entries appear when agents spawn browser containers for the first time.
The dashboard URL is configured per-claw in stack.yml under claws.<name>.domain and claws.<name>.dashboard_path:
| Variable | Purpose | Example |
|---|---|---|
OPENCLAW_DASHBOARD_DOMAIN |
Dashboard hostname | openclaw.example.com |
OPENCLAW_DASHBOARD_DOMAIN_PATH |
Dashboard base path | /dashboard (or empty for subdomain) |
| Setup | Domain | Path | DASHBOARD_BASE_PATH |
|---|---|---|---|
| Subpath on main domain | openclaw.example.com |
/dashboard |
/dashboard |
| Separate subdomain | dashboard-openclaw.example.com |
(empty) | (empty) |
OPENCLAW_DASHBOARD_DOMAIN_PATH is passed directly to the dashboard server as DASHBOARD_BASE_PATH — no parsing needed. The server strips this prefix from incoming requests and includes it in all generated URLs.
Auto-detection fallback: If DASHBOARD_BASE_PATH is empty but Cloudflare Tunnel sends requests with a path prefix (e.g., /dashboard/...), the server auto-detects the base path from the first unrecognized path segment. This prevents the dashboard from breaking if the env var is misconfigured. A log message indicates auto-detection occurred.
All paths below are relative to DASHBOARD_BASE_PATH (empty = root):
| URL (without base path) | Behavior |
|---|---|
/ |
Index page listing active browser sessions with live status |
/media/ |
Directory listing of agent media files |
/browser/<agent-id>/ |
Redirects to noVNC client |
/browser/<agent-id>/vnc.html?path=... |
noVNC client (proxied from browser container) |
/browser/<agent-id>/* |
HTTP proxy to browser container's noVNC static files |
/browser/<agent-id>/websockify (WebSocket) |
VNC stream proxy |
The ?path= query parameter tells the noVNC client where to connect the WebSocket. It includes the base path when set.
Examples:
- Subdomain:
https://dashboard-openclaw.example.com/browser/main/vnc.html?path=browser/main/websockify - Subpath:
https://openclaw.example.com/dashboard/browser/main/vnc.html?path=dashboard/browser/main/websockify
Node.js dashboard server (zero dependencies — built-in http module only). Reads DASHBOARD_BASE_PATH env var for subpath-aware routing. Handles:
- HTTP proxying: pipes request/response streams to the backend noVNC server
- WebSocket proxying: handles
upgradeevents, creates TCP socket to backend, pipes both directions - Health checking: TCP probes each container's noVNC port before proxying; shows friendly HTML error page if the container is down (avoids Cloudflare intercepting 502 errors)
- Index page: lists all registered sessions with live up/down status indicators, auto-refreshes every 10 seconds
The entrypoint starts the dashboard server as a background process. The dashboard is exposed on port 6090 (localhost-only via Docker port mapping) for tunnel access.
Add a route on the existing openclaw tunnel. Two approaches:
Option A: Separate subdomain (e.g., dashboard-openclaw.yourdomain.com)
| Subdomain | Domain | Path | Service |
|---|---|---|---|
dashboard-openclaw |
yourdomain.com |
(empty) | http://localhost:6090 |
Set OPENCLAW_DASHBOARD_DOMAIN=dashboard-openclaw.yourdomain.com and OPENCLAW_DASHBOARD_DOMAIN_PATH= (empty → DASHBOARD_BASE_PATH is empty).
Option B: Subpath on main domain (e.g., openclaw.yourdomain.com/dashboard)
| Subdomain | Domain | Path | Service |
|---|---|---|---|
openclaw |
yourdomain.com |
/dashboard |
http://localhost:6090 |
Set OPENCLAW_DASHBOARD_DOMAIN=openclaw.yourdomain.com and OPENCLAW_DASHBOARD_DOMAIN_PATH=/dashboard (→ DASHBOARD_BASE_PATH=/dashboard).
No new tunnel needed — just add a public hostname to the existing tunnel in the Dashboard.
Every request to the dashboard server (HTTP and WebSocket) must carry a valid Cf-Access-Jwt-Assertion header. The server verifies:
- Signature: cryptographically verified against Cloudflare's published public keys (fetched from the issuer's
/cdn-cgi/access/certsendpoint, cached 1 hour) - Expiration: rejected if
expclaim is in the past - Issuer: must be a
*.cloudflareaccess.comdomain - Audience (optional): if
CF_ACCESS_AUDis set, the JWT'saudclaim must match
Requests without a valid JWT get a 403 page. WebSocket upgrades without a valid JWT are silently destroyed.
Each Cloudflare Access application has a unique Application Audience (AUD) tag. Setting CF_ACCESS_AUD on the dashboard server ensures it only accepts JWTs issued for that specific application.
When to use it: Multi-instance setups where multiple OpenClaw deployments share a Cloudflare account. Without audience verification, a user authenticated for Instance A could access Instance B's browser sessions if Cloudflare Access path rules are misconfigured. The audience check provides defense-in-depth against upstream misconfiguration.
How to configure:
- Find your AUD tag: Cloudflare Dashboard > Zero Trust > Access > Applications > your app > Overview > Application Audience (AUD) Tag
- Add
CF_ACCESS_AUDto the gateway's environment indocker-compose.override.yml:
environment:
- CF_ACCESS_AUD=<your-application-aud-tag>- Restart the gateway (
docker compose up -d)
For single-instance deployments behind a properly configured Cloudflare Access application, the JWT signature and issuer checks alone provide sufficient protection.
Browser containers are started on-demand when an agent uses the browser tool and persist across agent turns within a session. They are stopped when:
- The gateway container restarts (
docker compose down/up) - The session ends
After a restart, browsers.json may still list stopped containers. The index page shows their status as "Stopped" with a red indicator, and clicking them shows a friendly "Browser Not Running" page instead of an error.
Each agent gets its own isolated browser container with separate:
- Chrome user data directory
- CDP port
- noVNC port
- Browser profiles
This avoids the concurrency problems of a shared browser sidecar approach.
- Gateway deployed with Sysbox (Docker-in-Docker)
- Cloudflare Tunnel connected
deploy/openclaw-stack/dashboard/server.mjsbind-mounted into the gateway container
- Go to Cloudflare Dashboard → Zero Trust → Networks → Tunnels
- Click your tunnel → Configure → Public Hostname tab
- Add a new public hostname pointing to
http://localhost:6090(see "Cloudflare Tunnel Route" above for subdomain vs subpath options) - Add a Cloudflare Access policy to restrict who can view browser sessions
- Set the domain and dashboard path in
stack.ymlunderclaws.<name>to match your chosen URL
# Dashboard is listening (use base path if configured)
sudo docker exec openclaw-stack-openclaw-main-claw curl -s http://127.0.0.1:6090/
# Or with base path:
sudo docker exec openclaw-stack-openclaw-main-claw curl -s http://127.0.0.1:6090/dashboard/
# Check startup log for base path
sudo docker logs openclaw-stack-openclaw-main-claw 2>&1 | grep 'dashboard'
# After a browser task runs, check session routing
sudo docker exec openclaw-stack-openclaw-main-claw curl -s http://127.0.0.1:6090/dashboard/browser/main/vnc.html
# External access via tunnel
curl -s https://<OPENCLAW_DASHBOARD_DOMAIN><OPENCLAW_DASHBOARD_DOMAIN_PATH>/The browser container isn't active. Send a browser task to the agent to start it, then refresh.
The noVNC WebSocket path is wrong. Ensure the URL includes ?path=browser/<agent-id>/websockify. The index page links include this automatically.
The dashboard server is returning a 5xx status. Check gateway logs:
sudo docker logs openclaw-stack-openclaw-main-claw 2>&1 | grep dashboardCheck that the dashboard is bind-mounted and the entrypoint reached Phase 2b:
sudo docker exec openclaw-stack-openclaw-main-claw ls -la /app/openclaw-stack/dashboard/server.mjs
sudo docker logs openclaw-stack-openclaw-main-claw 2>&1 | grep "Dashboard server"