August 1, 2026
How to Build a Selenium Grid with Docker Compose
Learn how to run Selenium Grid 4 with Docker Compose, wire up hub and browser nodes, configure test execution, and debug common local Grid failures.
If you need real browser execution without pushing every test run to a remote cloud, a local Selenium Grid is one of the most practical setups you can build. With Docker Compose, the grid becomes repeatable, versioned, and easy to tear down when you are done. That matters because a lot of Grid pain is not about Selenium itself, it is about inconsistent environments, browser/node drift, and the hidden cost of maintaining test infrastructure that only one person understands.
This tutorial walks through a working Selenium Grid Docker Compose setup, explains how the pieces fit together, and shows how to connect tests to it from code. The goal is not just to get containers running, but to set up something a team can actually debug when jobs fail.
What you are building
A modern Selenium Grid 4 setup usually includes three conceptual parts:
- a router or hub entry point for test sessions
- browser nodes that register themselves with the grid
- the tests that request a session from the grid and run inside a browser container
With Docker Compose, you describe those services in one file, then start the whole stack with a single command. For local development and CI validation, that is usually enough.
A useful rule: if your grid is hard to recreate from scratch, it will be hard to trust when tests start failing.
Before you build it, decide what problem you are solving. A local grid is good when you want:
- realistic browser execution on your workstation or CI runner
- reproducible browser versions
- parallel execution across a few browser sessions
- an easier path to debugging than a full remote browser cloud
It is not a substitute for disciplined test design. If your suite is already unstable because of bad waits, broken selectors, or environment coupling, adding containers will not magically fix it.
Prerequisites
You need:
- Docker installed
- Docker Compose available, either as
docker composeor the standalone Compose plugin - a Selenium client in your test framework, for example Python, Java, JavaScript, or C#
For official references, see the Selenium Grid documentation and the Docker Compose documentation.
A minimal Selenium Grid 4 Docker Compose file
The simplest useful Grid 4 deployment has a standalone router plus one or more browser nodes. Selenium provides official images, which is preferable to building your own browser containers unless you have a specific need for custom OS packages or fonts.
Here is a compact docker-compose.yml that starts a router and two node types, Chrome and Firefox:
services:
selenium-hub:
image: selenium/hub:4.23.1
container_name: selenium-hub
ports:
- "4444:4444"
environment:
- SE_SESSION_REQUEST_TIMEOUT=300
- SE_SESSION_RETRY_INTERVAL=1
chrome: image: selenium/node-chrome:4.23.1 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 - SE_NODE_MAX_SESSIONS=1 - SE_NODE_OVERRIDE_MAX_SESSIONS=true
firefox: image: selenium/node-firefox:4.23.1 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 - SE_NODE_MAX_SESSIONS=1 - SE_NODE_OVERRIDE_MAX_SESSIONS=true
Start it with:
docker compose up -d
Then open the Grid UI:
text http://localhost:4444
If everything is healthy, the Grid UI should show the connected nodes and any active sessions.
Why these settings matter
The SE_EVENT_BUS_* variables let the nodes talk to the hub. If those are wrong, the containers may start but the nodes never register, which looks like a network issue even though the root cause is usually a bad environment variable.
shm_size: 2gb matters more than people expect. Chrome in a container can crash or become unstable when /dev/shm is too small. A browser crash during a test can look like app flakiness, but it is often an infrastructure problem.
SE_NODE_MAX_SESSIONS=1 is a conservative default. It reduces resource contention and makes failures easier to reason about. If you increase session density, you increase throughput, but you also increase the chance that one browser job interferes with another through CPU, memory, or file descriptor pressure.
Verify that the Grid is healthy
A running container does not mean a healthy Grid. Check it explicitly.
Use the Selenium Grid status endpoint:
curl http://localhost:4444/status
A healthy response includes readiness and node information. You can also inspect the container logs:
docker compose logs -f selenium-hub
and:
docker compose logs -f chrome
Look for signs of registration, session allocation, or repeated reconnects.
Common failure modes at this stage:
- the hub is up, but nodes cannot register because the event bus host is wrong
- the browsers start, but tests hang because the node is overloaded
- Chrome crashes intermittently because shared memory is too small
- ports are already in use on the host machine
Connect a Selenium test to the local Grid
Once the Grid is up, point your test suite at http://localhost:4444/wd/hub or, for newer clients, the Grid root URL depending on your binding and version. Selenium 4 client libraries usually know how to talk to the Grid endpoint through a RemoteWebDriver.
Here is a Python example that opens a page through the local grid:
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
options = Options() options.add_argument(“–headless=new”)
driver = webdriver.Remote( command_executor=”http://localhost:4444/wd/hub”, options=options, )
try: driver.get(“https://example.com”) assert “Example Domain” in driver.title finally: driver.quit()
A few practical notes:
- headless mode is useful in CI, but do not assume it matches every browser behavior you see in a headed desktop session
- always call
quit()in afinallyblock, orphaned sessions make Grid debugging much harder - use explicit waits, not sleep calls, because container scheduling adds latency that fixed pauses handle poorly
If you are using JavaScript or TypeScript, the idea is the same, connect to the remote endpoint, create a browser session, and clean it up reliably.
Add a second browser and understand parallelism
One of the main reasons teams adopt Docker Compose Selenium Grid is to run tests in parallel on multiple browser types. That can improve feedback time, but only if the suite is structured for it.
A Grid with two node types is useful when you want to validate cross-browser behavior on the same run. That said, the grid itself is not your parallel test runner. Your framework still needs to split work across sessions.
For example, in CI you might run one job for Chrome and one for Firefox, both pointing to the same Compose-based Grid. That approach is often simpler than trying to orchestrate browser selection entirely inside a single test process.
Parallelism helps the most when your tests are isolated, deterministic, and independent. If the suite shares state, parallel execution usually accelerates the failures, not the value.
A practical Compose file for CI
For CI, you usually want three extra properties:
- deterministic startup and teardown
- browser logs visible in job output
- enough resource limits to avoid node starvation
Here is a slightly more operational example:
services:
selenium-hub:
image: selenium/hub:4.23.1
ports:
- "4444:4444"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:4444/status"]
interval: 10s
timeout: 5s
retries: 10
chrome: image: selenium/node-chrome:4.23.1 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_OVERRIDE_MAX_SESSIONS=true
The healthcheck helps the test job wait for the Grid to be ready instead of racing the containers. That race is a common source of intermittent failures in CI pipelines.
If your pipeline starts tests before nodes have registered, the failure often looks like a connection error or a session creation timeout. The actual problem is just startup ordering.
Debugging local Selenium Grid failures
When a local grid fails, the most useful evidence usually comes from the container logs, not from the test assertion.
Check these areas first:
1. Node registration
If a node does not appear in the Grid UI, inspect the node logs for connection attempts to the event bus. Mistyped environment variable names or wrong service names are frequent causes.
2. Shared memory and crashes
If Chrome closes unexpectedly, increase shm_size. Containerized browsers are sensitive to this.
3. Resource saturation
A laptop or CI runner with limited CPU and RAM can make browser startup look random. If test duration varies widely, resource contention may be the cause.
4. Network reachability
If the test code runs outside Docker but the Grid is in Docker, verify the endpoint is reachable from the test process. If the test code also runs inside Docker, localhost may refer to the wrong container, not the host machine.
5. Session leaks
If failed tests do not quit sessions, the Grid can fill up. The symptoms are new tests waiting indefinitely for capacity.
A good habit is to inspect the Grid status endpoint before and after a run, then compare the number of active sessions to the number of tests you expected.
Choosing browser images and versions
Using official Selenium images gives you a tested base, but you still need to control browser version drift. That is especially important if your application behavior depends on a specific browser release or if you are reproducing a bug in a single version.
When pinning versions, consider:
- pinning the Selenium image tag instead of using
latest - aligning Chrome and Firefox versions with what your CI environment supports
- documenting when you intentionally upgrade browser versions
The tradeoff is straightforward. Pinning improves reproducibility, but it also creates maintenance work when you need browser security updates or bug fixes.
When Docker Compose is enough, and when it is not
A local Selenium Grid with Docker Compose is a good fit when:
- one team owns the test stack
- you want reproducible browser runs on developer machines and CI
- you need modest parallelism
- you care about debugging infrastructure yourself
It starts to strain when:
- many repositories share the same browser infrastructure
- you need autoscaling, elastic capacity, or multi-region test execution
- browser management becomes more important than test authoring
- your team spends too much time operating containers instead of improving test coverage
At that point, the hidden cost is not just infrastructure, it is ownership concentration. Someone has to understand Compose files, browser images, node registration, logs, CI runners, and the failure modes that only appear under load.
If a team wants the results of grid-style browser execution without maintaining containers and browser nodes, a platform like Endtest, an agentic AI test automation platform, can be a simpler alternative because it abstracts the execution layer and provides maintained, human-readable test steps. That is not the right answer for every team, but it is worth evaluating if infrastructure maintenance has become the bottleneck.
A small CI example
This GitHub Actions job starts the grid, runs tests, and shuts everything down afterward:
name: browser-tests
on: push: pull_request:
jobs: selenium: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Start grid run: docker compose up -d - name: Wait for grid run: | for i in $(seq 1 30); do curl -sf http://localhost:4444/status && exit 0 sleep 2 done exit 1 - name: Run tests run: pytest -q - name: Tear down if: always() run: docker compose down -v
The important part is the wait loop. It prevents a race between container startup and test execution.
Operational habits that reduce flaky results
A Docker-based grid is only one layer of the system. The rest is test hygiene.
Focus on these practices:
- use explicit waits for elements and page states
- isolate test data, do not share mutable state across tests
- keep sessions short, terminate them deterministically
- collect browser logs and Grid logs as first-class artifacts
- version your Compose file alongside the test suite
If you are debugging a failing browser test, always ask which layer failed first: app code, test logic, browser runtime, or grid infrastructure. That mental separation saves a lot of time.
A simple decision checklist
Use a local Selenium Grid Docker Compose setup when your team needs:
- real browsers without a full remote service
- repeatable local reproduction of browser failures
- moderate parallelism for cross-browser validation
- direct control over browser node behavior
Reconsider it when the grid becomes a product in itself, with a backlog of upgrades, resource tuning, and failure triage that competes with actual test engineering.
Closing thoughts
A Selenium Grid Docker Compose setup is valuable because it turns browser execution into something you can version, inspect, and recreate. That makes it better than ad hoc local browser runs and easier to reason about than a pile of manual setup steps on each machine.
The trick is to treat it as infrastructure, not magic. Pin versions, watch the logs, validate readiness, and keep the test suite disciplined. If you do that, a local Selenium Grid can be a strong part of your automation stack. If you do not want to own that layer, there are simpler platforms that let you keep the browser execution benefits without taking on the container maintenance burden.