Resource library

QA How-To

Docker vs Kubernetes for Selenium Grid (2026)

Compare Docker vs Kubernetes for Selenium Grid in 2026, with runnable setups, scaling trade-offs, costs, observability, and a clear choice guide for QA teams.

19 min read | 2,684 words

TL;DR

Docker Compose is the practical default for a small or predictable Selenium Grid on one machine. Kubernetes earns its complexity when the Grid is a shared service that needs multi-node capacity, automatic recovery, controlled rollouts, and demand-based scaling.

Key Takeaways

  • Use Docker Compose when one host can supply the required browser capacity and a small team owns the Grid.
  • Use Kubernetes when concurrent demand changes sharply, node failure recovery matters, or multiple teams share the service.
  • Treat Selenium session slots, not pod count, as the capacity unit that determines useful throughput.
  • Apply CPU and memory requests before enabling Kubernetes autoscaling, or the autoscaler will make poor decisions.
  • Keep browser capacity bounded in both platforms so test bursts cannot exhaust the host or cluster.
  • Measure queue time, session creation failures, test duration, and infrastructure cost per completed test before migrating.

Docker vs Kubernetes for Selenium Grid is primarily a decision about operating scale, not browser automation capability. Both run the same Selenium server and browser images. Choose Docker Compose for a compact Grid with predictable concurrency on one host; choose Kubernetes when you need multi-node scheduling, resilient shared infrastructure, policy controls, or automated capacity changes.

The wrong question is which platform sounds more modern. The useful question is which failure modes and operational workload your team can justify. This guide builds a working example on each platform, verifies every deployment, and gives you measurable criteria for choosing without turning a test runner into an infrastructure project.

TL;DR

Decision area Docker Compose Kubernetes
Best fit One team, one host, stable demand Several teams, multiple worker nodes, variable demand
Initial setup One YAML file and Docker Engine Cluster, manifests, storage, ingress or port access, and policies
Scaling Manual replica count, limited by one host Replica changes across nodes, with optional autoscaling
Recovery Container restart on the same host Pod rescheduling when a pod or worker node fails
Isolation Container limits and a dedicated host Namespaces, quotas, requests, limits, affinity, and policies
Upgrades Simple but usually coordinated manually Declarative rolling updates and rollback history
Cost profile Low control-plane overhead Higher baseline and engineering overhead
Recommended default Start here Adopt when defined requirements exceed one host

A sensible path is to prove test behavior with Docker for Selenium Grid, collect queue and resource measurements, and migrate only when the evidence supports a cluster. Kubernetes does not make Selenium tests faster by itself. It gives the Grid scheduler a larger, more resilient pool of infrastructure to run on.

1. Docker vs Kubernetes for Selenium Grid: What Actually Changes

Selenium Grid 4 separates routing, session queuing, distribution, and browser execution. In a small deployment, the official standalone image can provide all those functions. In a distributed deployment, a Hub coordinates browser Nodes. Docker and Kubernetes package and place these processes differently, but RemoteWebDriver still sends W3C WebDriver requests to a Grid URL. Your test code should not care which scheduler placed Chrome.

Docker Compose describes containers on one Docker Engine. It creates a network, starts services in dependency order, applies local resource settings, and can create multiple replicas. This makes the deployment easy to read and reproduce. Its boundary is the machine. If that host loses power, runs out of memory, or needs maintenance, the entire Grid is affected unless you build additional failover around it.

Kubernetes describes desired state through API objects. A Deployment keeps a requested number of browser pods alive, Services give stable discovery names, and the scheduler places pods according to resources and constraints. If a pod exits, its controller replaces it. If a worker disappears, pods can be rescheduled on healthy workers when the cluster has capacity. Those benefits arrive with more objects, more permissions, and more ways to misconfigure networking or capacity.

Do not confuse Selenium's session scheduler with the platform scheduler. Selenium matches requested browser capabilities to available slots. Kubernetes selects a node for a browser pod. A healthy design makes both layers visible: queued Selenium sessions reveal browser scarcity, while pending pods reveal cluster scarcity.

2. Define Capacity and Reliability Before You Pick a Platform

