Resource library

QA How-To

Docker for Selenium Grid: Step by Step (2026)

Build Docker for Selenium Grid with pinned Hub and browser nodes, Compose health checks, Remote WebDriver, scaling, diagnostics, and secure CI operation.

27 min read | 2,961 words

TL;DR

Deploy Docker for Selenium Grid with matching selenium/hub and selenium/node images, a real Hub health check, 2 GB shared memory on browser nodes, and one session per node initially. Point current Remote WebDriver clients at port 4444, scale node replicas by capability, guarantee quit(), collect evidence, and never expose the Grid publicly.

Key Takeaways

  • Pin Selenium Hub, browser node, and client versions to the same reviewed release family.
  • Use Hub and Node Compose services with the included Grid health script and 2 GB shared memory per browser node as a measured starting point.
  • Connect host tests to localhost:4444 and Compose test containers to selenium-hub:4444.
  • Scale node replicas by browser capability before overcommitting several sessions inside one container.
  • Always quit Remote WebDriver sessions and use infrastructure timeouts only as recovery controls.
  • Preserve runner results, Grid logs, status snapshots, and selected browser evidence before Compose teardown.
  • Keep port 4444 private and authenticated because a Grid can reach internal applications and execute powerful browser actions.

Docker for Selenium Grid packages the Grid control plane, browser nodes, drivers, display server, and system libraries into repeatable services. The practical 2026 setup uses matching Selenium 4.45.0 image tags, a Hub plus one or more browser nodes, real readiness checks, one session per adequately resourced node by default, and a Remote WebDriver client that always quits its session.

A Grid is shared browser infrastructure, not merely a way to start Chrome. Once several suites depend on it, you must manage capacity, session queues, network exposure, artifacts, upgrades, and failure ownership. This guide starts with a runnable Compose file and develops it into a CI-ready operating model.

TL;DR

Concern Recommended starting point
Topology Standalone for one browser evaluation, Hub and Nodes for a shared small Grid
Images Pin Hub and every node to 4.45.0-20260606
Node memory shm_size: 2gb for each browser node
Concurrency One session per node until resource measurements justify more
Readiness Included /opt/bin/check-grid.sh health check against port 4444
Client URL http://localhost:4444 from host, http://selenium-hub:4444 inside Compose
Scaling docker compose up -d --scale chrome=3 without fixed container names
Security Private network, authenticated gateway, least privilege, no public port 4444

Start with one Chrome and one Firefox node, execute a small remote smoke test, observe session creation and cleanup, then scale node replicas. Increasing SE_NODE_MAX_SESSIONS is not the first scaling step.

1. Understand the Selenium Grid 4 Architecture

Selenium Grid receives W3C WebDriver commands and routes sessions to nodes with matching capabilities. In a Hub and Node deployment, the Hub combines the Router, Distributor, New Session Queue, Session Map, and Event Bus responsibilities. Browser nodes register their available slots and execute sessions.

The request path explains many failures:

  1. A client sends a new-session request to port 4444.
  2. The Router places it in the New Session Queue.
  3. The Distributor matches requested capabilities with a registered free slot.
  4. The Session Map records which node owns the session.
  5. Later commands are routed to that node until quit() closes the session.

If no matching slot is free, the request waits in the queue until capacity becomes available or the request times out. A long wait is therefore not automatically a browser launch problem. It may be capacity, capability mismatch, failed node registration, or an abandoned session.

Docker separates these processes into replaceable services. The Hub and nodes communicate on a private network through the Event Bus. Only the Router entry point normally needs to be reachable by test clients. Publishing Event Bus ports to an untrusted network expands the attack surface without helping a host-side client.

Grid supplies browsers, routing, and observability. Your test framework still supplies discovery, assertions, retries, data isolation, and reports. Learn the runner side in Selenium RemoteWebDriver best practices.

2. Choose a Docker for Selenium Grid Topology

Selenium Docker images support several execution modes. Select the smallest topology that meets the actual sharing and scaling need.

