# Deploy Selenium Grid with the selenium-grid-testing template

Source: https://docs.quake.ai/resources/deployments/deploy-selenium-grid-testing-template
Markdown: https://docs.quake.ai/resources/deployments/deploy-selenium-grid-testing-template.md

---

# Deploy Selenium Grid with the selenium-grid-testing template

Stand up a [Selenium Grid 4](https://www.selenium.dev/documentation/grid/) hub and three browser nodes on a single Quake AI instance using the [validated OpenTofu template](/docs/platform/validation#how-infrastructure-templates-are-checked) `selenium-grid-testing`. You apply the template, confirm the nodes register with the hub, run one sample WebDriver test against Chrome and one against Firefox, and scale the number of Chrome nodes.



A Selenium Grid ships with no login and no API token. Anyone who can reach port 4444 can drive a browser session, and anyone who can reach 4442 or 4443 can register a rogue node with the hub. This deployment restricts every grid port to `grid_allowed_cidr`, which defaults to the private network only. Reach the grid from a CI runner or workstation on that network, or scope `grid_allowed_cidr` to your CI runner IPs or VPN CIDR. Never point a public domain at this grid.



The grid runs on infrastructure you own; this is a self-hosted tool you operate for CI, not a managed multi-tenant device farm.

<Figure size="md" caption="What you'll build: a stateless Selenium Grid hub and three browser nodes on a single instance, reached over an SSH tunnel or a CIDR-restricted CI runner">

```d2
direction: right

runner: CI runner or workstation {shape: person}
fip: Floating IP
instance: Ubuntu instance {
  hub: Selenium hub
  chrome1: Chrome node 1
  chrome2: Chrome node 2
  firefox1: Firefox node
  chrome1 -> hub: registers over event bus
  chrome2 -> hub: registers over event bus
  firefox1 -> hub: registers over event bus
}

runner -> fip: WebDriver session (4444), restricted CIDR
fip -> instance.hub
```

</Figure>

<PricingCompanion
  components={[
    { kind: "template", slug: "selenium-grid-testing", required: true },
  ]}
/>

## Prerequisites

You need:

- OpenTofu 1.6.0 or later (or Terraform 1.6.0 or later) installed locally.
- Your OpenStack credentials sourced into the shell (`source openrc.sh`). See [the OpenStack CLI guide](/docs/tools/openstack-cli).
- An SSH keypair that already exists in your project. Record its name for the `key_name` variable.
- A copy of the `selenium-grid-testing` template directory from [the template reference page](/resources/iac-templates/selenium-grid-testing).
- Python 3 installed locally, to run the sample WebDriver scripts in this walkthrough.

## Step 1: Apply the template

Copy the template's example variables file and set `key_name`. If you plan to reach the grid directly from your workstation rather than tunneling over SSH, also set `grid_allowed_cidr` to your workstation's CIDR:

```bash
cp terraform.tfvars.example terraform.tfvars
```

```hcl
key_name = "YOUR_KEY_NAME"
# grid_allowed_cidr = "YOUR_WORKSTATION_IP/32"
```

Initialize, preview, and apply:

```bash
tofu init
tofu plan
tofu apply
```

OpenTofu provisions a private network, a router, a security group, an instance, and a floating IP. On first boot, cloud-init installs Docker Engine and starts the hub and all three nodes: no credential to generate and no manual configuration step blocks first use.

Read the outputs and record `floating_ip` and `grid_url`:

```bash
tofu output
```

## Step 2: Confirm the nodes registered with the hub

Open `grid_url` from the previous step (`http://YOUR_FLOATING_IP:4444`) in a browser. If you have not opened `grid_allowed_cidr` to your workstation, tunnel over SSH instead:

```bash
ssh -L 4444:localhost:4444 ubuntu@YOUR_FLOATING_IP
```

Then open `http://localhost:4444`. The Grid UI shows the hub's status. Confirm three nodes are listed: two Chrome and one Firefox. Nodes usually finish registering within a few seconds of the containers starting.

## Step 3: Run a sample WebDriver test against Chrome and Firefox

Install the Python Selenium client locally:

```bash
pip install selenium
```

Save the following as `grid_test.py`, then run it once with `BROWSER=chrome` and once with `BROWSER=firefox`:

```python
import os

from selenium import webdriver
from selenium.webdriver.chrome.options import Options as ChromeOptions
from selenium.webdriver.firefox.options import Options as FirefoxOptions

grid_url = "http://localhost:4444/wd/hub"  # or your floating IP if grid_allowed_cidr permits
browser = os.environ.get("BROWSER", "chrome")

options = ChromeOptions() if browser == "chrome" else FirefoxOptions()
driver = webdriver.Remote(command_executor=grid_url, options=options)

try:
    driver.get("https://example.com")
    print(f"{browser}: page title is '{driver.title}'")
finally:
    driver.quit()
```

```bash
BROWSER=chrome python grid_test.py
BROWSER=firefox python grid_test.py
```

Each run prints the page title, confirming the grid dispatched the session to a registered node of the requested browser type.

## Step 4: Scale the number of Chrome nodes

SSH to the instance and add more Chrome capacity:

```bash
cd /opt/selenium
sudo docker compose up -d --scale chrome-1=3
```

The Grid UI now shows additional Chrome nodes registered alongside the original two. Any new node needs the same `shm_size: 2gb` and `SE_EVENT_BUS_HOST` environment variables as the existing services in `/opt/selenium/docker-compose.yml`, which `--scale` reuses automatically since it replicates the same service definition.

## What you built

- **Applied the `selenium-grid-testing` template** to provision a network, security group, instance, and floating IP, with the hub and three browser nodes started automatically by cloud-init
- **Confirmed the nodes registered** with the hub over the Grid UI
- **Ran a sample WebDriver test** against both a Chrome node and a Firefox node
- **Scaled the number of Chrome nodes** with `docker compose up -d --scale`

## Scope of this deployment

This template runs a single-VM Selenium Grid, not a managed device farm. The instance is CPU-only and runs in one region, has no built-in authentication, and holds no persistent state: sessions are ephemeral and nothing survives a container restart. You operate the instance, Docker, the hub, and every node yourself: patch the pinned `selenium_version` tag periodically and watch memory use as you add nodes or run heavier test suites.

## Next steps

- [Selenium Grid testing template](/resources/iac-templates/selenium-grid-testing): the template reference, parameters, and resource map
- [Forgejo Git and CI](/resources/iac-templates/forgejo-git-ci): a self-hosted CI runner to trigger test suites against this grid
- [Security hardening checklist](/docs/security/hardening-checklist): tighten SSH access and exposure before you widen `grid_allowed_cidr`

## Clean up

When you no longer need the deployment, destroy everything the template created:

```bash
tofu destroy
```

Because every container in this stack is stateless, `tofu destroy` removes the grid entirely with no data to export first.
