Home / QA & DevOps Engineering
Test Automation β€’ Container Architecture

Selenium Grid 4 Setup: Modern Docker Compose vs. Legacy --link 🌐

An enterprise engineering blueprint for building an elastic, distributed automated browser testing farm. Master Selenium Grid 4 distributed components, migrate away from deprecated container links, resolve Linux shared memory crashes, and scale nodes on demand.

Published by AHP β€’ cicdclouds.com

Selenium Grid 4 Distributed Docker Architecture

1. The Problem with Sequential Testing & Legacy Docker Links

Running extensive end-to-end regression suites sequentially against a single localized browser creates massive development friction. A test suite containing hundreds of scenarios can easily take hours to execute, stalling continuous delivery pipelines and delaying release feedback loops.

To parallelize test runs, quality engineering teams adopt Selenium Grid. Historically, teams launched standalone Hub and Node containers using the legacy Docker --link command:

# DEPRECATED & FRAGILE: Legacy Docker link pattern
docker run -d -p 4444:4444 --name selenium-hub selenium/hub:3.141.59
docker run -d --link selenium-hub:hub -v /dev/shm:/dev/shm selenium/node-chrome:3.141.59

Why the --link Flag Was Deprecated

  • Static /etc/hosts Mutation: The --link mechanism mutated the container's internal /etc/hosts file with the IP address of the target container at boot time. If the Hub container restarted or received a dynamic IP reassignment, the linked Node lost connection immediately.
  • Zero Dynamic Discovery: Links were unidirectional and strictly static, making horizontal autoscaling (e.g., spinning up 10 temporary Chrome nodes) impossible without manually chaining container flags.
  • Security Coupling: Linked containers shared ambient environment variables, inadvertently exposing database credentials and access tokens between unrelated processes.

Modern Standard: Today, the official standard relies on Docker Compose and User-Defined Bridge Networks, where an embedded DNS server handles dynamic service discovery, automatic alias resolution, and isolated subnet routing without hardcoded host entries.

2. Understanding Selenium Grid 4 Distributed Topology

Unlike Selenium Grid 3, which utilized a monolithic Hub architecture, Selenium Grid 4 was redesigned from the ground up as a distributed, microservice-based event system:

[Client Test Suite: Python / Java / WebdriverIO]
                       β”‚ (HTTP JSON Wire / W3C Protocol)
                       β–Ό
            β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
            β”‚     Router (4444)    β”‚
            β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                       β”‚
       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
       β–Ό               β–Ό               β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Distributor β”‚β”‚ Session Map  β”‚β”‚Session Queue β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚
       β–Ό (Event Bus: 4442/4443)
 ═══════════════════════════════════════════════
       β”‚                       β”‚
       β–Ό                       β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Chrome Node  β”‚        β”‚ Firefox Node β”‚
β”‚ (Worker)     β”‚        β”‚ (Worker)     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
  • Router: The entry point of the Grid listening on port 4444. It inspects incoming HTTP requests, differentiates new session handshakes from active commands, and redirects traffic accordingly.
  • Distributor: Queries the Session Queue for pending requests and assigns available slots across registered browser nodes based on requested browser capabilities.
  • Session Map: A high-performance in-memory lookup table mapping active Session IDs to the specific physical node IP hosting the browser container.
  • Session Queue: Buffers incoming test requests when all browser slots are occupied, preventing socket rejection until nodes become free.
  • Event Bus: Powered by zero-dependency asynchronous messaging (typically Netty) listening on ports 4442 (Publish) and 4443 (Subscribe) to orchestrate internal node heartbeats.

3. The Infamous Browser Crash: Mastering shm_size

The most prevalent failure encountered when deploying headless Chrome or Firefox in Docker is an instantaneous crash during rendering:

org.openqa.selenium.WebDriverException: unknown error: session deleted because of page crash
from unknown error: cannot determine loading status
from tab crashed (Session info: chrome=128.0.6613.119)

Root Cause: The 64 MB Default /dev/shm Limit

Modern web browsers are multi-process applications. Chrome utilizes POSIX Shared Memory (/dev/shm) for inter-process communication (IPC) between the main browser process, rendering engines, and GPU compositors.

By default, Docker allocates an ultra-restrictive 64 MB partition to /dev/shm for every container. When modern, JavaScript-heavy single-page applications (React, Angular, complex DOMs) render in headless mode, the browser immediately exhausts the 64 MB partition, triggering an internal memory allocation abort and crashing the tab.

The Architectural Solution

In production Docker configurations, you must override this limit by specifying shm_size: 2gb in your Compose definition, or by mounting the host system's shared memory directly via volumes: - /dev/shm:/dev/shm.

4. Production-Ready Docker Compose Blueprint