Write a one-page workload profile before installing anything. Record peak concurrent sessions, browser mix, average test duration, daily burst windows, acceptable queue time, recovery objective, and ownership. A suite that peaks at eight Chrome sessions for twenty minutes has different needs from an internal service accepting hundreds of jobs from ten repositories.

Use session slots as the capacity unit. If you run four Chrome containers and each exposes one slot, useful capacity is four concurrent tests. Increasing a test runner's thread count to twenty only creates a queue. Conversely, raising sessions per browser container can increase contention and instability because several browsers compete for shared memory, CPU, video buffers, and temporary storage. Begin with one session per browser container or pod, then load-test any denser configuration.

Set an explicit service objective. For example: 95 percent of sessions should start within 60 seconds during the normal CI window, and a browser pod failure should not make the Grid endpoint unavailable. This is an internal target, not a universal benchmark. It lets you compare actual outcomes instead of comparing feature lists.

Also define a capacity ceiling. An unlimited burst can consume every shared cluster node and disrupt applications. Docker naturally stops at host capacity, though failure may be abrupt. Kubernetes needs quotas, maximum replica counts, and scheduling boundaries. For a broader capacity model, read the Selenium Grid cloud scaling guide.

3. Prerequisites and a Platform-Neutral Smoke Test

For the Docker path, install a current Docker Engine with the Compose v2 plugin. For the Kubernetes path, use a conformant cluster with at least two schedulable worker nodes if you want to test failure recovery, plus a current kubectl configured for that cluster. Pin an official Selenium image tag that your team has tested. The examples use selenium/hub:4.39.0-20251212 and matching Chrome Node images so the components stay on one release. Before production use, confirm that tag exists in your registry and update all Selenium images together.

Create this Python smoke test as smoke_test.py. It uses only public Selenium Python APIs and accepts the Grid URL from the environment.

import os
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

grid_url = os.getenv("SELENIUM_GRID_URL", "http://localhost:4444")
options = webdriver.ChromeOptions()
options.add_argument("--headless=new")
options.add_argument("--window-size=1280,720")

driver = webdriver.Remote(command_executor=grid_url, options=options)
try:
    driver.get("https://www.selenium.dev/selenium/web/web-form.html")
    text_box = WebDriverWait(driver, 10).until(
        EC.element_to_be_clickable((By.NAME, "my-text"))
    )
    text_box.send_keys("grid smoke test")
    driver.find_element(By.CSS_SELECTOR, "button").click()
    message = WebDriverWait(driver, 10).until(
        EC.visibility_of_element_located((By.ID, "message"))
    )
    assert message.text == "Received!"
    print(f"PASS session={driver.session_id} browser={driver.capabilities['browserVersion']}")
finally:
    driver.quit()

Install the client in an isolated environment and check the import.

python3 -m venv .venv
. .venv/bin/activate
python -m pip install 'selenium>=4.39,<5'
python -c 'import selenium; print(selenium.__version__)'

Verification: the last command must print a 4.x version that satisfies the declared range. Keeping the smoke test unchanged across both deployments makes the comparison fair. Only SELENIUM_GRID_URL will vary.

4. Run Selenium Grid with Docker Compose

Create compose.yaml. The Hub publishes its client and status endpoint on port 4444. Chrome registers through the Grid event bus. The shared-memory mount avoids Chrome failures caused by Docker's small default /dev/shm. A single browser session per Node makes capacity obvious.

services:
  selenium-hub:
    image: selenium/hub:4.39.0-20251212
    ports:
      - "4444:4444"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:4444/status"]
      interval: 10s
      timeout: 5s
      retries: 12
    restart: unless-stopped

  chrome:
    image: selenium/node-chrome:4.39.0-20251212
    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"
    restart: unless-stopped

Start the Hub and three Chrome Nodes. Compose's --scale flag is preferable to copying nearly identical services.

docker compose up -d --scale chrome=3
docker compose ps
curl --fail --silent http://localhost:4444/status

Verification: docker compose ps should show one healthy Hub and three Chrome containers. The status response should contain "ready": true. Run the platform-neutral test next.

SELENIUM_GRID_URL=http://localhost:4444 python smoke_test.py