Mode Best fit Main limitation
Standalone browser Local evaluation, one isolated CI job, one browser family Scaling means adding independent endpoints or orchestration
Standalone all browsers Lightweight convenience when image size is acceptable Browser availability differs by architecture
Hub and Nodes Shared small or medium Grid with explicit browser pools Team manages node replicas and capacity
Fully distributed Grid Large or specialized control-plane scaling More services, networking, and operational complexity
Dynamic Grid Create browser containers per session Requires Docker socket or compatible daemon access and stronger host security
Kubernetes Helm deployment Elastic cluster-native operation Kubernetes ownership, storage, ingress, and autoscaling are required

For a first shared Grid, Hub and Nodes is a good teaching and operating boundary. Chrome and Firefox capacity can scale independently, and the Grid UI shows registration and sessions. Standalone is simpler when each CI job owns its own Grid and never shares it.

Dynamic Grid can provide strong session-level disposal, but mounting the Docker socket gives the controlling container significant power over the host. Treat that as infrastructure, not a convenient test setting. Isolate the daemon and review the official Dynamic Grid configuration.

Do not build a fully distributed topology just because the components exist. Separate Router, Distributor, Queue, Session Map, and Event Bus services when scale or failure data justifies the complexity. For another distribution model, compare Selenium Grid vs Playwright sharding.

3. Pin Versions and Check Host Prerequisites

The examples use Selenium 4.45.0, released in June 2026, and the official Docker image build 4.45.0-20260606. Use the same full tag for Hub, Chrome, and Firefox. Mixed Grid versions create an unnecessary compatibility variable during registration and troubleshooting.

Verify current tools:

docker version
docker compose version

The official Docker Selenium project recommends current Docker Engine and Compose releases. Your organization's supported versions take priority, but they must support the Compose features used in the file.

Plan resources by concurrent browser session, not container count. The Selenium project uses roughly one CPU and 1 GB of memory per browser as a starting reference, but explicitly recommends measurement. Browser-heavy applications, video recording, large downloads, and multiple tabs can require more.

Architecture also affects browser availability. Firefox and Chromium have broader Arm64 support in official images, while proprietary Chrome and Edge availability can differ. Check the release matrix before forcing an AMD64 image under emulation. Emulation can change performance enough to create misleading timeout failures.

Create a dedicated Docker network through Compose and expose only port 4444 to the host if host-side tests need it. On a shared server, bind to a private interface or place the Grid behind an authenticated proxy. Selenium warns that an exposed Grid can access internal applications and allow hostile browser activity.

4. Create Docker for Selenium Grid With Compose

Save this as compose.yaml:

services:
  selenium-hub:
    image: selenium/hub:4.45.0-20260606
    ports:
      - "4444:4444"
    environment:
      SE_SESSION_REQUEST_TIMEOUT: '120'
      SE_SESSION_RETRY_INTERVAL: '2'
    healthcheck:
      test:
        - CMD-SHELL
        - /opt/bin/check-grid.sh --host 0.0.0.0 --port 4444
      interval: 10s
      timeout: 30s
      retries: 10

  chrome:
    image: selenium/node-chrome:4.45.0-20260606
    shm_size: 2gb
    depends_on:
      selenium-hub:
        condition: service_healthy
    environment:
      SE_EVENT_BUS_HOST: selenium-hub
      SE_EVENT_BUS_PUBLISH_PORT: '4442'
      SE_EVENT_BUS_SUBSCRIBE_PORT: '4443'
      SE_NODE_MAX_SESSIONS: '1'
      SE_NODE_SESSION_TIMEOUT: '120'
      SE_ENABLE_BROWSER_LEFTOVERS_CLEANUP: 'true'

  firefox:
    image: selenium/node-firefox:4.45.0-20260606
    shm_size: 2gb
    depends_on:
      selenium-hub:
        condition: service_healthy
    environment:
      SE_EVENT_BUS_HOST: selenium-hub
      SE_EVENT_BUS_PUBLISH_PORT: '4442'
      SE_EVENT_BUS_SUBSCRIBE_PORT: '4443'
      SE_NODE_MAX_SESSIONS: '1'
      SE_NODE_SESSION_TIMEOUT: '120'
      SE_ENABLE_BROWSER_LEFTOVERS_CLEANUP: 'true'

