# How to Run a Selenium Grid in a Cloud Sandbox

> Start the official Selenium hub, Chrome and Firefox node images with Docker Compose in a default Sandboxer sandbox, host port 4444 with sbx host, and pass the URL's token to webdriver.Remote as a cookie header. Our remote tests passed on both browsers; each WebDriver command took about 136 ms from outside and 7 ms inside.

Source: https://sandboxer.com/guides/run-selenium-grid-in-a-sandbox. Updated 2026-10-27.

**Short answer:** Start the official Selenium hub, Chrome and Firefox node images with Docker Compose in a default Sandboxer sandbox, host port 4444 with sbx host, and pass the URL's token to webdriver.Remote as a cookie header. Our remote tests passed on both browsers; each WebDriver command took about 136 ms from outside and 7 ms inside.

## What we ran

Everything below ran on production on October 27, 2026, in one `default` sandbox (4 vCPU, 8 GB, $0.036/hour) created with `--no-env`. The Grid images are `selenium/hub`, `selenium/node-chrome` and `selenium/node-firefox` at tag `4.51.0-20261010`, from the [docker-selenium project](https://github.com/SeleniumHQ/docker-selenium). The test client is Python Selenium 4.51.0 on our own machine. Timings are single runs or short series, so treat them as rough.

| Step | Result |
| - | - |
| `docker compose up -d` (pulls 3 images) | 25.7 s |
| Grid status | Ready, Chrome 155.0 node (2 slots) and Firefox 158.0 node (1 slot) |
| Hosted Grid URL without the token | `401` |
| Remote test from outside, Chrome | New session 0.54 to 0.63 s, test 1.15 to 1.41 s (10 runs) |
| Remote test from outside, Firefox | New session 1.85 to 2.18 s, test 1.30 to 1.53 s (3 runs) |
| One WebDriver command, client outside | 135.9 ms median |
| One WebDriver command, client inside the sandbox | 6.8 ms median |
| 4 Chrome tests at once, 2 slots | All passed; the extra sessions waited in the Grid's queue, 7.9 s wall time |
| Memory used with the Grid idle | About 1.5 GB of 8 GB |
| `sbx stop --wait` then `sbx resume` | Memory restored, same containers, tests passed on the same URL |
| Boot from disk | Containers back within 5 s; the first request came before the hub was listening, a retry worked |

## 1. Start the Grid with Docker Compose

Docker and Compose are already installed in every sandbox. This file follows the hub-and-nodes layout from the [docker-selenium README](https://github.com/SeleniumHQ/docker-selenium#docker-compose), with three changes: pinned image tags, `restart: unless-stopped`, and two Chrome sessions per node.

```yaml
# compose.yaml
services:
  selenium-hub:
    image: selenium/hub:4.51.0-20261010
    restart: unless-stopped
    ports:
      - "4444:4444"
  chrome:
    image: selenium/node-chrome:4.51.0-20261010
    restart: unless-stopped
    shm_size: 2gb
    depends_on: [selenium-hub]
    environment:
      - SE_EVENT_BUS_HOST=selenium-hub
      - SE_EVENT_BUS_PUBLISH_PORT=4442
      - SE_EVENT_BUS_SUBSCRIBE_PORT=4443
      - SE_NODE_MAX_SESSIONS=2
      - SE_NODE_OVERRIDE_MAX_SESSIONS=true
    ports:
      - "7900:7900"
  firefox:
    image: selenium/node-firefox:4.51.0-20261010
    restart: unless-stopped
    shm_size: 2gb
    depends_on: [selenium-hub]
    environment:
      - SE_EVENT_BUS_HOST=selenium-hub
      - SE_EVENT_BUS_PUBLISH_PORT=4442
      - SE_EVENT_BUS_SUBSCRIBE_PORT=4443
```

```bash
sbx new --name selenium-grid --type default --no-env
id=sb_hwfyk2qg7a    # the id that sbx new printed

sbx exec $id "mkdir -p grid"
sbx scp compose.yaml $id:/home/user/grid/compose.yaml
sbx exec $id --timeout 600 "cd grid && docker compose up -d"
```

```text
 Container grid-selenium-hub-1 Started
 Container grid-firefox-1 Starting
 Container grid-chrome-1 Starting
 Container grid-firefox-1 Started
 Container grid-chrome-1 Started
```

`sbx exec` stops waiting after 30 seconds by default, so give the first `up` a longer `--timeout` while it pulls the images. Check the Grid with its `/status` endpoint. We used a small script that prints one line per node:

```python
# status.py
import json, urllib.request
v = json.load(urllib.request.urlopen("http://localhost:4444/status"))["value"]
print("ready:", v["ready"], "|", v["message"])
for n in v["nodes"]:
    browsers = sorted({s["stereotype"]["browserName"] + " " + s["stereotype"].get("browserVersion", "") for s in n["slots"]})
    print(n["version"], n["availability"], "maxSessions", n["maxSessions"], browsers)
```

```bash
sbx scp status.py $id:/home/user/grid/status.py
sbx exec $id "python3 grid/status.py"
```

```text
ready: True | Selenium Grid ready.
4.51.0 (revision 35c5fab …) UP maxSessions 1 ['firefox 158.0']
4.51.0 (revision 35c5fab …) UP maxSessions 2 ['chrome 155.0']
```

Three settings in the file matter:

- `shm_size: 2gb`. The docker-selenium README asks for 2 GB of shared memory per browser container; Docker's default is 64 MB.
- `SE_NODE_MAX_SESSIONS` with `SE_NODE_OVERRIDE_MAX_SESSIONS=true`. Each node runs one session by default. The README advises at most one session per CPU, so on a 4 vCPU sandbox keep the total across nodes at 4 or fewer.
- `restart: unless-stopped`. When the sandbox boots from disk instead of restoring memory, Docker starts the Grid again (step 5).

## 2. Give the Grid a private HTTPS URL

```bash
sbx host $id 4444 --title "Selenium Grid"
```

```text
https://apt-scarlet-ginger-4444.box.sandboxer.com?_token=<token>
```

The URL is private. Anyone who runs tests through it can start browsers in your sandbox, so keep the token out of logs and CI output.

```bash
curl -s -o /dev/null -w "%{http_code}\n" https://apt-scarlet-ginger-4444.box.sandboxer.com/status
```

```text
401
```

Opening the full URL in a browser answers `302` and sets the `__sbx_auth` cookie, so the Grid UI at `/ui/` loads normally after that (`200` in our test). How private URLs work is covered in [Give a sandbox port an HTTPS URL](/guides/give-a-sandbox-port-an-https-url).

## 3. Run a remote WebDriver test against it

A WebDriver client sends many requests to paths under the Grid address (`/session`, `/session/<id>/url` and so on). So give `webdriver.Remote` the bare address and send the token as the `__sbx_auth` cookie on every request. Selenium's `ClientConfig` takes extra headers for this ([Selenium HTTP client docs](https://www.selenium.dev/documentation/webdriver/drivers/http_client/)).

```python
# remote_test.py: run one WebDriver test on the Grid in the sandbox
import os, sys, time
from urllib.parse import urlsplit, parse_qs
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.remote.client_config import ClientConfig

hosted = os.environ["GRID_URL"]          # the private URL from `sbx host`, with ?_token=…
parts = urlsplit(hosted)
token = parse_qs(parts.query)["_token"][0]
grid = f"{parts.scheme}://{parts.netloc}"

browser = sys.argv[1] if len(sys.argv) > 1 else "chrome"
options = webdriver.ChromeOptions() if browser == "chrome" else webdriver.FirefoxOptions()
config = ClientConfig(remote_server_addr=grid,
                      extra_headers={"Cookie": f"__sbx_auth={token}"})

t0 = time.perf_counter()
driver = webdriver.Remote(command_executor=grid, options=options, client_config=config)
t1 = time.perf_counter()
try:
    driver.get("https://www.selenium.dev/selenium/web/web-form.html")
    driver.find_element(By.NAME, "my-text").send_keys("hello from the grid")
    driver.find_element(By.CSS_SELECTOR, "button").click()
    message = driver.find_element(By.ID, "message").text
    t2 = time.perf_counter()
    caps = driver.capabilities
    print(f"{caps['browserName']} {caps['browserVersion']} | {driver.title} | {message}")
    print(f"new session {t1 - t0:.2f}s, fill + submit {t2 - t1:.2f}s")
finally:
    driver.quit()
```

The page under test is the Selenium project's own demo form. Run it from your machine:

```bash
pip install selenium                      # 4.51.0 in our run
export GRID_URL='https://apt-scarlet-ginger-4444.box.sandboxer.com?_token=<token>'
python remote_test.py chrome
python remote_test.py firefox
```

```text
chrome 155.0.8059.79 | Web form - target page | Received!
new session 0.62s, fill + submit 1.41s
firefox 158.0 | Web form - target page | Received!
new session 1.85s, fill + submit 1.46s
```

The Grid picks the node from the browser in your options, so the same URL serves both browsers.

## 4. Where to run the test runner

Every WebDriver call is an HTTP round trip to the Grid. We timed 20 calls to `driver.title` in one session, first from our machine through the hosted URL, then from inside the sandbox against `http://localhost:4444`:

```text
apt-scarlet-ginger-4444: median 135.9 ms per command
localhost:4444: median 6.8 ms per command
```

The gap is network distance, about 20 times per command in our test. Your number depends on where you are; the sandbox ran in `eu-central`. A test with 200 WebDriver calls spends about 27 seconds waiting on the network from outside, and about 1.4 seconds inside. Two ways to use this:

| Situation | Run the tests from | Grid URL |
| - | - | - |
| Debugging one test from your IDE | Your machine | The private hosted URL, cookie header as above |
| A full suite, or tests in CI | Inside the sandbox: `sbx scp` the tests in, `sbx exec` them | `http://localhost:4444`, no token |
| Many suites at once | One sandbox per suite (see [Run coding agents in parallel](/guides/run-coding-agents-in-parallel)) | `localhost` in each |

Inside the sandbox, Python, `uv` and Docker are already there. `uv venv .venv && uv pip install --python .venv/bin/python selenium` took a few seconds in our run.

### More sessions than slots

We started 4 Chrome tests at once against the 2 Chrome slots. All 4 passed. The Grid queued the extra session requests and started them as slots freed up, so new-session times ranged from 0.69 s to 6.32 s, and the batch took 7.9 s. The README describes the queue timeout settings (`SE_SESSION_REQUEST_TIMEOUT`) if your suites queue for longer.

## 5. Watch a test through noVNC

Each node image serves a noVNC page on port 7900. The compose file publishes the Chrome node's; host it like the Grid:

```bash
sbx host $id 7900 --title "Chrome node VNC"
```

Without the token it returned `401`. With it, the noVNC page loaded, and the WebSocket handshake to `/websockify` returned `101` with the cookie. The VNC password is `secret` unless you set `SE_VNC_PASSWORD` ([docker-selenium VNC docs](https://github.com/SeleniumHQ/docker-selenium#using-your-browser-no-vnc-client-is-needed)). The hosted URL's token stays the first lock; the VNC password is a second one.

This view shows the browsers inside the node container. It is separate from the sandbox desktop (`sbx desktop`), which [Run Browser Use or Playwright with real Chrome](/guides/run-browser-use-or-playwright-with-real-chrome) uses with the Chrome installed on the VM itself.

## 6. Stop, resume and cold boot

**Stop and resume with memory.** `sbx stop $id --wait` took 2.2 s and `sbx resume $id` 1.3 s. The sandbox showed `memoryRestored: true`. The three containers had the same uptime as before the stop, the Grid was ready, and the remote Chrome and Firefox tests passed again on the same URL and token. A stopped sandbox costs nothing for compute.

**Boot from disk.** Memory is restored only on a machine with the same CPU type; otherwise the sandbox boots from disk. We forced that by resuming on another size (`sbx resume $id --type large`, which reported `memoryRestored: false`, `resumeReason: "resized"`):

```text
grid-chrome-1 Up 5 seconds
grid-firefox-1 Up 5 seconds
grid-selenium-hub-1 Up 5 seconds
ready: True | Selenium Grid ready.
```

Docker started all three containers again because of `restart: unless-stopped`. The hosted route and token were kept. Our first remote test right after the boot got the proxy's "Nothing is answering on port 4444" page, because the hub wasn't listening yet; a retry a few seconds later passed. In CI, poll `/status` until `ready` is true before you start the suite. Moving to a smaller size isn't supported yet, so size the sandbox for the Grid you want from the start.

## FAQ

### Can I use `selenium/standalone-chrome` instead of a hub and nodes?

Yes. The README's quick start runs one standalone container on port 4444 with `--shm-size="2g"`. Host port 4444 the same way and the client code doesn't change. We tested the hub-and-nodes layout because it runs Chrome and Firefox behind one URL.

### Why not put the `?_token=` URL straight into `command_executor`?

The client adds paths such as `/session` to the address you give it, and the token belongs in the query string, not in the path. Pass the bare address and send the token as the `__sbx_auth` cookie, as in step 3. For a public Grid (`sbx host $id 4444 --public`) no cookie is needed, but then anyone with the URL can start browsers in your sandbox.

### How many parallel browsers fit in a `default` sandbox?

The docker-selenium README recommends at most one browser session per CPU, which is 4 on a `default` sandbox. Our idle Grid used about 1.5 GB of the 8 GB. If a browser runs out of memory, its process exits with code 137; see [Fix exit code 137 or a full disk](/guides/fix-exit-code-137-or-a-full-disk-in-a-sandbox).

### Did any request fail?

Two did in about 20 remote runs on October 27, 2026. One came right after a cold boot, before the hub was listening (step 6). The other was a rare 502 during a platform update; retry after a few seconds. The retry passed and the next 15 runs all passed. Poll `/status` before a suite starts.

## Related

- [Give a sandbox port an HTTPS URL](/guides/give-a-sandbox-port-an-https-url)
- [Run Docker Compose in a sandbox with a URL per service](/guides/run-docker-compose-in-a-sandbox-with-a-url-per-service)
- [Run Browser Use or Playwright with real Chrome](/guides/run-browser-use-or-playwright-with-real-chrome)
- [Hosting](/docs/hosting)