Verification: the process prints PASS, a nonempty session identifier, and a Chrome version, then exits with status 0. Run three copies concurrently to validate the declared capacity.

for run in 1 2 3; do SELENIUM_GRID_URL=http://localhost:4444 python smoke_test.py & done
wait

Verification: you should receive three PASS lines. If one hangs in the queue, inspect docker compose logs selenium-hub chrome. The dynamic Selenium Nodes with Docker tutorial covers an alternative in which the Grid creates containers on demand instead of keeping fixed Node replicas alive.

Compose is attractive because one engineer can understand this deployment in minutes. Keep the host dedicated, monitor disk and memory, and patch Docker Engine. For a disposable integration environment that includes databases and services as well as browsers, the Docker Compose test environments guide shows the broader pattern.

5. Run Selenium Grid on Kubernetes

Use a dedicated namespace so quotas, access, and cleanup apply only to the Grid. The following manifest creates a Hub Deployment, a stable Service, and three Chrome replicas. Resource requests give the scheduler a realistic placement signal. Limits bound a runaway browser, although you must tune these illustrative values against your pages and test behavior.

Save it as selenium-grid.yaml.

apiVersion: v1
kind: Namespace
metadata:
  name: selenium-grid
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: selenium-hub
  namespace: selenium-grid
spec:
  replicas: 1
  selector:
    matchLabels:
      app: selenium-hub
  template:
    metadata:
      labels:
        app: selenium-hub
    spec:
      containers:
        - name: hub
          image: selenium/hub:4.39.0-20251212
          ports:
            - { name: web, containerPort: 4444 }
            - { name: publish, containerPort: 4442 }
            - { name: subscribe, containerPort: 4443 }
          readinessProbe:
            httpGet: { path: /readyz, port: 4444 }
            initialDelaySeconds: 5
            periodSeconds: 5
          resources:
            requests: { cpu: "250m", memory: "512Mi" }
            limits: { cpu: "1", memory: "1Gi" }
---
apiVersion: v1
kind: Service
metadata:
  name: selenium-hub
  namespace: selenium-grid
spec:
  selector:
    app: selenium-hub
  ports:
    - { name: web, port: 4444, targetPort: 4444 }
    - { name: publish, port: 4442, targetPort: 4442 }
    - { name: subscribe, port: 4443, targetPort: 4443 }
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: selenium-chrome
  namespace: selenium-grid
spec:
  replicas: 3
  selector:
    matchLabels:
      app: selenium-chrome
  template:
    metadata:
      labels:
        app: selenium-chrome
    spec:
      containers:
        - name: chrome
          image: selenium/node-chrome:4.39.0-20251212
          env:
            - { name: SE_EVENT_BUS_HOST, value: selenium-hub }
            - { name: SE_EVENT_BUS_PUBLISH_PORT, value: "4442" }
            - { name: SE_EVENT_BUS_SUBSCRIBE_PORT, value: "4443" }
            - { name: SE_NODE_MAX_SESSIONS, value: "1" }
            - { name: SE_NODE_OVERRIDE_MAX_SESSIONS, value: "true" }
          volumeMounts:
            - { name: dshm, mountPath: /dev/shm }
          resources:
            requests: { cpu: "500m", memory: "1Gi" }
            limits: { cpu: "2", memory: "2Gi" }
      volumes:
        - name: dshm
          emptyDir:
            medium: Memory
            sizeLimit: 2Gi

Apply it and wait for both Deployments.

kubectl apply -f selenium-grid.yaml
kubectl -n selenium-grid rollout status deployment/selenium-hub --timeout=180s
kubectl -n selenium-grid rollout status deployment/selenium-chrome --timeout=300s
kubectl -n selenium-grid get pods -o wide

Verification: the Hub should report 1/1 ready, and three Chrome pods should report 1/1 ready. Pending Chrome pods mean the cluster cannot satisfy requests, taints, affinity, or quotas. That is a cluster capacity problem, not a Selenium registration problem.

Forward the Service to your workstation in a dedicated terminal.

kubectl -n selenium-grid port-forward service/selenium-hub 4444:4444

In another terminal, verify status and run the same smoke test.