Save the following specification as docker-compose.yml. It provisions an elastic, health-checked Selenium Grid 4 cluster containing an isolated Hub, scalable Chrome and Firefox worker nodes, container resource boundaries, and dynamic bridge networking:

name: enterprise-selenium-grid

networks:
  grid-net:
    driver: bridge
    ipam:
      config:
        - subnet: 172.28.0.0/16

services:
  selenium-hub:
    image: selenium/hub:4.25.0
    container_name: selenium-hub
    restart: unless-stopped
    ports:
      - "4442:4442" # Event Bus Publish
      - "4443:4443" # Event Bus Subscribe
      - "4444:4444" # Web Console & API Gateway
    environment:
      - GRID_MAX_SESSION=16
      - GRID_SESSION_TIMEOUT=300
      - GRID_CLEAN_UP_CYCLE=5000
    deploy:
      resources:
        limits:
          cpus: '2.0'
          memory: 2048M
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:4444/wd/hub/status"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - grid-net

  chrome-node:
    image: selenium/node-chrome:4.25.0
    shm_size: 2gb
    restart: unless-stopped
    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=4
      - SE_NODE_SESSION_TIMEOUT=300
      - VNC_NO_PASSWORD=1
    deploy:
      resources:
        limits:
          cpus: '2.0'
          memory: 2560M
    networks:
      - grid-net

  firefox-node:
    image: selenium/node-firefox:4.25.0
    shm_size: 2gb
    restart: unless-stopped
    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=4
      - SE_NODE_SESSION_TIMEOUT=300
    deploy:
      resources:
        limits:
          cpus: '2.0'
          memory: 2560M
    networks:
      - grid-net

5. Launching, Dynamic Scaling, and Test Execution

Cluster Lifecycle Commands

# 1. Launch the base infrastructure in detached mode
docker compose up -d

# 2. Scale Chrome worker capacity to 4 containers (16 concurrent browser threads)
docker compose up -d --scale chrome-node=4

# 3. Verify real-time cluster readiness via Grid HTTP status API
curl -s http://localhost:4444/status | jq '.value.ready'

# 4. Monitor active session counts across registered nodes
curl -s http://localhost:4444/status | jq '.value.nodes[].stereotypes'

Python Test Runner Example (Concurrent Execution)

Configure your test client to point directly to the central Router endpoint. The Grid automatically matches requested capabilities to an available container slot:

from selenium import webdriver
from selenium.webdriver.chrome.options import Options

def run_remote_test():
    # Define browser capabilities
    options = Options()
    options.add_argument("--headless=new")
    options.add_argument("--disable-gpu")
    options.add_argument("--window-size=1920,1080")
    
    # Optional enterprise metadata
    options.set_capability("se:recordVideo", True)
    options.set_capability("se:timeZone", "Asia/Kolkata")

    print("Connecting to central Selenium Grid Router...")
    driver = webdriver.Remote(
        command_executor="http://localhost:4444",
        options=options
    )

    try:
        driver.get("https://cicdclouds.com")
        print(f"Executed on Node successfully. Page Title: {driver.title}")
        assert "cicdclouds" in driver.title.lower()
    finally:
        # Crucial: Always quit session to release node slot back to the Distributor
        driver.quit()

if __name__ == "__main__":
    run_remote_test()

6. Production Troubleshooting & Operational Pitfalls

Operating containerized Selenium farms at high concurrency reveals several critical failure modes:

1. Orphaned Browser Sessions Blocking the Grid

If a developer's test script crashes unexpectedly or omits driver.quit() inside a finally block, the browser instance remains active indefinitely inside the container, holding a slot in the Session Map and starving the queue.

Remedy: Always enforce server-side session timeouts by setting SE_NODE_SESSION_TIMEOUT=300 (5 minutes) and GRID_SESSION_TIMEOUT=300 in the Hub/Node environment variables. The Grid will automatically harvest and terminate zombie browser processes.

2. Session Queue Rejection (HTTP 500 / Timeout)

When executing massive parallel test runs (e.g., 50 tests launched concurrently against 16 browser slots), client tests fail with:

org.openqa.selenium.SessionNotCreatedException: Could not start a new session.
Unable to execute request for an existing session: Timeout while waiting for slot to be free

Remedy: Increase the client HTTP connection timeout in your language binding from default (60s) to 180s, and scale the node containers horizontally:

docker compose up -d --scale chrome-node=8

3. Live Test Debugging via Embedded VNC

When running automated tests in CI/CD, debugging visual glitches or timing issues can be difficult without visual feedback. Official Selenium node images bundle an integrated noVNC web server.

If you expose port 7900 on an individual node container, you can navigate to http://localhost:7900 in any standard web browser to view the live desktop session and inspect the automated browser interactions in real time.