browser-remote — a real, clickable window into a headless Chrome — Spunto<br>Get started<br>Blog
Mathieu<br>Founder, Spunto
I run a fair amount of browser automation these days — scripts that log into things, click through onboarding flows, scrape a page, or just check that a deploy didn't break some form somewhere. Headless Chrome handles all of that fine, right up until something goes wrong. Then you're left staring at a stack trace and a screenshot taken three steps too late, trying to reconstruct what the page actually looked like when it happened.
What I wanted was much dumber than that: a normal-looking browser window, with real tabs, that I could just look at and click into whenever I needed to. Not a recorded video, not a screenshot dump — a live view, the kind you'd get from screen-sharing someone's laptop.
So I built browser-remote: one Docker image, one port, a headless Chromium inside it, and a small web UI that gives you a tab bar and a live screen for that browser. Point your own browser at it and you're driving.
What it actually looks like
The control UI — a tab bar, an address bar, and a live view of whatever the headless browser is currently showing<br>That's not a mockup — it's the actual UI, open on its own GitHub repo, which felt like the right thing to point it at for a screenshot. Tabs, back/forward/reload, an address bar you can type into, and the page itself streamed live as you interact with it.
Why not just use browserless / Selenium Grid / noVNC
Because I didn't want three moving parts where one would do. The usual way to get a "watchable" headless browser is either:
browserless or similar — great, but it's another service to run, with its own opinions about session pooling and licensing, on top of the Chrome it wraps.
A VNC-based setup — a full X server plus a VNC server plus a web client, to stream what amounts to a whole virtual desktop just to look at one browser window.
Both work. Both are also more infrastructure than the problem needs. Chrome already has a built-in way to stream exactly its own pixels and forward input back — the DevTools Protocol. browser-remote is just that, wired up directly: Chromium (the BSD-licensed one, apt install chromium, no separate binary to trust) plus a thin Node server, in one image.
How it's wired together
The core trick is Page.startScreencast, a CDP method that makes Chrome push you a JPEG frame every time the page repaints. The server opens a CDP connection to the tab you're looking at, forwards those frames over a WebSocket to a in the browser, and forwards mouse/keyboard events the other way with Input.dispatchMouseEvent / Input.dispatchKeyEvent. From the outside it just looks like the page is rendering directly in your canvas — it isn't, it's a screencast, but the round-trip is fast enough that you stop noticing.
The tab bar is a thin layer on top of the same protocol: Target.getTargets for the list, Target.activateTarget when you click one, Target.closeTarget when you close it. None of this needed a browser automation library — it's CDP calls the server already had a WebSocket open for.
DevTools was the fiddly part. Chrome ships its own DevTools frontend and will happily serve it to you, but it hardcodes the host it expects to be reached at — so an iframe pointing at /devtools/inspector.html works fine on localhost:3000 and breaks the moment you access the container through any kind of proxy or tunnel, because the Host header no longer matches what Chrome baked into the page. The fix is a small reverse-proxy layer in front of Chrome's own DevTools and /json/* endpoints that rewrites the Host on the way through, so the exact same iframe works whether you're on localhost or three proxies deep.
Full DevTools, docked right in the same page, for when the live view isn't enough and you need to actually inspect something<br>Note<br>There's also an optional ACTIVATE_STEALTH_PLUGIN flag, which pulls in puppeteer-extra-plugin-stealth to patch the usual headless tells (missing navigator.webdriver, the HeadlessChrome UA string, an empty navigator.plugins, that kind of thing). Off by default — I only reach for it when a site is actively hostile to automation, not as a standing setting.
Running it
docker run -p 3000:3000 ghcr.io/spuntodotnet/browser-remote:latest
or with a docker-compose.yml if you want it alongside other things:
services:<br>browser-remote:<br>image: ghcr.io/spuntodotnet/browser-remote:latest<br>ports:<br>- "3000:3000"
Open http://localhost:3000/, and you've got a browser. There's a handful of env vars if you need them — a custom CA for testing local HTTPS certs (say, from mkcert), a different Chrome binary path, the stealth flag above — all documented in the README.
Warning<br>There is no authentication in front of any of this. Anyone who can reach port 3000 has full control of the browser, including whatever it happens to be logged into. It's fine on a private network or behind something you control — don't put it on the open internet...