curl --fail --silent http://localhost:4444/status
SELENIUM_GRID_URL=http://localhost:4444 python smoke_test.py

Verification: status contains "ready": true, and the test prints PASS. Port forwarding is appropriate for local validation, not a production CI access pattern. CI runners inside the cluster can use http://selenium-hub.selenium-grid.svc.cluster.local:4444; external runners need an authenticated, encrypted ingress or private network path. See running Selenium Grid in Kubernetes for a focused deployment walkthrough.

6. Compare Scaling, Recovery, and Upgrade Behavior

Docker Compose scales fixed Nodes with one command: docker compose up -d --scale chrome=6. It is fast and transparent, but every replica still competes for the same host. Keep enough CPU, RAM, and shared memory for the peak. A restart policy replaces a failed container, but it cannot move work away from a dead machine. If the Hub is unavailable, new sessions fail or queue until it returns.

Kubernetes can spread replicas across worker nodes. Add topology spread constraints or pod anti-affinity when losing one worker must not remove most browser capacity. A Deployment replaces failed pods and supports controlled image rollouts. Verify every change with kubectl rollout status, inspect events when pods remain pending, and retain the previous image digest for rollback. Kubernetes does not preserve an active browser session when its pod dies. Your test runner still needs retries at the job or failed-test level, chosen carefully to avoid hiding product defects.

Autoscaling requires more thought than attaching an HPA to CPU. Browser CPU is often bursty, and CPU utilization does not directly represent the Selenium session queue. A CPU-based HorizontalPodAutoscaler can be a useful first control only after requests are realistic and the metrics pipeline works. Queue-aware scaling is stronger: export queue depth, expose it through a metrics adapter, and scale browser replicas against demand while enforcing a maximum. The Selenium Grid Kubernetes autoscaling tutorial covers that implementation.

Regardless of platform, test scaling with a controlled burst. Submit more sessions than available slots, record queue time, and confirm the system returns to its baseline after the run. Scaling that starts pods after the CI job has already timed out is operationally correct but useless to testers.

7. Compare Security, Observability, and Cost

Neither default sample should be exposed directly to the public internet. A Grid endpoint can launch browsers and access network destinations available from its Nodes. Put it on a private network, authenticate access at a proxy or gateway, restrict inbound clients, and limit browser egress to required test environments. Run containers with the least privilege supported by the official image, scan pinned images, and plan regular upgrades.

Kubernetes offers stronger shared-platform controls through namespaces, ResourceQuota, LimitRange, RBAC, NetworkPolicy, admission rules, and workload identity. Those controls only help when implemented and audited. A loosely governed cluster can be harder to reason about than a locked-down Docker host. Compose remains viable when the host is isolated, access is narrow, secrets are not embedded in YAML, and the blast radius is acceptable.

Observe both Selenium and infrastructure. At minimum, capture session requests, queue duration, rejected or timed-out sessions, active slots, test duration, container restarts, CPU throttling, memory pressure, and disk growth from downloads or videos. Correlate the Selenium session ID printed by the smoke test with test reports and Grid logs. For distributed traces and metrics, use the Selenium Grid OpenTelemetry monitoring setup.

Cost is not simply server price. Compare monthly compute, idle browser capacity, cluster control-plane charges where applicable, logging volume, engineering hours, upgrades, incident response, and CI time lost in queues. Kubernetes can reduce idle browser capacity through scaling, yet its baseline services and specialist support may cost more than one well-sized host. Compose can be inexpensive until host saturation causes long queues or recurring outages. Calculate cost per completed, trustworthy test run using your own billing and labor data. Do not justify a migration with an invented universal saving percentage.

8. Which Should You Choose: Docker vs Kubernetes for Selenium Grid

Choose Docker Compose when all of these are mostly true: one team owns the Grid, one host can handle peak sessions with safe headroom, test demand is predictable, brief maintenance windows are acceptable, and the team wants minimal platform work. It is also the best learning and proof-of-concept environment because the topology remains visible in one file. Start with bounded replicas, health checks, pinned images, and a monitored dedicated host.

