-
Notifications
You must be signed in to change notification settings - Fork 0
Deployment
Running the server. For pointing a client at one, see Installing.
The server needs a Selenium Grid to drive and does nothing useful without one. Every option below is really a question of where the server runs and how it reaches that Grid.
Every flag has an environment fallback, because containers are configured with environment variables and developers reach for flags.
| Env | Flag | Default | Notes |
|---|---|---|---|
GRID_URL |
--grid-url |
the in-cluster Grid Service | Selenium Grid hub |
MCP_AUTH_TOKEN |
--auth-token |
unset | Bearer token for both surfaces. Unset disables auth |
ROUTE_PREFIX |
--route-prefix |
/browser |
Path prefix for the HTTP endpoints |
TRANSPORT |
--transport |
http |
http or stdio
|
HOST / PORT
|
--host / --port
|
0.0.0.0 / 8000
|
|
SAVED_SESSIONS |
--no-saved-sessions |
true |
Let MCP callers omit session_id. Never affects HTTP |
SESSION_STORE |
— | memory |
memory or redis. Any REDIS_* setting implies redis
|
SESSION_TTL |
— | 3600 |
Seconds a caller's mapping is kept |
STATELESS_HTTP |
--stateless |
false |
Drop MCP transport sessions. Required for more than one replica |
PUBLIC_BASE_URL |
— | unset | Externally reachable root. Needed for absolute file links and the MCP app CSP |
GRID_CONSOLE_URL |
— | / |
Where the admin UI frames the Grid console from |
APPS_ENABLED |
--no-apps |
true |
Offer the MCP Apps components |
SKILL_ENABLED |
--no-skill |
true |
Serve the embedded Agent Skill |
WINDOW_WIDTH / WINDOW_HEIGHT
|
— | node default | Default window size for new sessions |
PAGE_LOAD_TIMEOUT |
— | unbounded | Seconds a navigation may take. Worth setting |
SCRIPT_TIMEOUT |
— | driver default | Seconds execute_script may take |
LOG_LEVEL |
--log-level |
INFO |
DEBUG logs which session key each call resolved to |
PAGE_LOAD_TIMEOUT deserves the emphasis: with no bound, one hung page holds a
Grid slot until the Grid reaps it, and slots are the scarce resource.
One process, one client, no network. The client launches the server and talks to it over pipes, which makes it the simplest option and the least shareable — the session lasts exactly as long as the process.
TRANSPORT=stdio GRID_URL=http://grid:4444 selenium-flowThere is no token here and none is needed: whoever can start the process is
already inside the trust boundary. Saved sessions work and are keyed on the
process, so session_id is optional throughout.
The admin UI, the signed file links and MCP Apps all need HTTP, so none of them exist in this mode.
The normal way to run it. One process serves any number of clients over
Streamable HTTP at /mcp and JSON at /browser/*.
MCP_AUTH_TOKEN=… GRID_URL=http://grid:4444 PORT=8000 selenium-flowOn more than one replica, set STATELESS_HTTP=true. MCP transport sessions
otherwise live in one process's memory, and a client whose next request lands on
another pod is told its session does not exist. The browser is unaffected either
way — it lives on the Grid and the caller carries its id.
If you want saved sessions to survive a restart or be shared across replicas,
point SESSION_STORE=redis at a Redis. The store only ever holds a
key → session-id mapping; the browser is on the Grid regardless.
docker compose up --build in a checkout brings up the server and a
standalone Grid, with auth off:
- server on
localhost:8000 - admin UI on
localhost:8000/admin - the Grid's noVNC view on
localhost:7900, where you can watch it work
For your own compose file, the published image is
kubed/selenium-flow:
services:
selenium-flow:
image: kubed/selenium-flow:latest
environment:
GRID_URL: http://selenium:4444
MCP_AUTH_TOKEN: ${MCP_AUTH_TOKEN}
PAGE_LOAD_TIMEOUT: "30"
ports:
- "8000:8000"
depends_on:
- selenium
selenium:
image: selenium/standalone-chrome:latest
shm_size: 2gb
environment:
SE_NODE_ENABLE_MANAGED_DOWNLOADS: "true"
ports:
- "7900:7900"SE_NODE_ENABLE_MANAGED_DOWNLOADS is not optional if you want session files:
without it the browser downloads into a directory nothing can reach, and
session_files, screenshot(save=true) and save_pdf have nowhere to put
anything. shm_size matters too — Chrome crashes under the default 64MB.
The reference deployment lives in the cluster repo under
apps/selenium, as a Kustomize app with three components:
| Component | Holds |
|---|---|
components/helm |
the Selenium Grid chart — hub and Chrome nodes |
components/mcp |
this server: Deployment, Service, ConfigMap, and its token |
components/ingress |
one host for both halves, plus the certificate |
Four details there are worth copying rather than rediscovering.
The token is generated, never authored. An External Secrets Password
generator mints it and an ExternalSecret materialises it into a Secret the
Deployment reads with envFrom. No value exists in git or in a vault first.
Declare labels on the generated Secret. kubectl apply --applyset stamps
its label onto everything it manages; a generated Secret inheriting the
ExternalSecret's labels ends up looking like a member of the applyset, and the
next apply prunes it — rotating the token out from under every connected client,
silently, because the pods keep the old value until they restart.
envFrom does not reload. Rotating the Secret needs a
kubectl rollout restart; the running pod holds the value it started with.
Mount an emptyDir at /tmp if the container runs with a read-only root
filesystem. Uploads stage there, and without it they fail with
"No usable temporary directory".
The ingress puts the Grid console and this server on the same origin — the Grid
at / and the server under /flow with a Traefik stripPrefix:
annotations:
traefik.ingress.kubernetes.io/router.middlewares: flow-selenium-flow-strip@kubernetescrd
spec:
rules:
- host: selenium.example.com
http:
paths:
- path: /flow
pathType: Prefix
backend: { service: { name: selenium-flow, port: { name: http } } }
- path: /
pathType: Prefix
backend: { service: { name: selenium-grid-selenium-hub, port: { number: 4444 } } }Same-origin is the point: the admin UI frames the Grid console as a tab, and the
console's live browser view needs no cross-origin exception. Longest path wins in
Traefik, so /flow is matched before the Grid's catch-all, and StripPrefix
passes a path that does not carry the prefix through untouched.
With the prefix stripped, the server still serves /mcp and /admin
internally and knows nothing about being mounted — which is why
PUBLIC_BASE_URL has to be told: https://selenium.example.com/flow.
Nothing here runs a cleanup loop, and nothing should. The Grid expires idle
browsers on SE_NODE_SESSION_TIMEOUT and the session store expires its own
keys. If the Grid reaped a browser this server remembered, the next call notices
and reopens it at the page it was last on, with the same settings.
The action pages are generated from openapi.yaml, which is itself generated from the live MCP tool schemas — so they describe the server that shipped, not the one someone remembered. Prose belongs in wiki-notes/<tool>.md in the repo.
selenium-flow · MIT
Start here
Lifecycle
Going places
Doing things
Getting things out