Start the Grid:

docker compose up -d
docker compose ps
curl --fail http://localhost:4444/status

Open http://localhost:4444/ui for the Grid console. The status response's value.ready should be true, and registered nodes should expose Chrome and Firefox slots.

The included check-grid.sh script makes the Hub health check more reliable than a fixed sleep. Node depends_on waits for Hub health before starting. Node registration still takes time, so a client-side wait or bounded new-session queue remains useful.

Do not add container_name to scalable node services. Compose needs to create unique replica names. Keep the Hub singular in this topology and let Compose manage its generated name as well.

5. Run a Real Remote WebDriver Test

Create a virtual environment and install the matching Python binding:

python -m venv .venv
. .venv/bin/activate
python -m pip install selenium==4.45.0

Save this as grid_smoke.py:

import os

from selenium import webdriver
from selenium.webdriver.common.by import By

grid_url = os.getenv("SELENIUM_GRID_URL", "http://localhost:4444")
browser = os.getenv("BROWSER", "chrome").lower()

if browser == "firefox":
    options = webdriver.FirefoxOptions()
else:
    options = webdriver.ChromeOptions()

options.set_capability("se:name", f"grid smoke: {browser}")

driver = webdriver.Remote(
    command_executor=grid_url,
    options=options,
)

try:
    driver.get("https://example.com")
    heading = driver.find_element(By.CSS_SELECTOR, "h1")
    assert heading.text == "Example Domain"
finally:
    driver.quit()

Run both registered capabilities:

BROWSER=chrome python grid_smoke.py
BROWSER=firefox python grid_smoke.py

Current Selenium clients can use the root Grid URL. The older /wd/hub path remains familiar but is unnecessary for a standard Selenium 4 connection.

Always call quit() in finally or use a framework fixture that guarantees teardown. Closing the last window is not a substitute for ending the remote session. An abandoned session occupies a slot until timeout and can make later tests queue.

Use browser-specific options objects instead of legacy DesiredCapabilities construction. Add only capabilities that the Grid understands. Custom extension capability names must contain a colon, such as myCompany:environment.

6. Scale Nodes and Control Total Concurrency

Scale with additional node containers:

docker compose up -d --scale chrome=3 --scale firefox=2
docker compose ps

This creates three Chrome slots and two Firefox slots because each node uses SE_NODE_MAX_SESSIONS=1. That default favors isolation and predictable resource use. Total suite concurrency should not exceed the useful slots for requested capabilities.

Do not immediately raise SE_NODE_MAX_SESSIONS inside one container. The official images default to one session, and overriding beyond available processors requires SE_NODE_OVERRIDE_MAX_SESSIONS=true. Overcommitting can create browser crashes, timeouts, and misleading flakiness. Scaling containers gives clearer failure boundaries and simpler per-session artifacts.

Capacity is capability-specific. Five free Firefox slots do not satisfy a Chrome request. Likewise, a custom platform or version constraint can make a node ineligible. Monitor requested capabilities and queue time before adding generic replicas.

Calculate end-to-end concurrency:

test runner processes x parallel jobs x data providers x browser sessions per test

A runner configured for eight threads can flood a five-slot Grid. Requests wait, framework setup timeouts expire, and the failure looks random. Either bound runner concurrency to capacity or ensure its new-session timeout accounts for a deliberate queue.

The application under test also has limits. More browsers can overwhelm one test tenant, identity provider, or database. Scale the target and isolate data before interpreting lower Grid time as overall test improvement.

7. Make Readiness, Teardown, and CI Lifecycle Explicit

A container being in the running state does not mean the Grid has registered useful nodes. Poll the status endpoint and require value.ready. For browser-specific readiness, inspect registered node stereotypes or execute a tiny session probe for the required capability.

In CI, give each job its own Compose project so concurrent runs do not share names or networks:

export COMPOSE_PROJECT_NAME="grid-${CI_RUN_ID}"
docker compose up -d --wait
SELENIUM_GRID_URL=http://localhost:4444 python -m pytest
test_status=$?
docker compose logs --no-color > selenium-grid.log
docker compose down --volumes --remove-orphans
exit "${test_status}"

This sample assumes the CI system defines CI_RUN_ID and that pytest is the repository's runner. If the shell uses set -e, temporarily disable it around the test command so logs and teardown still run. Preserve the captured test status.

The Hub's SE_SESSION_REQUEST_TIMEOUT bounds how long a new request waits for a matching slot, while SE_SESSION_RETRY_INTERVAL controls how often the queue retries matching. The node session timeout cleans stale sessions, but it must not replace correct quit() behavior.

Use docker compose down --volumes --remove-orphans after the job. Volumes can contain downloads, videos, or temporary state. Decide what must be copied to CI artifacts before teardown. A canceled or forcibly killed job may skip cleanup, so ephemeral CI workers or scheduled orphan cleanup provide a second line of defense.

8. Collect Logs, Downloads, Screenshots, and Video Deliberately

Grid logs explain registration, capability matching, queue behavior, and node loss. Test runner reports explain assertions. Browser screenshots and video show user-visible behavior. Preserve each source with a clear purpose and run identifier.

Evidence Primary question answered Risk
Runner JUnit Which test and assertion failed? Low, unless names contain sensitive data
Grid logs Was the session queued, matched, or disconnected? Internal hosts and session metadata
Browser screenshot What was visible at failure? User or test data exposure
Downloaded file Did the application produce correct content? Potentially sensitive payload
Video What sequence led to failure? High storage and privacy cost
Grid status snapshot What nodes and slots existed? Infrastructure details

The Docker Selenium project supports video sidecars and dynamic naming, but version the video image according to the official release examples and give recordings unique session metadata. Video can consume significant CPU and disk. Start with screenshots and browser or driver logs, then add video to selected suites where it changes diagnosis.

For downloads, mount a controlled directory or copy files out before teardown. Avoid one shared host directory across concurrent nodes unless filenames are unique and permissions are understood. Validate file content in the test rather than treating existence as sufficient.

Redact secrets from test logs and application logs. Grid infrastructure should never log credentials embedded in URLs or capabilities. Apply retention limits and access controls to every CI artifact, not only videos.

9. Observe Grid Health and Diagnose Failures

Use three views together: the client exception, Grid logs, and /status or Grid UI. A SessionNotCreatedException can mean no matching capability, browser launch failure, driver mismatch inside a custom image, or a queue timeout. A disconnected session can mean node loss, browser crash, network interruption, or a test that exceeded infrastructure limits.

A practical triage sequence is:

  1. Confirm Hub readiness and registered stereotypes.
  2. Compare requested browserName and custom capabilities with available slots.
  3. Inspect queue time and active sessions.
  4. Inspect the selected node log for browser startup.
  5. Check container CPU, memory, and shared memory.
  6. Confirm the application is reachable from the node network.
  7. Verify the client always closes sessions.

Increase log level temporarily through supported SE_OPTS settings, then return to a useful production level. Excessive logs can hide the relevant event and expose more internal data.

Selenium Grid integrates with tracing based on OpenTelemetry concepts. For a small Compose Grid, structured container logs and status metrics may be enough. At larger scale, export traces and metrics to the organization's observability platform with controlled sampling and retention.

Do not classify every remote timeout as a flaky test. Grid queue time, browser startup, navigation, and assertion waiting are separate intervals. Capture timestamps for each layer so the owner receives actionable evidence.

10. Secure, Upgrade, and Maintain the Grid

Never expose an unauthenticated Grid to the public internet. Browser nodes can reach internal systems and process arbitrary navigation commands. Place the Router behind network controls and an authenticated gateway, restrict source networks, and use a dedicated account or cluster with least privilege.

Do not give ordinary test containers the Docker socket. Dynamic Grid requires container creation privileges and must be isolated as an infrastructure service. Avoid privileged mode, broad host mounts, and writable access to unrelated artifacts.