Choose Kubernetes when several independent signals exist: demand routinely exceeds one machine, multiple teams need an internal browser service, worker-level failure recovery matters, browser pools need policy isolation, or queue-driven scaling can produce measurable value. Kubernetes is especially reasonable when your organization already operates clusters, monitoring, ingress, secrets, and on-call processes. In that case, Selenium becomes another supported workload rather than a brand-new platform.

Do not migrate solely because the suite is flaky. Kubernetes can replace dead browser pods, but it cannot repair brittle locators, shared test data, missing waits, or environment instability. Establish a Compose baseline first. Record p50 and p95 session queue time, total run time, session creation error rate, completed tests per compute-hour, and operator time for at least several representative CI cycles. Pilot Kubernetes with the same image tags, test commit, browser count, and data. Migrate only if it improves the defined service objective enough to repay its operational cost.

A hybrid model can also be correct. Keep fast pull-request smoke suites on predictable Compose capacity and send scheduled high-concurrency regression jobs to a shared Kubernetes Grid. The test code stays portable because both expose the same RemoteWebDriver contract.

9. Common Mistakes

Treating replicas as throughput -> Count registered Selenium slots and completed sessions. A running pod that cannot register with the event bus contributes zero capacity.

Leaving image tags floating -> Pin the same Selenium release for Hub and Nodes. Validate upgrades in a staging Grid before changing production.

Packing too many sessions into one browser container -> Start with one session per Node. Increase density only after measuring memory, CPU throttling, crash rate, and runtime variance.

Ignoring /dev/shm -> Give Chrome a sufficiently sized shared-memory mount. Small shared memory often appears as browser crashes or unreachable renderer errors under concurrency.

Autoscaling without resource requests -> Define and tune requests first. CPU utilization autoscaling uses requests as its denominator, so missing or unrealistic values undermine the signal.

Exposing port 4444 publicly -> Keep the endpoint private and require authenticated, encrypted access through a controlled network boundary. Restrict browser egress as well as client ingress.

Using readiness as a complete test -> A ready Hub does not prove a Node can create a browser session and load the target application. Run the RemoteWebDriver smoke test after every deployment.

Blaming Selenium for pending pods -> Inspect Kubernetes scheduling events. Unsatisfied requests, quotas, taints, or affinity rules must be fixed at the platform layer.

Scaling after the queue timeout -> Include image pull and browser registration time in capacity planning. Pre-warm a small baseline when CI bursts are short.

Migrating to cure flaky tests -> Classify failures first. Infrastructure orchestration will not fix assertion races, coupled data, or unstable application behavior.

10. Interview Questions and Answers

A strong interview explanation separates Grid scheduling from container scheduling and ties the platform choice to measured constraints. Be ready to describe the decision through session slots, failure domains, queue time, resource limits, and operational ownership. The model answers in the structured interview section below cover six common questions without assuming that Kubernetes is automatically the senior choice.

11. Conclusion

For most teams evaluating Docker vs Kubernetes for Selenium Grid, Docker Compose is the correct first deployment. It proves the browser topology, test compatibility, resource envelope, and real concurrency requirement with little operational overhead. Keep it when one reliable host meets the service objective.

Move to Kubernetes when measurements show a need for multi-node capacity, shared-service governance, automatic rescheduling, or demand-based scaling, and when your organization can operate the added platform responsibly. Whichever path you choose, pin images, bound capacity, protect the endpoint, observe Selenium queue health, and run a real browser smoke test after every change. You can then improve the tests themselves in the QA practice workspace or compare your automation evidence against a target role in the resume analysis dashboard.

Interview Questions and Answers

How would you choose between Docker Compose and Kubernetes for Selenium Grid?

I would quantify peak sessions, browser mix, queue-time objective, failure-recovery requirement, and platform ownership. Compose is my default if a dedicated host has safe capacity and one team owns the service. I would choose Kubernetes when demand crosses a host boundary or the Grid needs shared-service controls, rescheduling, and justified autoscaling. I would validate the choice with the same workload and image versions on both platforms.

What is the difference between Selenium scheduling and Kubernetes scheduling?

Selenium matches a new WebDriver request and its browser capabilities to an available Grid slot. Kubernetes places the pod that supplies that slot onto a worker node according to resources and scheduling constraints. A Selenium queue means browser slots are unavailable, while a pending pod often means cluster placement failed. I monitor both layers because each has a different remedy.

