Skip to content

Deployment

Kelly Ferrone edited this page Sep 9, 2026 · 1 revision

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.

Configuration

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.


stdio

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-flow

There 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.

HTTP

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-flow

On 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

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.

Kubernetes

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".

One host, two halves

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.

Session lifetime is the Grid's job

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.

Clone this wiki locally