Pin release images, subscribe to Selenium and Docker Selenium releases, scan images, and rebuild any internal derivatives. Upgrade Hub and nodes together in a staging Grid. Run session creation for every supported browser, downloads, alerts, windows, proxy paths, certificates, and any BiDi or CDP features your suite uses.

Use rolling replacement only when the topology and scheduler support safe draining. Stopping a node with active sessions will fail those tests. For a small CI-owned Grid, the clearer approach is to create a new project with new image tags, validate it, then remove the old project after sessions finish.

Review capacity and timeouts quarterly. A high session timeout delays recovery from abandoned clients, while a low timeout can kill legitimate long tests. Browser leftover cleanup is a safety net, not a substitute for driver teardown and job cleanup.

Interview Questions and Answers

Q: What happens when a Remote WebDriver session is created?

The request enters the Grid Router and New Session Queue. The Distributor matches its capabilities to a free node slot, and the Session Map records the node that owns it. Later commands are routed to that node until the client quits the session.

Q: Why is shm_size: 2gb commonly set on browser nodes?

Chrome and other browsers use shared memory for multiprocess work. A small container /dev/shm can contribute to crashes or tab failures. Two gigabytes is the official image example, but the team should measure its application and concurrency.

Q: How do you scale a Docker Selenium Grid?

Keep one session per adequately resourced node initially and add node replicas by browser capability. Then bound the runner to the useful slot count and monitor queue time, CPU, memory, and target capacity. Increasing sessions inside one node is a measured exception.

Q: Why must Grid port 4444 be protected?

A WebDriver endpoint can navigate browsers to internal sites, interact with applications, and execute powerful browser workflows. Public unauthenticated access can expose infrastructure and data. The Grid belongs behind network policy and authentication.

Q: What is the difference between a node timeout and proper teardown?

A node timeout eventually removes a stale session after inactivity. Proper teardown calls quit() immediately when the test finishes or fails, releasing the slot and browser resources. Timeout is a recovery mechanism, not normal lifecycle management.

Q: How do capabilities affect queueing?

The Distributor only assigns a request to a slot whose stereotype matches the required capabilities. Free nodes with a different browser or custom constraint do not help. Capability-specific queue metrics are therefore more useful than one total node count.

Q: When would you choose Standalone instead of Hub and Nodes?

Standalone is ideal for a local proof, one isolated CI job, or a simple single-browser need. Hub and Nodes is more appropriate when several clients share differentiated browser capacity. The simplest topology that meets the requirement is easier to secure and maintain.

Q: How do you diagnose SessionNotCreatedException in Grid?

I compare requested capabilities with registered slots, inspect queue and Hub logs, then inspect the selected node for browser startup errors. I also check image alignment, architecture, shared memory, resource limits, and network reachability. The client stack trace alone rarely identifies the infrastructure layer.

The interviewQnA field provides concise versions for practice. A strong interview answer should distinguish Grid capacity from runner parallelism and should include security, teardown, and observability.

Common Mistakes

  • Using latest for Hub and node images or mixing release tags.
  • Publishing Event Bus and Router ports broadly when only port 4444 is needed.
  • Treating a running Hub container as proof that browser nodes are registered.
  • Replacing readiness with a fixed sleep.
  • Omitting driver.quit() and waiting for the node timeout to recover slots.
  • Setting high SE_NODE_MAX_SESSIONS on an undersized container.
  • Launching more test threads than capability-specific Grid capacity.
  • Forgetting shm_size and misclassifying browser crashes as test flakiness.
  • Adding fixed container_name values to services that need Compose scaling.
  • Sharing download and video filenames across parallel nodes.
  • Mounting the Docker socket into an ordinary browser node.
  • Exposing an unauthenticated Grid outside the trusted network.
  • Upgrading browsers, clients, Hub, and nodes independently without compatibility tests.
  • Saving verbose logs and videos indefinitely without privacy controls.

Conclusion

Docker for Selenium Grid is dependable when the Grid is operated as browser infrastructure: matched and pinned images, private networking, real readiness, capability-aware capacity, guaranteed session teardown, bounded queues, and retained evidence. The runnable Hub and Node Compose setup is enough for a small shared Grid and a sound foundation for measured scaling.

Start the two-browser Grid, run the Python smoke test, watch the session appear and disappear, then scale Chrome to three replicas and repeat. That sequence proves registration, routing, teardown, and capacity before the Grid becomes a dependency for a larger CI portfolio.

Interview Questions and Answers

Describe the Selenium Grid new-session flow.

The Router accepts the W3C new-session request and places it in the New Session Queue. The Distributor finds a node slot matching the requested capabilities, and the Session Map records the owner. Later WebDriver commands are routed to that node until the session is quit.

Why should Hub and node image tags match?

Matching tags remove protocol and configuration differences as troubleshooting variables. The release images are tested as a family. I upgrade Hub and nodes together and run browser-specific acceptance tests before production use.

How would you scale a containerized Selenium Grid?

I would add node replicas by demanded browser capability while keeping one session per adequately resourced node initially. I would bound client concurrency to available slots and monitor queue time, CPU, memory, and application load. Only measured evidence would justify multiple sessions in one node.

What causes a new session to remain queued?

All matching slots may be busy, no registered stereotype may satisfy the capabilities, a node may have failed registration, or capacity may be unhealthy. I inspect status, requested capabilities, the queue, Hub logs, and node logs before increasing timeouts.

Why is driver.quit() essential with Grid?

quit() ends the remote session and releases the browser process and Grid slot. Closing a window does not guarantee that lifecycle. A session timeout is only a recovery mechanism for abandoned clients.

What security controls does Selenium Grid require?

I keep the Router off the public internet, restrict source networks, require authentication at the gateway, and use dedicated least-privilege infrastructure. I avoid Docker socket mounts and broad host volumes. Logs and recordings receive access and retention controls.

How do you distinguish Grid failure from test failure?

I correlate the client exception with Hub and node logs, the status snapshot, and resource data. Assertion evidence points toward product or test logic, while capability mismatch, browser startup, node loss, or queue timeout points toward Grid. Network reachability and application health can span both layers.

When is Standalone preferable to Hub and Nodes?

Standalone is preferable for a single isolated CI job, local evaluation, or a simple browser need because it has fewer moving parts. Hub and Nodes fits shared, capability-specific capacity. I choose the smallest topology that meets the availability and scaling requirement.

Frequently Asked Questions

What Docker image version should I use for Selenium Grid in 2026?

The examples use the official 4.45.0-20260606 Hub and browser node images. Pin the same full tag across Hub and nodes, verify it against the official release, and upgrade all components together.

What URL should Remote WebDriver use for a Docker Selenium Grid?

A test running on the Docker host normally uses http://localhost:4444. A test container on the same Compose network uses http://selenium-hub:4444; the /wd/hub suffix is not required for a standard Selenium 4 Grid.

How do I know when Selenium Grid is ready?

Use the included /opt/bin/check-grid.sh health command or poll the status endpoint until value.ready is true. Also confirm that the required browser node stereotypes and free slots are registered.

How many sessions should one Selenium Docker node run?

Start with one session per node, which is the official image default. Increase it only after measuring CPU, memory, browser stability, and application capacity, and understand the override setting required beyond available processors.

How do I scale Chrome nodes with Docker Compose?

Run docker compose up -d --scale chrome=3 and avoid a fixed container_name on the chrome service. The Grid registers each replica as a separate node and exposes its slots through the Hub.

Why do Selenium browser containers need shm_size?

Browsers use shared memory for their multiprocess architecture, and a small /dev/shm can cause crashes. The official examples use 2 GB as a starting value for each node, subject to measurement.

Is Selenium Grid safe to expose on the internet?

No. An unauthenticated Grid can drive browsers into internal applications and expose infrastructure or data. Put it behind a private network, firewall rules, and an authenticated gateway with least-privilege access.

Related Guides