How do you verify a Selenium Grid deployment after an upgrade?

I first wait for the platform rollout and check that the Grid status reports ready with the expected registered slots. Then I create a real RemoteWebDriver session, load a controlled page, interact with an element, assert the result, and quit cleanly. I also run a bounded concurrent batch equal to expected capacity. Readiness alone is insufficient because it does not prove browser creation and page execution.

Why are CPU and memory requests important for Selenium browser pods?

Requests tell the Kubernetes scheduler how much capacity a pod needs and form the denominator for common CPU utilization autoscaling. If they are absent or unrealistic, pods may be packed onto an overloaded worker or scaled on a misleading signal. Limits bound consumption, but limits that are too tight cause throttling or browser termination. I tune both from observed workloads rather than copying a universal value.

How would you autoscale Chrome Nodes in Kubernetes?

I would keep a small warm baseline and cap maximum replicas with quota. After defining accurate resource requests, I could start with CPU scaling, then move toward a queue-aware external metric because queued sessions represent unmet browser demand more directly. I would account for image download, pod startup, and Grid registration latency. Finally, I would load-test burst and scale-down behavior without allowing active sessions to be disrupted.

What failure does Kubernetes not solve for Selenium tests?

Kubernetes cannot preserve a browser session when its pod or worker dies, and it cannot fix flaky test logic. It can create a replacement pod for future sessions, but the interrupted test still fails unless the CI workflow applies an intentional retry policy. It also cannot repair unstable locators, shared data collisions, or slow application dependencies. Those require test and product engineering changes.

How would you secure an internal Selenium Grid?

I would keep the Grid off the public internet and permit only approved CI runners over private networking or an authenticated TLS gateway. Browser egress would be restricted to required test destinations, and credentials would come from a secret manager instead of manifests. I would pin and scan images, minimize workload privileges, log session creation, and apply namespace quotas and network policies on Kubernetes.

Frequently Asked Questions

Is Docker or Kubernetes better for Selenium Grid?

Docker Compose is better for a small, predictable Grid that fits on one host. Kubernetes is better for a shared or variable-demand Grid that needs multi-node placement, policy controls, and automated recovery. The better option is the least complex platform that meets a documented reliability and capacity target.

Does Kubernetes make Selenium tests run faster?

Not by itself. Kubernetes can provide more browser capacity and reduce queue time when the cluster has available resources, but individual test speed still depends on the application, browser, network, and test design. Poorly sized pods can make execution slower through CPU throttling or memory pressure.

How many Selenium sessions should run in one container?

Start with one session per browser Node container or pod because it gives clear isolation and predictable capacity. Raise the density only after a controlled load test shows stable memory, CPU, browser crash rate, and duration variance. A higher configured maximum is not useful if contention makes tests unreliable.

Can Docker Compose scale Selenium Grid across multiple machines?

Docker Compose normally manages services on one Docker Engine, so its replicas share one host boundary. You can build external arrangements around multiple engines, but at that point a real scheduler or managed browser service is usually easier to operate. Compose remains ideal when peak browser demand fits safely on one machine.

How should Selenium Grid autoscaling work on Kubernetes?

Begin with accurate pod resource requests, a minimum warm replica count, and a strict maximum. CPU-based scaling can be an initial control, but Selenium queue depth and session demand are more direct signals when exposed through a supported metrics adapter. Validate image-pull and registration delay so new capacity arrives before client timeouts.

What metrics matter when comparing Docker and Kubernetes for Grid?

Track session queue time, session creation failure rate, active and available slots, completed tests, test duration, restart count, resource pressure, and operator effort. Add infrastructure cost per completed run and separate Selenium queueing from Kubernetes pod scheduling delays. Compare both platforms with the same tests, images, and concurrency.

Should Selenium Grid be exposed through a public load balancer?

No, not as an unauthenticated public endpoint. Browsers can reach systems available from their network and consume substantial compute. Use private connectivity plus authenticated TLS access, narrowly scoped ingress, restricted egress, and monitoring for unexpected session creation.

Related Guides