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.
| Component | Role |
|---|---|
| Router | The endpoint your tests connect to, on port 4444 |
| Distributor | Matches a requested capability set to a free node |
| Session map | Tracks which session lives on which node |
| Node | A machine or container running browser drivers |
| Event bus | Internal 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 truelets 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.Remoteinstead ofwebdriver.Chrome. - Request capabilities narrowly. Asking for
chromeon 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 -vshm_size: 2gbprevents the browser crashing in a container with a small/dev/shm; it is the most common fix for random node failures.SE_NODE_MAX_SESSIONScontrols 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/- Publish the JUnit XML so the pipeline can show which tests failed rather than a wall of stdout.
- Upload artefacts with
if: always(), or a failing run gives you nothing to inspect. - Set a job timeout; a hung browser session otherwise holds a runner until the platform kills it.
- Keep the failing artefacts for a few days and delete them automatically - screenshots and page dumps contain user data.
- 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.htmlFAQ
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.
Related
Structuring a suite: page objects, pytest and parallelism Screenshots, logs and debugging flaky tests
Last refreshed 2026-09-18.