Selenium Grid, Docker and CI

Run the same suite against a Grid of browser nodes, containerise the setup, and keep the reporting and artefacts a pipeline needs.

Grid architecture

Grid separates the session request from the browser that fulfils it. Your test connects to the Grid's router; the Grid decides which node can satisfy the requested capabilities and forwards the session. Tests do not need to know where the browser is running.

ComponentRole
RouterThe endpoint your tests connect to, on port 4444
DistributorMatches a requested capability set to a free node
Session mapTracks which session lives on which node
NodeA machine or container running browser drivers
Event busInternal transport between the components
from selenium import webdriver
from selenium.webdriver.chrome.options import Options

options = Options()
options.set_capability("browserName", "chrome")
options.set_capability("browserVersion", "stable")
options.set_capability("platformName", "linux")

driver = webdriver.Remote(
    command_executor="http://grid.internal:4444",
    options=options,
)
driver.get("https://example.com")
driver.quit()
  • In modern Grid the hub and node are the same distribution; a single jar can run as standalone, hub or node.
  • --detect-drivers true lets a node advertise the browsers it finds on the machine.
  • Switching from a local driver to a Grid is a change of one line: webdriver.Remote instead of webdriver.Chrome.
  • Request capabilities narrowly. Asking for chrome on any platform gives the scheduler the most room to place the session quickly.
💡
A Grid node only sees files on its own filesystem. Anything the test uploads must exist on the node, so either use Selenium's file detector to ship it with the session or mount a shared volume that every node can read.

Docker Compose

services:
  chrome:
    image: selenium/node-chrome:4.27.0
    shm_size: 2gb
    depends_on:
      - hub
    environment:
      - SE_EVENT_BUS_HOST=hub
      - SE_EVENT_BUS_PUBLISH_PORT=4442
      - SE_EVENT_BUS_SUBSCRIBE_PORT=4443
      - SE_NODE_MAX_SESSIONS=4
      - SE_NODE_OVERRIDE_MAX_SESSIONS=true

  firefox:
    image: selenium/node-firefox:4.27.0
    shm_size: 2gb
    depends_on:
      - hub
    environment:
      - SE_EVENT_BUS_HOST=hub
      - SE_EVENT_BUS_PUBLISH_PORT=4442
      - SE_EVENT_BUS_SUBSCRIBE_PORT=4443

  hub:
    image: selenium/hub:4.27.0
    ports:
      - "4444:4444"
docker compose up -d --scale chrome=3
open http://localhost:4444        # the Grid console
docker compose logs -f chrome
docker compose down -v
  • shm_size: 2gb prevents the browser crashing in a container with a small /dev/shm; it is the most common fix for random node failures.
  • SE_NODE_MAX_SESSIONS controls how many sessions a node accepts; more sessions per node is cheaper and slower per session.
  • Pin the image tag. A floating tag means your suite changes without a commit.
  • Scale nodes for parallelism rather than running several browsers in one node.

A CI job with reports

name: e2e
on: pull_request

jobs:
  selenium:
    runs-on: ubuntu-latest
    services:
      chrome:
        image: selenium/standalone-chrome:4.27.0
        ports: ["4444:4444"]
        options: >-
          --shm-size=2g
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install -r requirements.txt

      - name: Run the suite
        env:
          BASE_URL: ${{ vars.STAGING_URL }}
          SELENIUM_REMOTE_URL: http://localhost:4444
        run: pytest -n 4 --junitxml=reports/junit.xml -q

      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: results
          path: |
            reports/
            artifacts/
  1. Publish the JUnit XML so the pipeline can show which tests failed rather than a wall of stdout.
  2. Upload artefacts with if: always(), or a failing run gives you nothing to inspect.
  3. Set a job timeout; a hung browser session otherwise holds a runner until the platform kills it.
  4. Keep the failing artefacts for a few days and delete them automatically - screenshots and page dumps contain user data.
  5. Run the smoke subset on every pull request and the full suite nightly, so feedback stays fast.
# fail the build from the report, with a readable summary
python -m junitparser merge reports/*.xml merged.xml
python -m junit2html merged.xml reports/summary.html

FAQ

Should I run Selenium in Docker locally too?
Yes, when you want the same configuration everywhere and a clean browser per run. The trade-off is slower startup and no visible browser, so many teams run headed locally while developing a test and use the container images for CI.
Do I need a Grid if I only run Chrome?
No. Use the standalone image, which is a Grid with one node, and connect to it as a remote driver. A full Grid becomes worthwhile when you need multiple browsers or more parallelism than one machine can provide.

Structuring a suite: page objects, pytest and parallelism Screenshots, logs and debugging flaky tests

Last refreshed 2026-09-18.