-
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 |
/ |
Where the whole server is mounted. Every tree is fixed beneath it; /health, /started, /ready and /info also answer at the root |
TRANSPORT |
--transport |
http |
http or stdio
|
HOST / PORT
|
--host / --port
|
0.0.0.0 / 8000
|
|
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 |
FLOW_DATA_DIR |
— | unset | Where saved flows and kept files live. Unset turns both off |
SECRETS_DIRS |
— | unset | Directories of secrets, separated like PATH. Unset turns them off |
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 its session is named stdio and needs no configuration.
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/*, both under ROUTE_PREFIX
when it is set.
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 session name leads back to it.
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 - 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.
There are now two ways to do this, and the second is the tidier one:
-
Strip at the ingress, as above. The server runs at the root, knows nothing
about being mounted, and
PUBLIC_BASE_URLhas to be told where it really lives:https://selenium.example.com/flow. -
Tell the server instead:
ROUTE_PREFIX=/flow, and dropStripPrefix. The served paths and the public URL then agree, which is what makes a link the server hands out work without a second setting to keep in sync.
Either way the admin UI is fine: it derives everything from its own path, so it
does not need to be told. /health, /started, /ready and /info answer at
the root as well as under the prefix, so a probe never depends on that choice.
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
Guides
Lifecycle
Going places
Doing things
Getting things out