Skip to content

feat(docker): add a Chrome driver and images to replace browserless - #645

Merged
route merged 23 commits into
mainfrom
feat/docker
Oct 17, 2026
Merged

route merged 23 commits into
mainfrom
feat/docker

Conversation

@route

@route route commented Oct 12, 2026 •

Copy link
Copy Markdown
Member

What

Better than browserless, not equal to it. A self-hosted Chrome service for Ferrum and Cuprite users: a small Go driver (docker/) and two images, ghcr.io/rubycdp/chrome and ghcr.io/rubycdp/chromium, each for amd64 and arm64. Moving from browserless usually means swapping the image and connecting to /.

docker run --rm -p 3000:3000 ghcr.io/rubycdp/chromium
Ferrum::Browser.new(url: "http://localhost:3000")

Open http://localhost:3000 in a browser to see running sessions and inspect their pages live in DevTools.

Why

Most remote-Chrome issues reported here come from browserless's own logic, not from CDP. Its Puppeteer resets the viewport (#592, #492), it writes a raw HTTP 408 onto a live socket (#538), it reports 0.0.0.0 as the WebSocket host (#501), and it leaves Chrome processes and zombies behind (#505, #364). docker/DESIGN.md lists all of them and compares the driver with browserless v2.

How

  • One Chrome per connection. Each Chrome runs in its own process group with a fresh profile, on a random port read from its stderr. When the connection closes, the group gets TERM, then KILL, and the profile is deleted. Each session logs why it ended.
  • Bytes pass through unchanged. The WebSocket goes through httputil.ReverseProxy. Nothing else talks to the client's targets.
  • /json/version is answered on the fly. It starts the session's Chrome, returns its real metadata, and rewrites the ws URL to a reachable host while keeping the query. A Chrome nobody connects to is stopped after 30s. Connections to / claim a Chrome already started with the same query, falling back to another pending one if the id was taken.
  • Per-connection options in the URL. launch (JSON or base64) takes args, defaultViewport, ignoreDefaultArgs and headless; headless: false runs Chrome on its own Xvfb display. timeout sets a session timeout.
  • Limits. CONCURRENT (default: CPUs) and QUEUED (default 10), answering 429 with Retry-After before the upgrade. TIMEOUT is off by default. EXTERNAL / PROXY_URL set the advertised address behind a proxy.
  • Default flags are Ferrum's, the same as dockerize: true locally, window size included, except --remote-allow-origins.
  • DevTools routes. /json, /json/list, /json/new, /json/protocol, /devtools/page/<id> and the DevTools frontend, with URLs pointing at the driver.
  • Dashboard at /. Running sessions and their pages, each linking to DevTools; every "session opened" log line links to it.
  • driver --help documents env, endpoints, launch options and the default flags.
  • Images.
    • debian:trixie-slim with dumb-init as PID 1, and a HEALTHCHECK on /health.
    • Runs under any uid.
    • Open fonts stand in for Microsoft ones.
    • Google Chrome (.deb) or Debian Chromium. No Node.
    • The driver's commit is in the org.opencontainers.image.revision label.
  • CI (.github/workflows/docker.yml).
    • Go formatting, vet and go test -race gate the builds.
    • Each browser and architecture builds on a native runner (no QEMU).
    • The Ferrum suite runs against every image. Specs tagged local_browser, which start their own Chrome, are skipped when FERRUM_URL is set.
    • On main it publishes latest, the major and the full browser version as multi-arch tags. A failed build or suite publishes nothing.
    • A daily run checks for new Chrome and Chromium versions and builds only the browsers that changed. Older tags stay as they were.

Testing

  • Go tests with -race, using the test binary as a fake Chrome.
  • The Ferrum suite against the driver with no local Chrome: 540 examples, 0 failures, 36 pending.
  • Run by hand on arm64 against both images: url: and ws_url: flows, launch options, headful under Xvfb, parallel sessions with CONCURRENT/QUEUED, timeouts, the dashboard and DevTools on a live page, and no Chrome or zombie processes left behind.

Not in this PR

  • The Ferrum side: applying window_size remotely, Browser#debug in remote mode, and never looking for a local binary when url or ws_url is set.
  • A zero-copy tunnel instead of ReverseProxy.
  • Cleanup of untagged image versions on GHCR.

A small Go server that looks like browserless v2 from the outside but runs no Puppeteer: each WebSocket
connection gets its own Chrome, traffic is proxied unchanged, and Chrome's process group is killed when the
connection ends. /json/version starts the session's Chrome and returns its real metadata with a reachable
ws URL; unclaimed browsers are stopped after 30s. DESIGN.md lists the browserless pitfalls it avoids.
@route route changed the title feat(docker): add a browserless-compatible Chrome driver and images feat(docker): add a Chrome driver and images to replace browserless Oct 17, 2026
route added 22 commits October 17, 2026 22:23
…aken by another connection

With ws_url:, Ferrum calls /json/version and then connects to /, which claims any pending Chrome with the same
query. With parallel clients that can be the Chrome another client's /json/version started, and that client
then got 404 on /devtools/browser/<id>. Chromes started with the same query are interchangeable, so the id
route now falls back the same way.
… publishing them

FERRUM_URL points the suite at a driver. CI passes Ferrum's default window size and --disable-web-security in
the launch query, so the specs see the same browser as locally. The custom browser path spec needs a local
browser and skips remotely.
…owsers that changed

A scheduled run compares the newest Chrome (Google's apt repository) and Chromium (Debian's apt-cache policy) with the tags on GHCR. The version is a build arg, so a new release busts the layer cache, and the image is labeled with the driver's commit.
@route
route merged commit f9ff4e2 into main Oct 17, 2026
14 checks passed
@route
route deleted the feat/docker branch October 17, 2026 19:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant