Skip to content

Selenium Grid testing

Template

Selenium Grid testing

This pattern composes Compute and Network into a self-hosted browser-testing grid for WebDriver-based UI test suites, on infrastructure you control.

What this template does#

Provisions a single instance running a Selenium Grid 4 hub plus three browser nodes (two Chrome, one Firefox), a self-hosted alternative to BrowserStack or Sauce Labs for CI test runs:

  • Compute instance that runs the hub and every node as Docker containers on 8 vCPU and 8 GiB RAM, sized for the memory headroom three browser containers need
  • Private network, subnet, router, port, and security group; a floating IP for reaching the grid from a CI runner outside the private network
  • No block volume and no bundled database: sessions are ephemeral by design, so only the default boot disk is used
  • cloud-init installs Docker Engine and starts the hub and all three nodes automatically; there is no credential to generate and no manual configuration step

No built-in authentication#

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 template restricts all three ports to grid_allowed_cidr, which defaults to the private network only, and never fronts the grid with a public HTTPS domain the way the other self-hosted tools in this library are. Reach the grid from a CI runner on the private network, or scope grid_allowed_cidr to your CI runner IPs or VPN CIDR.

Parameters#

ParameterDescriptionDefault
key_nameSSH keypair name (must already exist)No default
flavor_nameInstance size (hub plus three browser nodes needs 8 vCPU / 8 GiB)s1a.large
image_nameOperating system imageUbuntu-24.04
app_nameDisplay name prefix for resourcesselenium-grid
external_networkExternal network for floating IP allocationPublicStatic
private_cidrCIDR for the private subnet10.54.0.0/24
grid_allowed_cidrCIDR allowed to reach the grid ports (4442-4444)10.54.0.0/24
selenium_versionImage tag pinned across the hub and every node4.45.0-20260606

Finish setup after apply#

cloud-init starts the hub and all three nodes immediately; the grid is reachable on port 4444 within a few seconds of the containers starting, once the nodes register with the hub over the event bus:

  1. Open grid_url from the outputs (or tunnel over SSH) and confirm the Grid UI shows three registered nodes.
  2. Point a WebDriver client at <grid_url>/wd/hub or the bare grid_url; both endpoints work identically against Grid 4.

Scale nodes#

Add more Chrome or Firefox capacity with docker compose up -d --scale chrome-1=N from /opt/selenium, or add a selenium/node-edge service to the compose file for a third browser. Give each additional node its own shm_size: 2gb and the same SE_EVENT_BUS_HOST environment variables as the existing nodes.

Access and security#

The security group restricts ports 4442-4444 to grid_allowed_cidr. Never widen this to 0.0.0.0/0: the grid has no built-in authentication, so an open port exposes an unauthenticated WebDriver endpoint that anyone can use to drive a browser or register a rogue node.

When to use this pattern#

Run WebDriver-based UI test suites from CI without a per-minute device-farm bill. Sessions are ephemeral: nothing a test run produces persists across a container restart, so there is nothing to back up beyond the pinned image tag and the compose file itself.

Estimated cost#

Monthly cost estimate

Pricing calculator ↗

Sized as a custom package on shared vCPU.

Starting template$63.40/mo

Monthly total for the required template above. Use the configurator below to add optional pieces and see the total update.

What each resource is for

Selenium Grid host

s1a.large · 8 shared vCPU, 8 GiB RAM, 0.5 Gbps

Runs a stateless Selenium Grid hub plus two Chrome nodes and one Firefox node, all ephemeral with no attached data volume.

The hub plus three browser node containers need real memory headroom to avoid crashes under load; 8 vCPU and 8 GiB RAM is the floor. Size up further for more nodes or heavier test suites.

$66.00/mo

Compute shown per role at custom-package rates ($29/dedicated vCPU, $7.25/shared vCPU, $1/GiB RAM). The headline above is the billed total: the cheaper of a named plan and the custom package, plus add-ons.

Included in baseline

s1a.large

8 shared vCPU, 8 GiB RAM, 0.5 Gbps

$66.00

Compute + RAM rate basis

8 vCPU + 8 GiB RAM at $29/dedicated vCPU, $7.25/shared vCPU, $1/GiB RAM (regular). Totals apply the flat −$5/mo package promotion.

—

Block storage (30 GiB)

30 GiB at $0.08/GiB/mo

$2.40

Public IP (included)

1 included with the custom package

$0.00

Package promotional discount

Flat −$5.00/mo on the custom package (same promotion as named plans).

$-5.00

Included at no charge

These line items are zero on Quake AI. Many other providers meter them separately.

Data transfer (inbound and outbound)

Unlimited data transfer on every plan; Quake AI does not meter per-GB egress.

AWS, GCP, and Azure meter outbound transfer per GB. DigitalOcean and Hetzner include an allowance on compute plans, then charge overage.

Learn more
$0.00

Private networking

Private networks, subnets, Neutron routers, and security groups are included with the plan.

VPC objects are usually free to create elsewhere, but NAT gateways bill hourly plus per-GB processed. Quake AI uses router SNAT with no separate NAT line item.

$0.00

Control-plane API requests

OpenStack API calls for provisioning and management are included.

Some managed services on other clouds meter API calls or charge for premium control-plane features.

$0.00

Pricing data last validated: . For current rates, check quake.ai/pricing.

Template source#

7 files. Download the zip or expand to copy any file.Download selenium-grid-testing.zip
Show source (7 files)
main.tfHCL
data "openstack_images_image_v2" "os" {
  name        = var.image_name
  most_recent = true
}

data "openstack_networking_network_v2" "external" {
  name = var.external_network
}

resource "openstack_networking_network_v2" "private" {
  name           = "${var.app_name}-net"
  admin_state_up = true
}

resource "openstack_networking_subnet_v2" "private" {
  name            = "${var.app_name}-subnet"
  network_id      = openstack_networking_network_v2.private.id
  cidr            = var.private_cidr
  ip_version      = 4
  dns_nameservers = ["1.1.1.1", "8.8.8.8"]
}

resource "openstack_networking_router_v2" "main" {
  name                = "${var.app_name}-router"
  external_network_id = data.openstack_networking_network_v2.external.id
}

resource "openstack_networking_router_interface_v2" "private" {
  router_id = openstack_networking_router_v2.main.id
  subnet_id = openstack_networking_subnet_v2.private.id
}

resource "openstack_networking_secgroup_v2" "selenium" {
  name        = "${var.app_name}-sg"
  description = "SSH plus the Grid event bus and WebDriver ports, restricted to grid_allowed_cidr since Selenium Grid has no built-in authentication"
}

resource "openstack_networking_secgroup_rule_v2" "ssh" {
  direction         = "ingress"
  ethertype         = "IPv4"
  protocol          = "tcp"
  port_range_min    = 22
  port_range_max    = 22
  remote_ip_prefix  = "0.0.0.0/0"
  security_group_id = openstack_networking_secgroup_v2.selenium.id
}

# Ports 4442 (event bus publish) and 4443 (event bus subscribe) let the nodes
# register with the hub; 4444 carries the Grid UI and the WebDriver endpoint.
# None of these carry authentication, so this rule is restricted to
# grid_allowed_cidr, never opened to 0.0.0.0/0.
resource "openstack_networking_secgroup_rule_v2" "grid" {
  direction         = "ingress"
  ethertype         = "IPv4"
  protocol          = "tcp"
  port_range_min    = 4442
  port_range_max    = 4444
  remote_ip_prefix  = var.grid_allowed_cidr
  security_group_id = openstack_networking_secgroup_v2.selenium.id
}

resource "openstack_networking_port_v2" "selenium" {
  name               = "${var.app_name}-port"
  network_id         = openstack_networking_network_v2.private.id
  security_group_ids = [openstack_networking_secgroup_v2.selenium.id]

  fixed_ip {
    subnet_id = openstack_networking_subnet_v2.private.id
  }

  depends_on = [openstack_networking_router_interface_v2.private]
}

resource "openstack_compute_instance_v2" "selenium" {
  name        = var.app_name
  flavor_name = var.flavor_name
  key_pair    = var.key_name

  user_data = templatefile("${path.module}/cloud-init/selenium-grid.yaml.tftpl", {
    app_name         = var.app_name
    selenium_version = var.selenium_version
  })

  block_device {
    uuid                  = data.openstack_images_image_v2.os.id
    source_type           = "image"
    destination_type      = "volume"
    volume_size           = 30
    boot_index            = 0
    delete_on_termination = true
  }

  network {
    port = openstack_networking_port_v2.selenium.id
  }
}

resource "openstack_networking_floatingip_v2" "selenium" {
  pool = var.external_network
}

resource "openstack_networking_floatingip_associate_v2" "selenium" {
  floating_ip = openstack_networking_floatingip_v2.selenium.address
  port_id     = openstack_networking_port_v2.selenium.id
}
variables.tfHCL
variable "key_name" {
  description = "SSH keypair name (must already exist in your project)"
  type        = string
}

variable "flavor_name" {
  description = "Instance size. The hub plus three browser node containers (two Chrome, one Firefox) need real memory headroom to avoid crashes under load; 8 vCPU and 8 GiB RAM (s1a.large) is the floor."
  type        = string
  default     = "s1a.large"
}

variable "image_name" {
  description = "Operating system image. Ubuntu 24.04 is the recommended base."
  type        = string
  default     = "Ubuntu-24.04"
}

variable "app_name" {
  description = "Display name prefix for compute and network resources"
  type        = string
  default     = "selenium-grid"
}

variable "external_network" {
  description = "Shared external network for router gateway and floating IPs; defaults to PublicStatic (persisted FIP / production pattern). Override with PublicEphemeral for ephemeral demos."
  type        = string
  default     = "PublicStatic"
}

variable "private_cidr" {
  description = "CIDR for the private tenant network the instance lives in"
  type        = string
  default     = "10.54.0.0/24"
}

variable "grid_allowed_cidr" {
  description = "CIDR allowed to reach the Grid UI and WebDriver endpoint (ports 4442-4444). Defaults to the private network only. A Selenium Grid has no built-in authentication: never widen this to 0.0.0.0/0. Scope it to your CI runner IPs or VPN CIDR, or reach the grid over an SSH tunnel instead."
  type        = string
  default     = "10.54.0.0/24"
}

variable "selenium_version" {
  description = "Image tag pinned across the hub and every node, so the Grid server version and bundled browser/driver versions stay in lockstep. Verify the current stable release tag at https://github.com/SeleniumHQ/docker-selenium/releases before applying; do not use 'latest' in a kept-running deployment."
  type        = string
  default     = "4.45.0-20260606"
}
outputs.tfHCL
output "instance_id" {
  description = "ID of the compute instance running the Selenium Grid hub and nodes"
  value       = openstack_compute_instance_v2.selenium.id
}

output "floating_ip" {
  description = "Public floating IP address of the Selenium Grid host"
  value       = openstack_networking_floatingip_v2.selenium.address
}

output "private_ip" {
  description = "Private IP address of the instance"
  value       = openstack_compute_instance_v2.selenium.access_ip_v4
}

output "grid_url" {
  description = "Grid UI and WebDriver endpoint. Reachable from grid_allowed_cidr (the private network by default) only: the grid has no built-in authentication, so this endpoint must never be exposed to the public internet. Point WebDriver clients at http://<grid_url>/wd/hub, or at the bare grid_url for Grid 4's native endpoint."
  value       = "http://${openstack_networking_floatingip_v2.selenium.address}:4444"
}
versions.tfHCL
terraform {
  required_version = ">= 1.6.0"

  required_providers {
    openstack = {
      source  = "terraform-provider-openstack/openstack"
      version = "~> 2.0"
    }
  }
}

provider "openstack" {}
terraform.tfvars.exampleHCL
# Required: SSH keypair must already exist in your project
key_name = "YOUR_KEY_NAME"

# Recommended: restrict the grid ports (4442-4444) to your CI runner IPs or
# VPN CIDR. Leave unset to keep the grid reachable only from the private
# network and tunnel over SSH. Never widen this to 0.0.0.0/0: the grid has no
# built-in authentication.
# grid_allowed_cidr = "203.0.113.0/24"

# flavor_name = "s1a.large"
# image_name = "Ubuntu-24.04"
# app_name = "selenium-grid"
# external_network = "PublicStatic"
# private_cidr = "10.54.0.0/24"
# selenium_version = "4.45.0-20260606"
cloud-init/selenium-grid.yaml.tftpl
#cloud-config
package_update: true
packages:
  - ca-certificates
  - curl
write_files:
  - path: /opt/selenium/docker-compose.yml
    permissions: "0644"
    content: |
      # Selenium Grid 4 for ${app_name}: a stateless hub plus two Chrome nodes
      # and one Firefox node, registering over the event bus. There is no
      # datastore and no volume: sessions are ephemeral, and nothing persists
      # across a container restart. Unlike every other self-hosted ops tool in
      # this library, this stack has no built-in authentication, so it is
      # never fronted with a public domain; the security group restricts
      # ports 4442-4444 to grid_allowed_cidr.
      services:
        selenium-hub:
          image: selenium/hub:${selenium_version}
          container_name: selenium-hub
          restart: unless-stopped
          ports:
            - "4442:4442"
            - "4443:4443"
            - "4444:4444"
        chrome-1:
          image: selenium/node-chrome:${selenium_version}
          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
        chrome-2:
          image: selenium/node-chrome:${selenium_version}
          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
        firefox-1:
          image: selenium/node-firefox:${selenium_version}
          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
runcmd:
  - |
    set -e
    # Install Docker Engine plus the compose plugin from Docker's convenience
    # script. No data volume to mount: every container in this stack is
    # stateless, so the boot disk is the only storage this template uses.
    curl -fsSL https://get.docker.com | sh
    # Bring the hub and all three nodes up. No credential to generate and no
    # manual configuration step blocks first use: the grid is reachable on
    # port 4444 as soon as the nodes finish registering with the hub, usually
    # within a few seconds of the containers starting.
    cd /opt/selenium
    docker compose up -d
README.mdMarkdown
# Selenium Grid testing

Single compute instance running a [Selenium Grid 4](https://www.selenium.dev/documentation/grid/) hub plus three browser nodes (two Chrome, one Firefox), a self-hosted alternative to BrowserStack or Sauce Labs for running WebDriver-based UI test suites from CI, on infrastructure you control.


**Network class:** production — `external_network` defaults to `PublicStatic` for persisted floating IPs and multi-tier stacks; override with `PublicEphemeral` for ephemeral demos.

The instance provisions a private network and a floating IP. cloud-init installs Docker Engine and brings up the hub and all three nodes with no manual steps required: no encryption key, no auth callback, and no datastore to bring up first.

## Where this fits

A Selenium Grid centralizes browser capacity for parallel WebDriver test runs: CI jobs point a `RemoteWebDriver` client at the grid instead of launching a local browser, and the grid distributes sessions across its registered nodes.

## No built-in authentication: this is not an internet-facing tool

Unlike every other self-hosted ops tool in this library, this template never fronts the app with a public HTTPS domain. 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/4443 can register a rogue node with the hub. The security group restricts ports 4442-4444 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 widen it to `0.0.0.0/0`.

## No datastore, no data volume

Sessions are ephemeral by design: nothing a test run produces persists across a container restart. This template has no block volume and no bundled database, a lighter footprint than every other template in this library.

## Prerequisites

- OpenTofu >= 1.6.0 or Terraform >= 1.6.0
- Quake AI account with OpenStack credentials
- An existing SSH keypair in your project (the value of `key_name` must match that keypair)

## Resource baseline

The hub plus three browser node containers (two Chrome, one Firefox) need real memory headroom to avoid crashes under load. The default `s1a.large` flavor (8 vCPU, 8 GiB RAM) is the floor; size up further if you add more nodes or run heavier test suites.

## Usage

1. Clone or copy this template directory
2. Copy `terraform.tfvars.example` to `terraform.tfvars` and fill in your values
3. Source your OpenStack credentials: `source openrc.sh`
4. Initialize: `tofu init`
5. Preview: `tofu plan`
6. Apply: `tofu apply`

After apply, cloud-init installs Docker and starts the hub and all three nodes. No credential ships with this template: there is none to generate. The grid is reachable on port 4444 within a few seconds of the containers starting, once the nodes finish registering with the hub over the event bus.

## Access and security

The Grid UI and the WebDriver endpoint both live on port 4444; the event bus uses 4442 and 4443 for node registration. All three ports are restricted to `grid_allowed_cidr`. Point WebDriver clients at `http://<floating_ip>:4444/wd/hub` (backward-compatible path) or the bare `http://<floating_ip>:4444` (Grid 4's native endpoint); both work identically against this image.

## Scaling nodes

Add more Chrome or Firefox capacity with `docker compose up -d --scale chrome-1=N` from `/opt/selenium`, or add a `selenium/node-edge` service to the compose file for a third browser. Each additional node needs its own `shm_size: 2gb` and the same `SE_EVENT_BUS_HOST` environment variables as the existing nodes.

## Variables

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `key_name` | string | yes | n/a | SSH keypair name (must already exist in your project) |
| `flavor_name` | string | no | `s1a.large` | Instance size (hub plus three browser nodes runs on 8 vCPU / 8 GiB) |
| `image_name` | string | no | `Ubuntu-24.04` | Operating system image |
| `app_name` | string | no | `selenium-grid` | Display name prefix for resources |
| `external_network` | string | no | `PublicStatic` | Persisted FIP / production default; override with `PublicEphemeral` for demos |
| `private_cidr` | string | no | `10.54.0.0/24` | CIDR for the private subnet |
| `grid_allowed_cidr` | string | no | `10.54.0.0/24` | CIDR allowed to reach the grid ports (4442-4444) |
| `selenium_version` | string | no | `4.45.0-20260606` | Image tag pinned across the hub and every node |

## Outputs

| Name | Description |
| --- | --- |
| `floating_ip` | Public floating IP assigned to the instance |
| `private_ip` | Private IP address of the instance |
| `grid_url` | Grid UI and WebDriver endpoint on port 4444 |
| `instance_id` | Compute instance ID |

## Scope

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

## Documentation

See also: [Forgejo Git and CI](/resources/iac-templates/forgejo-git-ci), [Simple VM deployment](/resources/deployments/deploy-simple-vm-template)
Resources, parameters, and variables
Provisions
Parameterized by
Variables
  • key_namerequired
  • flavor_name="s1a.large"
  • image_name="Ubuntu-24.04"
  • app_name="selenium-grid"
  • external_network="PublicStatic"
  • private_cidr="10.54.0.0/24"
  • grid_allowed_cidr="10.54.0.0/24"
  • selenium_version="4.45.0-20260606"

Customize this pattern#

See also#

Usage Guidelines

The sample code, software libraries, command line tools, proofs of concept, templates, and other related technology on this page (including any of the foregoing that is provided by Quake AI personnel) is provided to you as Quake AI Content under the Quake AI Customer Agreement, or the relevant written agreement between you and Quake AI (whichever applies). Do not use this Quake AI Content in your production accounts, or on production or other critical data. You are responsible for testing, securing, and optimizing the Quake AI Content (such as sample code) as appropriate for production grade use based on your specific quality control practices and standards. Deploying Quake AI Content may incur Quake AI charges for creating or using Quake AI chargeable resources, such as running Compute instances or storing data in Object Storage. Your use is also subject to the Acceptable Use Policy.

For the full policy, see Usage Guidelines.

Was this page helpful?