Stealth headless Chromium in a container. Exposes Chrome DevTools Protocol (CDP) on port 9222. Connect from Playwright, Puppeteer, MCP browser tools, or any agent that wants a remote browser without bundling one.
docker run --rm -p 9222:9222 --shm-size=512m ghcr.io/askalf/browser-bridge:latest// Then connect from anywhere on the host
import { chromium } from 'playwright';
const browser = await chromium.connectOverCDP('http://localhost:9222');Bundling a browser into every agent / scraper / MCP server / test runner is overhead — image size, OS dependencies, font rendering, fingerprint maintenance. browser-bridge centralizes one browser container that any number of clients can share via CDP. Production-grade defaults (full puppeteer-extra stealth evasions, non-root user, healthcheck, optional VPN proxy) so you don't have to assemble them yourself.
- Stealth — puppeteer-extra with the full evasion set:
navigator.webdriver,navigator.plugins,navigator.languages, WebGL vendor, Chrome runtime, iframe quirks, the works.--enable-automationis dropped from the default args. Passes the common bot-detection checks. - CDP on 0.0.0.0:9222 — Chromium binds to localhost on recent versions; we use socat to expose it on the wildcard so other containers (or your dev machine) can reach it.
- Realistic browser args — 1920×1080 viewport,
en-US,enlang, accelerated 2D canvas, WebGL on, font-render hinting set. Many "headless" containers fail bot checks because they ship without these; we ship with them. - Optional VPN proxy — set
HTTPS_PROXYorHTTP_PROXYto route Chromium's traffic through a VPN sidecar (Gluetun, etc.). Supported out of the box. - Non-root — runs as the
browseruser, not root. CDP escapes don't get privilege. - Healthchecked — Docker-level healthcheck hits
/json/versionevery 15s. - Heartbeat logs — one log line per minute confirming the browser is still connected. Pair with restart policy for self-recovery.
docker run --rm -p 9222:9222 --shm-size=512m ghcr.io/askalf/browser-bridge:latest--shm-size matters: Chromium's default /dev/shm (64MB) is too small for non-trivial pages and you'll see crashes without it.
services:
browser:
image: ghcr.io/askalf/browser-bridge:latest
expose:
- "9222"
shm_size: '512m'
restart: unless-stoppedservices:
vpn:
image: qmcgaw/gluetun
cap_add: [NET_ADMIN]
environment:
VPN_SERVICE_PROVIDER: protonvpn
OPENVPN_USER: ${VPN_USER}
OPENVPN_PASSWORD: ${VPN_PASS}
browser:
image: ghcr.io/askalf/browser-bridge:latest
network_mode: "service:vpn"
shm_size: '512m'
environment:
HTTPS_PROXY: http://localhost:8888
HTTP_PROXY: http://localhost:8888import { chromium } from 'playwright';
const browser = await chromium.connectOverCDP('http://localhost:9222');
const ctx = browser.contexts()[0] ?? await browser.newContext();
const page = await ctx.newPage();
await page.goto('https://example.com');
console.log(await page.title());import puppeteer from 'puppeteer-core';
const browser = await puppeteer.connect({
browserWSEndpoint: 'ws://localhost:9222',
});The CDP endpoint http://localhost:9222/json/version and ws://localhost:9222/devtools/... are standard. Most MCP browser servers accept a browserURL config option — point it at this container.
| Env var | Default | Effect |
|---|---|---|
PUPPETEER_EXECUTABLE_PATH |
/usr/bin/chromium |
Which Chromium binary to launch (rarely needs overriding). |
HTTPS_PROXY |
unset | Outbound proxy passed to Chromium as --proxy-server. |
HTTP_PROXY |
unset | Same as HTTPS_PROXY; either works. |
Ports:
- 9222 (TCP) — CDP entry point. The image's
EXPOSEandHEALTHCHECKboth target this.
ghcr.io/askalf/browser-bridge:latest— bleeding edge frommaster.ghcr.io/askalf/browser-bridge:v<X.Y.Z>— pinned releases.ghcr.io/askalf/browser-bridge:vX.Yandghcr.io/askalf/browser-bridge:vX— minor/major aliases pointing at the latest matching release.
- Runs as non-root (
browser:browser). --no-sandboxis set inside the container because Chromium's setuid sandbox doesn't work in unprivileged containers; the broader sandbox is the Linux user namespace the container provides.- CDP is exposed without authentication — anyone who can reach
:9222can drive the browser. Bind to a private network (docker-compose service network, internal VPN, etc.). Don't expose:9222to the public internet. - Every Chromium command is exposed via CDP. Treat the CDP endpoint with the same care you'd treat raw shell on the container.
- Not a queue or scheduler. It's just one browser. Run multiple containers + a queue (BullMQ, redisflex's InMemoryQueue, whatever) for parallelism.
- Not session-pinned. All clients share the same Chromium instance. For session isolation, use Playwright/Puppeteer browser contexts.
- Not a Chrome extension host. Headless Chromium doesn't load extensions reliably.
MIT — see LICENSE.
| Project | What it does |
|---|---|
| arnie | Portable IT troubleshooting companion. Networking, AD, Windows Update, package managers, log triage, hardware checks. |
| brio | Capability layer for AI workloads — semantic cache, cost tiering, policy. Sits in front of any Anthropic-compat endpoint. |
| claude-bridge | Bridge Claude Code sessions to Discord. |
| dario | Local LLM router. Use your Claude Max/Pro subscription as an API. |
| deepdive | Local research agent. Plan → search → fetch → extract → synthesize. Cited answers. |
| git-providers | Unified GitHub + GitLab + Bitbucket Cloud REST clients behind one GitProvider interface. Plus a 44-entry api-key-provider taxonomy. |
| hands | Cross-platform computer-use agent. Mouse, keyboard, screen. |
| install-kit | curl-pipe-bash template for self-hosted Docker apps. |
| pgflex | One Postgres API. Two modes (real PG ↔ PGlite WASM). |
| redisflex | One Redis API. Two modes (ioredis ↔ in-process). |