Running Selenium in Docker is one of those ideas that sounds simple until you try to make it reliable across laptops, CI jobs, and browser versions. The appeal is obvious: isolate dependencies, standardize browser environments, and make Selenium container tests easier to reproduce. The catch is that Docker solves environment drift, not test design problems. If your suite depends on timing, poor locator strategy, or uncontrolled parallelism, containers will package the pain very efficiently.

This tutorial focuses on the practical setup I would use for a small to medium test suite that needs to run Selenium tests in Docker locally and in CI. I will show a minimal working pattern, explain when to use Selenium Grid, and call out the failure modes that usually show up after the first green run.

For teams that do not want to own browser containers, driver images, and CI plumbing, Endtest is a simpler alternative worth evaluating. It uses an agentic AI workflow and editable, human-readable steps, so you spend less time maintaining infrastructure and more time testing the product. But if you do want to keep Selenium, Docker is still a solid operational choice when used carefully.

What Docker actually gives you for Selenium

Docker does three useful things for browser testing:

  1. It pins browser and driver dependencies.
  2. It makes the runtime disposable, so every test job starts clean.
  3. It gives CI a repeatable way to start browsers without installing them on the host machine.

That sounds like a small improvement, but in practice it removes an entire class of setup bugs. On a developer laptop, those bugs often look like mismatched ChromeDriver versions, stale browser installs, or path differences between macOS and Linux. In CI, they look like one runner image working and another failing because a browser update landed at the wrong time.

The tradeoff is that Docker introduces a second environment to manage. You are now responsible for image tags, memory limits, startup timing, networking between containers, and whether your test code expects a local browser or a remote WebDriver endpoint.

Docker is not a test strategy. It is a packaging strategy for your test runtime.

The official Selenium documentation is still the best starting point for driver and Grid behavior, especially if you are using Selenium WebDriver and Grid. Docker is the transport layer around that setup, not a replacement for understanding how WebDriver sessions work.

The two common architectures

There are two common ways to run Selenium tests in Docker.

1. App test container plus browser container

Your application under test may run in one container, while the browser runs in another container, usually a Selenium standalone or Grid node. The test process runs either on your host machine, in a test container, or in a CI runner.

This is the simplest model when you want remote WebDriver control over a browser container.

2. Full test stack in Docker Compose

This puts the app, test runner, and browser service into a single Docker Compose environment. It is useful when you want a stable integration environment that CI and developers can both use.

For most teams, Docker Compose is the easiest starting point. It keeps the moving parts visible and makes service discovery simple. If you later need more browsers or parallelism, you can move toward Selenium Grid.

Minimal setup with Selenium and a browser container

A practical Docker browser testing setup usually includes:

  • a Selenium browser image, such as standalone Chrome
  • your application or test target
  • a test runner container or local test process
  • a Docker network so the components can talk to each other

Example Docker Compose file

Here is a small example that runs a Selenium standalone Chrome container and lets your tests connect to it remotely:

version: '3.8'
services:
  selenium:
    image: selenium/standalone-chrome:latest
    shm_size: 2gb
    ports:
      - "4444:4444"

tests: build: . depends_on: - selenium environment: SELENIUM_REMOTE_URL: http://selenium:4444/wd/hub

A few details matter here:

  • shm_size: 2gb is not cosmetic. Chrome often crashes or becomes unstable when shared memory is too small.
  • depends_on does not mean the browser is ready, only that the container has started.
  • Exposing port 4444 lets you debug the Grid UI and inspect the Selenium endpoint from the host machine.

Example Selenium test code

This Python example connects to the browser container using Remote WebDriver.

import os
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.options import Options

remote_url = os.environ[“SELENIUM_REMOTE_URL”]

options = Options() options.add_argument(“–headless=new”)

