Skip to content

zoexx/homelab-cloud-browser

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

homelab-cloud-browser

A self-hosted "cloud browser" for AI agents, designed to live on a Proxmox LXC on your home network. Headless Chromium with its DevTools Protocol (CDP) exposed over an authenticated reverse proxy on the LAN.

Drop-in target for browser-harness: set BU_CDP_URL=http://<lxc-ip>:9223/<token> and every helper in the library — new_tab, click_at_xy, http_get, the interaction-skills library — runs against this Chromium instead of your laptop's.

Not for public-internet exposure. There is no TLS in front; the auth model is a pre-shared token in the URL path, sized for LAN trust. If you need a public endpoint, put cloudflared / a reverse proxy with mTLS in front yourself.

Why this exists

Three patterns are common today, and all have problems:

  1. Drive your laptop's Chrome via CDP. Fine until an agent jumps in while you're working in the same browser and clicks the wrong tab.
  2. Wrap a headless Chromium in an MCP server. Works for MCP clients (Claude Desktop, Cursor) but flattens harness-style libraries into N round-trips and one screenshot per action.
  3. Pay for a hosted cloud browser. Works, costs money, leaves your homelab idle.

This repo is the third pattern, self-hosted. CDP in, CDP out, on your LAN. Two systemd services, one auth proxy, no MCP layer. Use it directly from any CDP client (browser-harness, Puppeteer, Playwright-over-CDP, raw cdp-use).

If you want an MCP-facing surface on top of the same Chromium, run qa-master-mcp alongside — that project's job is the MCP translation; this project's job is the browser.

Architecture

┌──── LAN ────┐                                            ┌──── LXC ────┐
│  agent or   │  ws://lxc:9223/<token>/devtools/...        │  cdp-auth   │
│  browser-   ├───────────────────────────────────────────>│  -proxy     │
│  harness    │  http://lxc:9223/<token>/json/version       │  :9223      │
│             │                                            │     │       │
└─────────────┘                                            │     ▼       │
                                                           │  chromium   │
                                                           │  headless   │
                                                           │  :9222 loop │
                                                           └─────────────┘
  • chromium-headless.service runs Chromium with --remote-debugging-port=9222 bound to 127.0.0.1 (loopback only — never directly exposed).
  • cdp-auth-proxy.service listens on 0.0.0.0:9223. It strips the /<token>/ prefix, forwards HTTP and WebSocket traffic to Chromium, and rewrites webSocketDebuggerUrl in JSON responses so clients keep talking through the proxy.
  • The token lives in /etc/homelab-cloud-browser/token, generated at install time. Rotate it by overwriting the file and systemctl restart cdp-auth-proxy.

Quick install (Proxmox LXC)

Once accepted upstream, one command from the Proxmox shell:

bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/homelab-cloud-browser.sh)"

Until then, see manual install below.

After install, the script prints:

  • LXC IP
  • The generated token
  • The full BU_CDP_URL value to copy into your shell

Manual install (on an existing Debian 12/13 LXC)

ssh root@<lxc-ip>
curl -fsSL https://raw.githubusercontent.com/zoexx/homelab-cloud-browser/main/install.sh | bash

Or clone and run locally:

git clone https://github.com/zoexx/homelab-cloud-browser.git
cd homelab-cloud-browser
sudo ./install.sh

The installer is idempotent. Re-running it preserves the existing token.

Usage

From browser-harness

export BU_CDP_URL="http://192.168.1.117:9223/REPLACE_WITH_TOKEN"

browser-harness <<'PY'
new_tab("https://example.com")
wait_for_load()
print(page_info())
PY

That's it. No start_remote_daemon call needed — BU_CDP_URL is browser-harness's documented escape hatch for arbitrary CDP endpoints, and this server speaks the same /json/version discovery + WebSocket protocol that Browser Use's cloud uses.

Parallel lanes (multi-agent)

Each connecting WebSocket gets its own Chromium browser context server-side — separate tabs, separate cookies, fully isolated. Two clients can drive the LXC at once without seeing each other's tabs.

browser-harness already has the right knob: BU_NAME selects the laptop-side daemon process. Each distinct BU_NAME opens its own WebSocket to this proxy → its own context. Reuse the same BU_NAME to keep state across invocations in the same shell; use different BU_NAME per agent or task to get independence.

# These two won't see each other's tabs.
BU_NAME=satisfaitla browser-harness <<'PY'
new_tab("https://satisfaitla-staging.example.com")
PY

