WebSummoner 1.0.0 — Selenoid, summoned back to life #1
GhaziTriki
started this conversation in
Announcements
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
🧙 WebSummoner 1.0.0
It is here. 🎉 The first release of WebSummoner: the Selenium hub many of us ran for years as Selenoid, alive again, current, and looking forward. Thank you for being here on day one.
WebSummoner is a maintained fork of Aerokube Selenoid, which its owners archived on 17 December 2024 — the repository is read-only and takes no further changes. WebSummoner keeps what made Selenoid good — a ~10 MB Go binary, one container per session, no JVM — and brings the parts that had aged: current browsers, current toolchains, and a set of browser fixes Selenoid never had.
Summon a fleet of browsers into Docker containers. A fast Selenium hub with ephemeral, session-scoped environments.
If you were running Selenoid: welcome back. Nothing you built is lost, and moving over is a rename plus one path change. If you are new: welcome, you are five minutes away from your first summoned browser. 🧙
🚀 Get it
docker run -d --name websummoner \ -p 4444:4444 \ -v $PWD/browsers.json:/etc/websummoner/browsers.json:ro \ -v /var/run/docker.sock:/var/run/docker.sock \ websummoner/websummoner:latestSelenium URL:
http://localhost:4444/wd/hub. Binaries for Linux, macOS and Windows and the SPDX SBOM are attached to this release; the image is signed with cosign.📖 Documentation · 🔁 Migrating from Selenoid · 🌐 Browser images · 💬 Discussions
Every browser claim below is verified against the container test suite: 24 browser tests in 12 classes, run against each of the seven published images in isolation. We would rather show you the numbers than ask for your trust. The companion projects have their own release notes, linked at the end.
✨ Highlights
Here is what the first release brings. We are proud of every line of it, and prouder still of the two browsers that took the longest.
window.open.proxycapability are handled in the hub; cookies,quit()andclose()each need one line on the client side, and the docs say which.enableAudio: falseturns it off./metricsin Prometheus format,HEADhealth probes, and429when the queue is full and the client asked not to wait.selenoid:options, theX-Selenoid-*headers, the/aerokube/...path,cm selenoid …and the UI's--selenoid-uriall still work.golangci-lintandgovulncheckin CI, Dependabot, SBOM and signed images, and a 29-page documentation site.🔁 Migrating from Selenoid
✅ Unchanged
The wire protocol and endpoint (
http://host:4444/wd/hub), thebrowsers.jsonschema, every CLI flag,/status,/ping, video, logs, VNC, clipboard, download and DevTools proxying, and GGR compatibility. Any Selenium client works without changes.🤝 Still accepted, permanently
websummoner:optionsselenoid:options(the new block wins when both are present)X-WebSummoner-No-WaitX-Selenoid-No-WaitX-WebSummoner-FileX-Selenoid-File/wd/hub/session/<id>/websummoner/.../wd/hub/session/<id>/aerokube/...cm websummoner …,cm websummoner-ui …cm selenoid …,cm selenoid-ui …websummoner-ui --websummoner-uri--selenoid-uri🏷️ Renamed
selenoidbinary,aerokube/selenoidimagewebsummoner,websummoner/websummonerselenoid/chrome:128.0and friendswebsummoner/chrome:152.0and friendsselenoid/video-recorderwebsummoner/video-recorder(new default of-video-recorder-image)github.com/aerokube/selenoidgithub.com/websummoner/websummonergithub.com/websummoner/websummoner; the binary iswebsummonerand the imagewebsummoner/websummoner. Anything that imports the module, calls the binary by name or pullsaerokube/selenoidmust change./etc/websummoner/browsers.jsonand writes video to/opt/websummoner/video. Update the two mount targets, or pass your own-confand-video-output-dir.websummoner/video-recorder:latest-release. Pass-video-recorder-imageto keep the old one.goog:chromeOptionsforopera,yandexandbravesessions (see below). Clients cannot pass their own Chromium flags for those three browsers; Chrome and Edge are untouched.In the companion tools the only breaking change is the Go module path:
cm selenoid …and the UI's--selenoid-urikeep working as aliases.Details: Migrating from Selenoid.
🌐 Browsers
¹ the non-ASCII upload file name: chromedriver-family drivers reject the path. ²
close(): WebKitWebDriver ends the whole session, so nothing can be asserted afterwards. Both are limits of the driver, not the hub, and the suite reports them as skips rather than failures — a skipped test no longer counts as failed and no longer produces a failure screenshot. Every test that a driver can run, passes.Chrome, Firefox, Edge and Opera are published under three tags — line (
chrome:152), alias (chrome:152.0) and immutable full version (chrome:152.0.7977.75). Brave and Yandex have no separate alias level and carry line and full version (brave:1.94,brave:1.94.119); Safari carries the WebKitGTK version only (safari:2.52.6). VNC is built into every image; there are no separatevnc_variants. All images sharewebsummoner/browser-base:1.0.0on Ubuntu 26.04 LTS, which — like the video recorder — is published by GitHub Actions from theimagesrepository (see What GitHub Actions publishes below); the browser images themselves are built by RIADVICE.🎭 Opera works, and for a non-obvious reason
Selenium dropped Opera support in 4.3.0 and its recommended workaround — drive Opera with a Chrome-for-Testing chromedriver — is a trap. It starts sessions fine, then crashes the renderer whenever a page opens a window (
disconnected: Unable to receive message from renderer).The driver family's version check turns out to be a warning, not a refusal. Opera's own driver drives it correctly even one Chromium line behind the browser, which is where we were while Opera's driver for that line was not out yet:
Opera N ships Chromium N+16 and
operachromiumdrivertags follow the Chromium line, so the build tool derives the right line and, when Opera has not published a driver for it yet, falls back to the newest publishedoperadriver— never a chromedriver. A real driver one line behind beats a foreign driver on the exact line, because Opera patches its Chromium and only Opera's driver accounts for those patches. The 1.0.0 image ships Opera 135 with OperaDriver 151, the matching line.The second half is in the hub: operadriver answers a W3C session request in the legacy JSONWP dialect unless asked for W3C explicitly (the documented reason Selenium dropped Opera). The hub sets
goog:chromeOptions.w3c: truefor Opera sessions, and wraps any JSONWP reply that still comes back into the W3C envelope.Opera also reports its own UI (Speed Dial, address-bar dropdown) as WebDriver window handles. Match handles by title or URL; never assume the first is yours.
🧭 WebKit is a first-class target
Four gaps made WebKit second-class. Two are closed in the hub, so they apply to images built long before:
WebKitWebDriverignores theproxycapability (selenide#1575). The hub translates a manual proxy intohttp_proxy,https_proxyandno_proxyon the browser container, which is where WebKit picks its proxy up.localhost,127.0.0.1and::1are always excluded so the driver's own connection is never proxied.Three behaviours are documented with a one-line answer on the client side:
addCookiesilently drops cookies with nosameSite, while answering with success. Set it; it is good practice on every browser anyway.quit()throws even though the session ended — WebKit ends the session and then closes the socket without answering, so clients see an empty-message exception for a teardown that succeeded. Tolerate it, scoped to WebKit; Selenium's own WebKitGTK binding used to do the same.close()ends the session, reporting the same empty-message error. Use one window per session rather than closing windows.🦁 Brave is new
Brave's version line is unrelated to Chromium's, so the build tool reads the embedded Chromium version from the browser itself and fetches the matching chromedriver.
/usr/bin/brave-browseris a wrapper script that chromedriver cannot launch; the hub points the driver at the real binary.🦊 Firefox no longer embeds the hub binary
The Firefox image used to carry a copy of the hub inside it and expose
/wd/hub. It now runsgeckodriverdirectly (with--allow-hostsfor the container hostname), andbrowsers.jsonuses"path": "/"like every other image. That removes a build-time coupling between two repositories and a host path that leaked into published images.🗑️ Removed images
The
chromium,android,opera_presto,phantomjsand Selenium-JAR-based Firefox image sources are gone; none of them could be built or driven with current toolchains.⚙️ The hub in detail
Every block below is collapsed; open the ones you need.
🔀 Session handling
websummoner:optionsis the vendor capability block;selenoid:optionsis still merged, and the new block wins when both are present.X-WebSummoner-No-WaitjoinsX-Selenoid-No-Wait. Presence is what matters (GGR sends the header with an empty value). When the queue is full and either header is set, the hub answers429 Too Many Requestsinstead of a generic 500 unknown error (RFC 6585; upstream issue Incorrect status code in case of "Too many requests" response aerokube/selenoid#1203).browsers.json: an exact key wins, otherwise the longest key the requested version is a prefix of. Upstream took the first prefix match in map order, so"152"could resolve differently between restarts.versionrouting hint are stripped before the request reaches the driver; modern drivers reject unknown capabilities (upstream issue Remove version capability when proxying to driver aerokube/selenoid#909).adaptDriverCapabilities). Sessions foropera,yandexandbraveare forwarded to the in-container driver asbrowserName: chromewith the right binary (/usr/bin/opera,/usr/bin/yandex-browser,/opt/brave.com/brave/brave-browser),--no-sandboxand a per-request--user-data-dir. Yandex additionally gets--homepage=about:blank --no-first-run --no-default-browser-check, because it opens its own start page a moment after launch on top of whatever the driver navigated to. Opera getsw3c: true. Routing still happens on the real browser name.statusis treated as an error rather than a session, and a reply without asessionIdproduces a clear error instead of a panic.se:cdpandse:cdpVersionare advertised only for Chromium browsers. Advertising a CDP endpoint for Firefox or WebKit made clients fail their first WebSocket handshake.httputil.ProxyRequest.Rewriteand sets theX-Forwarded-*headers; theDirectorAPI is gone.📈 Observability
/metricsin Prometheus text format, no client library required:websummoner_sessions_active,websummoner_sessions_limit,websummoner_queue_depth,websummoner_queue_pending,websummoner_browser_sessions{browser="chrome:152.0"}, and the counterswebsummoner_sessions_created_total,websummoner_sessions_failed_total,websummoner_sessions_timed_out_total,websummoner_sessions_deleted_total,websummoner_video_sessions_total,websummoner_vnc_sessions_total,websummoner_audio_sessions_total.HEAD /pinganswers200, andHEAD /statusanswers200when a slot is free and503when the hub is full — Kubernetes probes and load balancers sendHEAD, and the old handlers only servedGET.log.Fatalfrom three places, and the signal channels are buffered, so shutdown is orderly: HTTP server drained, Docker client closed, error reported once.📤 Files, video and audio
/session/<id>/fileand the/session/<id>/se/filepath Selenium 4 actually sends (the old interception only matched the legacy four-fragment form, which is why it never fired and WebKit answered "Unknown command"). The hub unpacks the zip itself and places the file at/opt/websummoner/uploads/inside the browser container by streaming a tar totar -xrunning in the container, which honours tmpfs and volume mounts and leaves the file owned by the browser's own user. Images withouttarfall back to the Docker copy API, and atest -fexecuted in the container confirms the file is visible before the path is handed to the driver — the copy API writes underneath a mount and reports success.-enable-file-uploadgates the feature as before.enableAudio(defaulttrue) controls the audio track of recordings. It setsENABLE_AUDIOon the browser container andDISABLE_AUDIO=trueon the recorder when off. Audio metrics are counted per session.proxycapability translation for WebKit sessions, described above.websummoner/video-recorder:latest-release, published by GitHub Actions from theimagesrepository on its own release tag, so the hub can always pull it.🔒 Hardening
videoNameandlogNamemust be plain file names; anything with a path element is rejected withinvalid argumentinstead of being written where it points. The video delete endpoint applies the same check.0755; upstream created them0644, which is not traversable.SECURITY.mddocuments the reporting process and the operational reality: anyone who can reach the hub can start containers, so the port is privileged.📦 Dependencies
github.com/docker/dockertypes.ContainerJSON→container.InspectResponseand friends)github.com/docker/go-connectionsgithub.com/imdario/mergodario.cat/mergov1.0.2github.com/mafredri/cdpgolang.org/x/netgithub.com/gorilla/websocketgithub.com/stretchr/testifygithub.com/aws/aws-sdk-gogithub.com/aerokube/ggrgithub.com/websummoner/ggrDependabot runs weekly with grouped pull requests for Go modules, the Docker base image, GitHub Actions and the documentation site's npm packages.
🏗️ Build, CI and supply chain
golangci-lintgates every push and pull request:errcheck,govet,staticcheck,ineffassign,misspell,unusedandgofmt. The tree passes with the full set enabled.govulncheckruns in CI with a narrow, documented allowlist (ci/govulncheck.sh): two Moby daemon-side advisories that list no affected symbols and have no fixed version. Anything else fails the build.CGO_ENABLED=0; thewebsummoner/websummonerimage built forlinux/amd64andlinux/arm64; an SPDX SBOM generated with Syft and attached to the release; the image signed with cosign (keyless, OIDC). Docker Hub login reads the password from stdin and the repository name is lower-cased.🐳 What GitHub Actions publishes, and what it does not
Every image on Docker Hub comes from one of two paths, and it matters which:
websummoner/websummoner1.0.0,latest-release,latestwebsummoner/websummoner-ui,ggr,ggr-ui,cm1.0.0,latest-release,latestwebsummoner/browser-baseimagesrepository'simagesworkflow, on thebrowser-base-<version>tag1.0.0(what every browser Dockerfile is builtFROM) andlatestwebsummoner/video-recordervideo-recorder-<version>tag1.0.0,latest-release(the hub's default) andlatestwebsummoner/chrome,firefox,edge,opera,brave,yandex,safariimagestool from the public sourcesNeither the base nor the recorder follows a commit: an ordinary push to the
imagesrepository builds nothing, and only a release tag naming one of them —browser-base-1.0.0andvideo-recorder-1.0.0ship with this release — or a manual run given such a tag, builds and pushes that one image. A version with a pre-release suffix (1.2.0-rc1) publishes that exact tag only, so a release candidate never becomes the recorder every hub pulls by default.DRY_RUN=1prints the build and push commands without running them. This exists because the hub cannot record video unless the recorder is pullable, and no browser image can be built unless the base is.The seven browser images are deliberately not built in GitHub Actions. Each one needs the vendor's package for one exact version, a driver resolved against that version at build time (Chrome-for-Testing, Opera's Chromium line, Brave's embedded Chromium), for Safari a WebKitGTK compile that takes hours, and a run of the container suite before it may be tagged. RIADVICE builds and publishes them from the same public Dockerfiles with the same public tool, so any image can be reproduced by anyone; they follow each vendor release rather than each git push.
🧪 Tests
New test files cover driver mode end to end (
driver_mode_test.go), theHEADprobes (health_test.go), graceful start and shutdown (run_test.go), the metrics format and counters (metrics_test.go), the queue's no-wait and429paths (protect/queue_test.go), capability adaptation, JSONWP wrapping, CDP advertising, safe file names and the vendor-option stripping (safety_test.go,branches_test.go), plus unit tests forinfoandjsonerror. Tests run with the race detector and report coverage for every package.📚 Documentation
The AsciiDoc fragments under
docs/are gone.docs-site/is an Astro Starlight site — 29 pages, full-text search, dark mode, anllms.txtexport — deployed to GitHub Pages at websummoner.riadvice.com/websummoner by its own workflow, which also builds every pull request so a dead link fails early. New pages include Migrating from Selenoid, a source-checked Comparison with Selenoid, Moon, Selenium Grid, Selenide, Playwright and Puppeteer, Browser images with the per-browser behaviour above, Image tags, Building browser images, Docker Compose, S3 upload and a FAQ. The docs carry no analytics unless a measurement id is configured, and then only after consent.Stated plainly, because a release that hides these is worse than one that does not ship them.
unable to unzip 'file', Issue with file uploads with specific characters aerokube/selenoid#669); the hub now delivers the file into the container with correct UTF-8 bytes on every image. The last step belongs to the driver, and every chromedriver-family driver rejects the path withinvalid argument: File not found :. Firefox and WebKit work. The suite skips the case on Chromium rather than asserting it.close()on WebKit ends the session. Use one window per session on the Safari image; the suite skips the window-close case there.env, session limits and queueing, and file download ship without container tests. They are inherited from Selenoid and expected to work; they are not covered.🧩 Companion projects
Each has its own release notes; the one-line summary:
--websummoner-uriwith--selenoid-urias aliasRewriteproxy withX-Forwarded-*, sends both no-wait header spellings to backendscm websummoner/cm websummoner-uiwith theselenoidspellings as aliases, Docker client v28, current driver URLs💬 Join in
This release is a beginning, not a finish line, and it is better with you in it.
💬 Discussions is where the conversation lives · 🤝 Contributing guide if you want to send a change · ⭐ A star on the repository helps the next person searching for a Selenoid successor find it.
🙏 Thanks
To everyone who ran Selenoid in production for years and kept asking whether anyone would pick it up: yes. This one is for you.
To Aerokube and Ivan Krutov for Selenoid, which this is built on, and to Kirill Merkushev for the original UI. Archiving a project you have carried for years is a legitimate decision; picking it up is ours, and we intend to carry it well.
WebSummoner is developed and maintained by RIADVICE under Apache-2.0. Happy summoning. 🧙
All reactions