ძღriver = webdriver.Remote(command_executor=remote_url, options=options) try: driver.get(“https://example.com”) assert “Example Domain” in driver.title heading = driver.find_element(By.TAG_NAME, “h1”) assert heading.text == “Example Domain” finally: driver.quit()

I have intentionally kept this simple. The important part is the remote session setup, not the assertion style. If your suite already uses a page object layer or fixtures, keep that structure. Docker does not require a rewrite of your test architecture.

One practical note, the --headless=new flag is appropriate for current Chromium-based headless mode, but browser image compatibility can change. If a browser image changes behavior, pin the image tag rather than assuming latest will remain safe.

Dockerfile for the test runner

If you run tests inside a container, give the runner a small, predictable image.

dockerfile FROM python:3.12-slim

WORKDIR /tests

COPY requirements.txt . RUN pip install –no-cache-dir -r requirements.txt

COPY . . CMD [“pytest”, “-q”]

And a corresponding requirements.txt:

selenium==4.23.1
pytest==8.3.2

You do not need a giant image. The goal is to keep the test runner fast to build and easy to cache in CI. If your tests need other tools, such as curl for readiness checks or pytest-xdist for parallelism, add them intentionally.

Waiting for the browser and the app

The most common early failure in Selenium container tests is a race condition. The browser starts, but your tests begin before the browser endpoint or app endpoint is ready.

What to wait for

You should wait for both:

  • Selenium remote endpoint readiness
  • application readiness, often a health check endpoint

A simple readiness probe can save a lot of wasted debugging time.

import time
import requests

def wait_for_url(url, timeout=30): deadline = time.time() + timeout while time.time() < deadline: try: if requests.get(url, timeout=2).status_code < 500: return except requests.RequestException: pass time.sleep(1) raise RuntimeError(f”Timed out waiting for {url}”)

This is better than sleeping blindly because it gives you a failure with a reason. Sleep is not synchronization, it is guesswork.

Local development workflow

A good local Docker setup should let a developer run the same command repeatedly and get the same result.

A typical flow is:

  1. Start the stack with Docker Compose.
  2. Wait for the app and Selenium endpoint.
  3. Run the tests.
  4. Inspect logs if something fails.

Example:

docker compose up -d selenium
python -m pytest tests/test_login.py

If your tests run inside Docker too, you can keep the host machine cleaner and reduce “works on my laptop” variation.

docker compose up --build --abort-on-container-exit

That command is useful for CI parity because it forces the test container to use the same image you built locally. It also makes log collection easier since the test process and browser service are part of one stack.

When Selenium Grid is worth it

Standalone Chrome is enough for a lot of teams. Selenium Grid becomes useful when you need one or more of the following:

  • multiple browser types
  • parallel execution
  • session routing across nodes
  • scaling browser capacity independently from test execution

Grid adds operational complexity, so I would not introduce it early just because it sounds more scalable. If your suite is small, the simplest thing that works is usually a single browser container.

If you do need Grid, the Selenium documentation on Grid is the primary reference. Treat the Grid as infrastructure, with the same discipline you would apply to databases or queues. That means version pinning, health checks, and explicit timeouts.

Common failure modes in Docker browser testing

1. Shared memory exhaustion

Chrome can crash or behave erratically if /dev/shm is too small. This is why many browser images recommend a larger shm_size.

If you see random tab crashes, blank pages, or odd navigation failures, inspect shared memory before blaming the test.

2. Timing assumptions

Containers start quickly, but your app may still be warming up. Selenium tests that assume an element will exist immediately often become flaky under Docker because the environment is just fast enough to expose race conditions you previously missed.

Use explicit waits for real conditions, not arbitrary sleep calls.

from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

wait = WebDriverWait(driver, 10) wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, “#submit”)))

3. Overusing latest

latest is convenient, but it is a bad default for test infrastructure. Browser images and Selenium images evolve. If a change breaks your suite, you want a clean diff between image versions, not a mystery update.

Pin the image tag, document why you chose it, and upgrade deliberately.

4. Incorrect networking assumptions

localhost inside a container is not the host machine. It is the container itself. When your tests cannot reach the app, check whether you are pointing at the correct service name in the Docker network.

5. Test data cleanup

Docker does not clean your test data. If your test suite creates users, orders, or files, you still need teardown logic or disposable environments. Containerized browsers are not a substitute for good test isolation.

CI/CD integration pattern

The best Docker setup is the one your CI pipeline can run without special casing. For GitHub Actions, one clean pattern is to build the test image, start the services, and execute the test command inside the container.

name: selenium-tests

on: [push, pull_request]

jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Build and run tests run: docker compose up –build –abort-on-container-exit

That is not the only way to do it, but it keeps the CI logic close to the local workflow. If a developer can reproduce a failure with the same compose file, triage gets faster.

For larger suites, you may want to split build and test stages, cache dependencies, and publish image artifacts. At that point, treat the browser image as a dependency with release notes, not as a disposable utility.

Debugging tips that save time

A few habits make Selenium Docker debugging much less painful:

  • Enable verbose logs from the browser container.
  • Save screenshots and page source on failure.
  • Keep the browser endpoint accessible from the host for ad hoc inspection.
  • Log the current URL and browser capabilities in failures.

A small pytest fixture can help capture artifacts:

import pytest

def pytest_runtest_makereport(item, call): if call.excinfo is not None: driver = item.funcargs.get(“driver”) if driver: driver.save_screenshot(f”artifacts/{item.name}.png”)

This kind of artifact collection is boring, which is exactly why it matters. Most flaky-test triage is not about fancy tooling, it is about having enough evidence to reconstruct the failure.

How Docker changes the maintenance cost

Docker lowers environment drift, but it does not eliminate ownership. The true cost of Selenium Docker includes:

  • browser image maintenance
  • driver and Selenium version compatibility
  • container startup and resource tuning
  • CI time spent waiting for browser containers
  • debugging intermittent startup or network issues
  • onboarding time for engineers who need to understand the stack

That cost is acceptable when the suite is valuable and the team has the discipline to maintain it. It is often a poor tradeoff when the main goal is simply to get end-to-end coverage quickly without maintaining infrastructure.

In those cases, a maintained platform can be more rational. Endtest is one option to evaluate if you want to avoid owning browser containers and Docker test environments while still keeping automated browser coverage. Its agentic AI model and editable platform-native steps are aimed at reducing setup and maintenance overhead, which matters when the operational burden is the real problem.

Choosing between standalone containers and a maintained platform

A useful selection rule is this:

  • choose Selenium in Docker if you need control, portability, and existing framework compatibility
  • choose a maintained platform if your team is spending too much time on harness maintenance, browser provisioning, or Grid operations

A careful team should compare them against the same criteria: test creation speed, maintenance burden, debugging visibility, CI cost, and ownership concentration. If you are migrating an existing suite, Endtest’s Selenium migration documentation is relevant because it supports bringing in Java, Python, and C# test suites.

If you want a broader framework-level comparison, the Endtest vs Selenium page is the right place to start, but the main decision should still be operational, not ideological.

A practical checklist before you scale up

Before you add more browsers, more parallel jobs, or a full Grid, make sure you have the basics working:

  • image tags are pinned
  • container memory is sufficient
  • readiness checks are explicit
  • failing tests capture screenshots and logs
  • CI can reproduce local failures
  • your locator strategy is stable enough for repeated runs
  • you know why Docker is helping, not just that it is “modern”

If you can answer those points clearly, you are ready to expand the setup. If not, scaling will mostly scale confusion.

Final thoughts

To run Selenium tests in Docker well, keep the architecture small, pin your dependencies, and treat browser containers as infrastructure that needs operating discipline. Docker is excellent at making environments reproducible, but reproducibility only helps if your tests already express clear waits, stable locators, and deterministic setup.

The simplest working setup is often a single Selenium standalone browser container plus a test runner container in Docker Compose. Add Grid only when you actually need parallel browser capacity or multiple browser types. Keep failures observable with logs and artifacts, and avoid turning latest into a hidden dependency.

If your team wants the benefits of browser automation without owning browser containers, Endtest is a credible simpler alternative to evaluate. For teams that do want the control of Selenium Docker, the setup above is a good foundation for production-minded Test automation.