BU_NAME=research browser-harness <<'PY'
new_tab("https://news.ycombinator.com")
PY

Lifecycle: a context lives as long as its WebSocket is open. When the WebSocket closes (daemon stops, network drops, laptop sleeps past the 30-second heartbeat), the context is disposed and its tabs are gone. There is no on-disk persistence per lane.

Health check

GET /<token>/health returns proxy state as JSON:

curl -s "$BU_CDP_URL/health" | jq .
# {
#   "uptime_s": 12345,
#   "last_cdp_ok_age_s": 2,
#   "contexts": 3,
#   "ws_open": 3,
#   "ws_total_opened": 17
# }

uptime_s is the proxy's uptime, last_cdp_ok_age_s is seconds since the last successful upstream CDP call (null if none yet), contexts / ws_open track currently isolated lanes, ws_total_opened is a cumulative counter useful for spotting churn.

From raw CDP

TOKEN="..."
curl -s "http://192.168.1.117:9223/$TOKEN/json/version" | jq .
# {
#   "Browser": "HeadlessChrome/...",
#   "webSocketDebuggerUrl": "ws://192.168.1.117:9223/<token>/devtools/browser/<uuid>",
#   ...
# }

The webSocketDebuggerUrl already points back through the proxy with the token prefix, so any CDP client that does /json/version discovery will work without further configuration.

Configuration

/etc/homelab-cloud-browser/cloud-browser.env controls the proxy and the Chromium wrapper:

Var Default Notes
CDP_PROXY_BIND 0.0.0.0 Bind address for the proxy. Use 127.0.0.1 to require an SSH tunnel.
CDP_PROXY_PORT 9223 LAN-facing proxy port.
CHROMIUM_DEBUG_PORT 9222 Internal loopback port. No reason to change this.
CHROMIUM_USER_DATA_DIR /var/lib/homelab-cloud-browser/profile Persistent profile.
CHROMIUM_WINDOW_SIZE 1280,800 Default viewport.
CHROMIUM_EXTRA_ARGS (empty) Extra flags appended to chromium command.

After a Chromium restart

If Chromium restarts on the LXC — systemd will do this automatically on crash, or after systemctl restart chromium-headless — any browser-harness daemon already running on your laptop holds a now-dead WebSocket. The next call from that daemon will hang until its read timeout. Bounce the laptop-side daemon by killing the process (pkill -f browser_harness.daemon) or by starting a fresh browser-harness invocation with a different BU_NAME. The new daemon will open a fresh WebSocket to the proxy and get a fresh isolated context.

Security model

  • Token-in-path bearer. Anyone who learns the token gets full browser control: navigate any URL, execute arbitrary JS, read cookies, attach debuggers. Treat it like an SSH private key.
  • LAN-only by default. No TLS. Do not forward :9223 from your router.
  • No per-client isolation in this layer. Two clients sharing the same token share the same Chromium profile and can see each other's tabs. If you need isolation, run multiple LXCs or use qa-master-mcp (which keys per MCP session) in front.
  • Chromium runs with --no-sandbox. Safe inside an unprivileged LXC (the LXC itself is the sandbox), but don't render attacker-controlled URLs on this profile if you store sensitive cookies in it. Use a separate LXC with a separate token for adversarial sites.

What this is not

  • Not a public SaaS. No TLS, no rate limiting, no metering.
  • Not multi-tenant. Multiple parallel agents are fine; multiple distrustful users are not.
  • Not an MCP server. If your client only speaks MCP, run qa-master-mcp against this endpoint (point BU_CDP_URL at it instead of at a local Chromium).
  • Not a Playwright wrapper. CDP only. Playwright clients that can speak CDP (most of them, via chromium.connectOverCDP) will work fine.

Layout

deploy/
  chromium-headless              wrapper that builds the chromium command line
  chromium-headless.service      systemd unit running the wrapper as `bcb` user
  cdp-auth-proxy.py              the auth proxy (aiohttp)
  cdp-auth-proxy.service         systemd unit
  cloud-browser.env.example      env template

community-scripts/
  ct/homelab-cloud-browser.sh           entry script for community-scripts
  install/homelab-cloud-browser-install.sh   inner installer

install.sh                       canonical installer (manual-install path)

License

MIT.

About

Self-hosted CDP browser endpoint on a Proxmox LXC. Headless Chromium + auth proxy, drop-in for browser-harness via BU_CDP_URL.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors