QA How-To
Configure Selenium Grid Dynamic Docker Nodes
Follow this selenium grid dynamic node docker tutorial to launch isolated Chrome and Firefox containers on demand, verify sessions, and fix failures.
23 min read | 2,758 words
TL;DR
Run Selenium Hub beside an official node-docker container, give node-docker access to a dedicated Docker daemon, and map browser stereotypes to pinned standalone images in docker.toml. Each matching WebDriver request then creates an isolated browser container and removes it after quit().
Key Takeaways
- Use the official node-docker image as a dynamic node that asks the Docker daemon to create browser containers on demand.
- Map each WebDriver capability stereotype to a pinned standalone browser image in docker.toml.
- Mount the Docker socket only on a dedicated, trusted runner because it grants powerful control over the host.
- Set the session container network explicitly so disposable browsers can reach both Grid and the application under test.
- Verify container creation, browser matching, cleanup, concurrency, and failure artifacts as separate acceptance checks.
- Treat image versions, capacity limits, cleanup, observability, and host resources as production controls.
A selenium grid dynamic node docker tutorial should get you from an empty directory to browser containers that appear only when a test requests them. You will run Selenium Hub and selenium/node-docker, map Chrome and Firefox capabilities to pinned images, execute tests, and prove that each disposable container is removed.
Use the Selenium Grid cloud scaling complete guide if you first need to compare dynamic Docker nodes with static Compose replicas, Kubernetes, or a managed grid. This tutorial focuses on one job: configure a trusted Docker host to create one isolated browser container for each accepted session.
The example pins the official Selenium 4.45.0-20260606 release across Hub, node, and browser images. Keep every Selenium image on the same tested release tag, and run all verification checks again whenever you upgrade.
What You Will Build
You will build a small Dynamic Grid with these parts:
- A Selenium Hub that accepts Remote WebDriver requests on port 4444.
- One
selenium/node-dockerservice that registers browser stereotypes with the Hub. - A
docker.tomlmapping for Chrome and Firefox standalone images. - A shared Docker network for the Hub, dynamic node, session containers, and optional test runner.
- Python tests that prove browser matching, isolation, parallel capacity, and cleanup.
- An assets directory for logs and optional recordings from disposable sessions.
The dynamic node is not itself the browser. It receives a matching request, calls the Docker daemon, and creates a standalone browser container. That container registers as temporary capacity, runs the session, and exits after the session ends. This design gives stronger isolation than packing many sessions into a permanent browser node.
Prerequisites
Install Docker Engine 24 or newer with the Compose v2 plugin. Docker Desktop is suitable for a local lab. On Linux CI, use a dedicated runner rather than sharing the daemon with unrelated workloads. You also need Python 3.11 or newer and curl.
Check the tools:
docker version
docker compose version
python3 --version
curl --version
Allocate at least 4 CPU cores and 8 GB of memory for a two-browser exercise. Actual needs depend on your application and suite, so measure before increasing concurrency. Pull the pinned images before starting:
export SELENIUM_VERSION=4.45.0-20260606
docker pull selenium/hub:${SELENIUM_VERSION}
docker pull selenium/node-docker:${SELENIUM_VERSION}
docker pull selenium/standalone-chrome:${SELENIUM_VERSION}
docker pull selenium/standalone-firefox:${SELENIUM_VERSION}
The tutorial mounts /var/run/docker.sock. Anyone who can control that socket can effectively control the Docker host. Do not expose an unauthenticated Docker TCP endpoint, do not use a shared developer machine for untrusted tests, and do not treat a container boundary as a security boundary.
Verification: All four commands must report versions, and docker image inspect selenium/node-docker:4.45.0-20260606 must exit successfully.
Step 1: Create the Project and Docker Network
Create an isolated directory and an assets folder. The session containers write artifacts under assets, while Compose resources use a predictable project name.
mkdir -p selenium-dynamic-grid/assets
cd selenium-dynamic-grid
export COMPOSE_PROJECT_NAME=selenium-dynamic-grid
docker network create selenium-grid || true
The explicit selenium-grid network matters because node-docker creates containers through the daemon, outside Compose's normal service lifecycle. Naming the network lets docker.toml attach every disposable browser to the same network as the Hub. It also makes connectivity predictable if your tests target another container on that network.
Do not point a browser at localhost when the application runs on the host. Inside a session container, localhost means that browser container. On Docker Desktop, use host.docker.internal. On Linux, run the application on the shared network or add an explicit host-gateway mapping after reviewing your environment.
Verification: Run docker network inspect selenium-grid --format '{{.Name}} {{.Driver}}'. Expected output is selenium-grid bridge. Run pwd and confirm the current directory ends in selenium-dynamic-grid.
Step 2: Map Browser Stereotypes in docker.toml
Create docker.toml with a browser image followed by its JSON stereotype. The capability JSON is what Grid matches against a new session request.
[docker]
configs = [
"selenium/standalone-chrome:4.45.0-20260606", "{\"browserName\": \"chrome\", \"platformName\": \"linux\"}",
"selenium/standalone-firefox:4.45.0-20260606", "{\"browserName\": \"firefox\", \"platformName\": \"linux\"}"
]
url = "http://127.0.0.1:2375"
video-image = "selenium/video:ffmpeg-8.1-20260606"
assets-path = "/opt/selenium/assets"
[server]
port = 5555
[node]
max-sessions = 2
override-max-sessions = false
detect-drivers = false
[relay]
[events]
publish = "tcp://selenium-hub:4442"
subscribe = "tcp://selenium-hub:4443"
The node-docker startup scripts bridge the mounted Unix socket for the Docker client address in this official configuration pattern. Keep max-sessions within measured host capacity. It limits simultaneous requests handled by this dynamic node, while each created standalone container still runs one session by default.
The video-image entry only selects the image used when video recording is requested through supported Selenium capabilities. Pull and validate that image before enabling video, or remove the line if you do not use recordings. The core Chrome and Firefox flow does not require a video container.
Keep stereotypes minimal. A request with browserName=chrome and platformName=linux can match the Chrome entry. If you add custom capabilities, use namespaced keys such as se:recordVideo; do not invent unprefixed W3C capabilities.
Verification: Run grep -n 'standalone-' docker.toml. It must show one Chrome and one Firefox mapping, both on 4.45.0-20260606. Also confirm the image names locally with docker image inspect selenium/standalone-chrome:4.45.0-20260606 selenium/standalone-firefox:4.45.0-20260606.
Step 3: Define Hub and Dynamic Node with Compose
Create compose.yaml. The external network name must match Step 1, and the config path inside node-docker must be /opt/selenium/docker.toml.
services:
selenium-hub:
image: selenium/hub:4.45.0-20260606
container_name: selenium-hub
ports:
- "4442:4442"
- "4443:4443"
- "4444:4444"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:4444/status"]
interval: 5s
timeout: 3s
retries: 20
networks:
- grid
node-docker:
image: selenium/node-docker:4.45.0-20260606
container_name: selenium-node-docker
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: "2"
SE_NODE_OVERRIDE_MAX_SESSIONS: "false"
volumes:
- ./docker.toml:/opt/selenium/docker.toml:ro
- ./assets:/opt/selenium/assets
- /var/run/docker.sock:/var/run/docker.sock
networks:
- grid
networks:
grid:
external: true
name: selenium-grid
The event bus lets node-docker register with Hub. The Docker socket lets it create and remove session containers. The assets bind mount persists diagnostics beyond the temporary container lifetime. The read-only TOML mount prevents accidental config changes from inside the service.
On SELinux hosts, bind mounts can require an approved label strategy. Do not blindly disable SELinux or make the socket broadly writable. Prefer a dedicated host, minimal membership in the Docker group, and a runner lifecycle that removes all test resources afterward.
Verification: Run docker compose config --quiet. It must return no output and exit with code 0. Then run docker compose config --images; the output must include Hub and node-docker on the same Selenium version.
Step 4: Start the Selenium Grid Dynamic Node Docker Tutorial Stack
Start the two persistent services, then inspect their health and logs.
docker compose up -d
docker compose ps
docker compose logs --no-color node-docker
curl -fsS http://localhost:4444/status
The Grid status response should report ready as true. The console at http://localhost:4444/ui should show Chrome and Firefox stereotypes offered by the dynamic node. At this point, docker ps should show Hub and node-docker, but no standalone-chrome or standalone-firefox session container. That absence is expected. Dynamic capacity exists as configuration until a request arrives.
If Grid displays no stereotypes, read node-docker logs before changing configuration. Registration failures usually come from event bus DNS, malformed TOML, a socket permission problem, or an image mapping that does not parse. Keep the Hub name selenium-hub consistent in Compose and configuration.
Use the status API as the automation check and the UI as a human diagnostic. CI should not scrape the visual console. A successful HTTP response alone is not enough, so the next step will request an actual browser.
Verification: curl -fsS http://localhost:4444/status | grep -q '"ready": true' must succeed. Run docker ps --format '{{.Names}}'; it should list selenium-hub and selenium-node-docker, with no standalone browser container before a session starts.
Step 5: Run a Chrome Session and Prove Cleanup
Create a virtual environment, install Selenium, and save a complete smoke test as test_dynamic_grid.py.
python3 -m venv .venv
. .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install 'selenium>=4.29,<5'
from __future__ import annotations
import time
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
GRID_URL = "http://localhost:4444"
options = Options()
options.set_capability("platformName", "linux")
options.add_argument("--headless=new")
driver = webdriver.Remote(command_executor=GRID_URL, options=options)
try:
print(f"session={driver.session_id}")
driver.get("data:text/html,<title>Dynamic Grid Works</title><h1>OK</h1>")
assert driver.title == "Dynamic Grid Works"
print("chrome assertion: passed")
time.sleep(15)
finally:
driver.quit()
print("session closed")
Run python test_dynamic_grid.py. During the 15-second pause, use another terminal:
docker ps --format 'table {{.ID}}\t{{.Image}}\t{{.Names}}'
You should see a temporary selenium/standalone-chrome:4.45.0-20260606 container. After quit(), repeat the command until that container disappears. Always put quit() in finally; an assertion failure must not leak a session and occupy dynamic capacity.
The data: URL makes the first proof independent of DNS and the application under test. Once this passes, replace it with a URL reachable from the session container, not merely from the Python process.
Verification: The test must print chrome assertion: passed and session closed. A standalone Chrome container must exist during the pause and disappear shortly after the session closes.
Step 6: Verify Firefox Matching and Network Reachability
Use one parameterized script to request Firefox and open a real service on the shared network. Start a tiny web server container:
docker run -d --rm --name grid-demo-app --network selenium-grid \
nginx:1.27-alpine
Save test_firefox.py:
from selenium import webdriver
from selenium.webdriver.firefox.options import Options
options = Options()
options.set_capability("platformName", "linux")
options.add_argument("-headless")
driver = webdriver.Remote(
command_executor="http://localhost:4444",
options=options,
)
try:
driver.get("http://grid-demo-app")
assert "Welcome to nginx" in driver.title
assert driver.capabilities["browserName"] == "firefox"
print("firefox network test: passed")
finally:
driver.quit()
Run python test_firefox.py. Browser options automatically request browserName=firefox; the explicit Linux platform matches your TOML stereotype. The app name resolves because both the disposable browser and nginx are attached to selenium-grid.
A network test catches a frequent false success: the Grid can start browsers, but those browsers cannot reach the system under test. For a Compose application, attach its service to the external network and use its service or network alias. Avoid publishing a database or internal test endpoint solely to make browser routing convenient.
Verification: The script must print firefox network test: passed. While it runs, docker ps --filter ancestor=selenium/standalone-firefox:4.45.0-20260606 must show the temporary Firefox container. Remove the demo app with docker stop grid-demo-app.
Step 7: Test Two Concurrent Dynamic Sessions
Capacity claims need a concurrency test. Save test_parallel.py and request one Chrome and one Firefox session at the same time.
from concurrent.futures import ThreadPoolExecutor
from selenium import webdriver
from selenium.webdriver.chrome.options import Options as ChromeOptions
from selenium.webdriver.firefox.options import Options as FirefoxOptions
GRID_URL = "http://localhost:4444"
def run(browser: str) -> str:
options = ChromeOptions() if browser == "chrome" else FirefoxOptions()
options.set_capability("platformName", "linux")
if browser == "chrome":
options.add_argument("--headless=new")
else:
options.add_argument("-headless")
driver = webdriver.Remote(command_executor=GRID_URL, options=options)
try:
driver.get(f"data:text/html,<title>{browser} passed</title>")
assert driver.title == f"{browser} passed"
return f"{browser}: {driver.session_id}"
finally:
driver.quit()
with ThreadPoolExecutor(max_workers=2) as pool:
results = list(pool.map(run, ["chrome", "firefox"]))
for result in results:
print(result)
Run python test_parallel.py and watch docker stats in another terminal. Two dynamic session containers can run because both the TOML node limit and SE_NODE_MAX_SESSIONS allow two. Keep those settings aligned so configuration is obvious.
| Approach | Isolation | Idle resource use | Startup behavior | Best fit |
|---|---|---|---|---|
| Static browser nodes | Shared by configured slots | Higher | Browser node is already running | Stable, frequent suites |
| Dynamic Docker sessions | One disposable container per session | Lower | Includes container startup | Bursty trusted CI on one host |
| Kubernetes browser pods | Usually pod-level | Controlled by replicas | Includes scheduling and image startup | Multi-host elastic platforms |
| Managed grid | Provider-defined | No local browser fleet | Provider-controlled | Teams avoiding Grid operations |
Dynamic does not mean unlimited. The host remains the hard boundary for CPU, memory, shared memory, storage, and image pulls. Set a measured maximum and let excess requests queue instead of destabilizing every active test.
Verification: The script must print one Chrome and one Firefox session ID and exit with code 0. Afterward, run docker ps --filter ancestor=selenium/standalone-chrome:4.45.0-20260606 -q and docker ps --filter ancestor=selenium/standalone-firefox:4.45.0-20260606 -q separately. Both commands must eventually print nothing.
Step 8: Add Artifacts, Logs, and Safe Teardown
Capture enough context to connect a failed test with its Grid and container evidence. At minimum, log the WebDriver session ID, browser name, start time, test name, and Grid URL. Preserve files from assets under the CI job ID, then expire them according to your retention policy.
Inspect the running platform with these commands:
curl -fsS http://localhost:4444/status
docker compose logs --no-color --since=10m selenium-hub node-docker
find assets -maxdepth 3 -type f -print
docker system df
For visual evidence, follow the Selenium Grid video recording for failed sessions tutorial. Enable recording selectively and verify the exact video image tag. Video can consume meaningful disk and I/O, so upload useful artifacts and delete local copies rather than retaining everything on the runner.
Use Compose teardown for the persistent services. Session containers should already be gone because every test called quit().
docker compose down --remove-orphans
docker network rm selenium-grid
Do not add docker system prune -a to a shared runner. It can remove unrelated images, caches, networks, and stopped containers. On a dedicated ephemeral runner, destroy the runner itself or prune only resources carrying your controlled labels after confirming no job is active.
Verification: Before teardown, find assets should be readable by the CI user and logs should contain the test window. After teardown, docker ps --filter name=selenium- must show no tutorial containers, and docker network inspect selenium-grid should fail because the network was removed.
Troubleshooting
Node-docker reports permission denied for the Docker socket -> Confirm the host socket exists and the container received the bind mount. Use a dedicated runner with an approved Docker access model. Do not solve the problem by making the socket world-writable.
Grid is ready but a session stays queued -> Compare requested browserName and platformName with the exact TOML stereotype. Inspect node-docker logs for parsing or image errors. A request for an unmapped browser, platform, or vendor capability cannot match.
The browser image cannot be pulled -> Run docker pull for the exact mapped tag from the host and verify registry access, credentials, architecture, and disk space. Pin one tested tag across Hub, node-docker, and standalone browsers.
The session starts but cannot reach the application -> Inspect the disposable container's networks during a paused test. Put the application and browser on selenium-grid, use a resolvable service name, and remember that browser-container localhost is not the host.
Session containers remain after tests -> Ensure every driver reaches quit() through finally, even on assertion and setup failures. Inspect Grid and node-docker logs before removing an orphan so you can diagnose whether cleanup failed or the client leaked the session.
Browsers crash or Grid becomes unstable under parallel load -> Reduce max-sessions, inspect container exit codes and host memory, and monitor CPU, memory, disk, and shared memory. More configured slots do not create physical capacity.
Common Mistakes and Best Practices
- Do pin identical Selenium versions across control-plane and browser images. Do not use
latestin a reproducible pipeline. - Do run dynamic Grid on a dedicated trusted Docker host. Do not expose the daemon to untrusted tests or public TCP access.
- Do map only capabilities that your suite intentionally supports. Do not add arbitrary stereotypes to make a malformed request pass.
- Do attach disposable browsers to an explicit network. Do not assume they inherit Compose networking automatically.
- Do call
quit()infinallyand verify cleanup. Do not depend solely on timeouts to release capacity. - Do cap concurrency from measured CPU and memory data. Do not equate on-demand containers with infinite scaling.
- Do preserve session-correlated logs and selective artifacts. Do not retain recordings without an owner and expiry.
- Do test upgrades with Chrome, Firefox, queueing, cleanup, and networking. Do not update only one Selenium image.
Interview Questions and Answers
Q: What is a dynamic Docker node in Selenium Grid?
It is a Grid node that creates a disposable standalone browser container when a matching session request arrives. The temporary container runs the WebDriver session and is removed after the session ends. The node advertises capability stereotypes defined in docker.toml.
Q: Why does node-docker need access to the Docker daemon?
It must pull or locate browser images, create containers, attach networks and mounts, inspect their state, and remove them. Docker daemon access is highly privileged, so the Grid belongs on a dedicated trusted host with restricted access.
Q: How does Grid choose Chrome or Firefox?
The client options create W3C capabilities such as browserName and platformName. Grid matches that request to a stereotype in docker.toml, then node-docker launches the image paired with that stereotype. An unmatched request waits or fails according to Grid timeouts.
Q: What limits concurrency in a Dynamic Grid?
The dynamic node's maximum sessions is the configured logical limit. Docker-host CPU, memory, disk, image-pull bandwidth, and browser shared memory are the physical limits. Production capacity should use the smaller safe value established by load tests.
Q: How do you prevent leaked browser containers?
Put driver.quit() in a finally block, bound client and Grid timeouts, and monitor session containers after test completion. Correlate any orphan with its session ID and logs before controlled cleanup. Ephemeral dedicated runners provide the strongest final cleanup boundary.
Q: When would you choose Kubernetes instead?
Choose Kubernetes when you need multi-host scheduling, pod autoscaling, stronger workload placement controls, or integration with a cluster platform. Dynamic Docker is simpler for a trusted single-host CI runner, but its scaling ceiling is that host.
Where To Go Next
You now have a working Dynamic Grid that launches Chrome and Firefox on demand, connects browsers to a test network, handles two sessions concurrently, and cleans up afterward. Return to the complete Selenium Grid cloud scaling guide to decide how this single-host pattern fits your wider execution platform.
If you need multi-host elasticity, build the Selenium Grid Kubernetes autoscaling tutorial. Add the Selenium Grid OpenTelemetry monitoring setup before tuning concurrency so you can separate queue delay, container startup, browser startup, and test runtime. Add video recording for failed Selenium sessions when screenshots and logs do not explain intermittent UI failures.
Conclusion
This selenium grid dynamic node docker tutorial configures one official node-docker service to translate Grid capability requests into short-lived Chrome or Firefox containers. The essential controls are accurate stereotypes, pinned compatible images, an explicit network, bounded concurrency, guaranteed quit(), and a dedicated trusted Docker host.
Do not stop at seeing Grid's green status. Prove a container appears during each requested browser session, reaches the application, runs concurrently within the limit, produces correlated evidence, and disappears afterward. Those checks turn a clever Docker configuration into a dependable CI service.
Interview Questions and Answers
What is the difference between a static Selenium node and a dynamic Docker node?
A static node is a long-running browser service with a fixed number of slots. A dynamic Docker node advertises configured stereotypes and creates a disposable standalone browser container for each accepted session. Dynamic nodes reduce idle browser containers and improve session isolation, but add startup latency and require privileged Docker access.
How does node-docker select a browser image?
The Remote WebDriver client sends W3C capabilities. Grid matches them to a stereotype in docker.toml, and node-docker launches the paired image. Browser name, platform, and any namespaced custom capability must be compatible with that mapping.
What are the main security risks of Selenium Dynamic Grid?
The largest risk is Docker daemon access, which can provide control over the host. Untrusted test code is also executed inside browsers with access to configured networks. Use dedicated ephemeral runners, restrict test submitters and network reachability, pin images, and avoid public daemon endpoints.
How would you diagnose a session that remains queued?
First compare the requested capabilities with the advertised stereotypes. Then inspect node-docker and Hub logs for TOML errors, image pull failures, socket permissions, and capacity exhaustion. Finally check host resources and confirm the requested browser image exists for the host architecture.
How do you verify that dynamic session cleanup works?
Record the session ID and observe the standalone browser container while the test is active. Call quit() in a finally block, then assert that the corresponding container disappears within the expected cleanup period. Treat remaining containers as either leaked sessions or infrastructure cleanup failures and preserve logs before removal.
How do you capacity-plan a Selenium Dynamic Grid?
Measure representative browser sessions for peak CPU, memory, shared memory, startup time, and disk I/O. Set concurrency below the host's safe measured capacity and include headroom for Hub, node-docker, the operating system, and bursts. Validate queue time and stability under sustained load, not only a short smoke test.
When should a team move from Dynamic Docker Grid to Kubernetes?
Move when one Docker host is an availability or capacity bottleneck and the team can operate a cluster. Kubernetes adds multi-host scheduling, placement policies, autoscaling integrations, and pod-level lifecycle controls. It also adds operational complexity, so a trusted single-host Dynamic Grid can remain the better fit for smaller workloads.
Frequently Asked Questions
What is a Selenium Grid dynamic Docker node?
A dynamic Docker node creates a disposable standalone browser container for a matching WebDriver session. The container runs that session in isolation and is removed after the client calls quit().
Does Selenium Dynamic Grid require Kubernetes?
No. The official node-docker pattern works with a Docker daemon and Selenium Hub on one trusted host. Kubernetes is a separate choice for multi-host scheduling and autoscaling.
Why is docker.toml required for node-docker?
The file maps W3C capability stereotypes, such as Chrome on Linux, to concrete standalone browser images. It also carries Docker connection, network, asset, and node settings used to create session containers.
Is mounting the Docker socket into node-docker safe?
Docker socket access is highly privileged and should not be considered ordinary container isolation. Use a dedicated trusted runner, restrict who can submit tests, and never expose an unauthenticated Docker API publicly.
Why can the dynamic browser not reach localhost?
The browser runs inside its own container, where localhost refers to that container. Attach the application and browser to a shared network and use a resolvable service name, or use an explicitly configured host address.
How many Selenium browser containers can run at once?
The configured node maximum sets the logical limit, but the Docker host sets the real CPU, memory, disk, and shared-memory limit. Establish a safe value with representative parallel tests and leave capacity for Hub and the operating system.
How do I upgrade images in a Selenium Dynamic Grid?
Choose one tested Selenium tag and update Hub, node-docker, and every standalone browser mapping together. Pull the images in staging, then rerun browser matching, network, concurrency, artifact, and cleanup checks before production.
Related Guides
- Monitor Selenium Grid with OpenTelemetry: Selenium Grid OpenTelemetry Monitoring Setup
- Autoscale Selenium Grid on Kubernetes Step by Step
- Docker for Selenium Grid: Step by Step (2026)
- How to Use Selenium By xpath dynamic in Java (2026)
- How to Use Selenium DevTools in Selenium 4 in Java (2026)
- Record Videos for Failed Selenium Grid Sessions