GUIDE / automation
Run Selenium Tests in Docker: Complete QA Guide
Learn how to run Selenium tests in Docker with browsers, Grid, CI pipelines, debugging artifacts, stable setup, and fewer environment issues.
Run Selenium tests in Docker when you want browser automation to behave the same on a laptop, CI runner, release branch, and scheduled regression job. Docker gives you a repeatable browser environment, pinned dependencies, and a cleaner path to Selenium Grid without asking every tester to install the same browser, driver, system libraries, and display stack by hand.
This guide walks through the practical setup: choosing the right Docker pattern, running a single Chrome container, connecting Selenium test code to a remote browser, scaling with Grid, wiring everything into CI, and debugging failures without guessing. It also covers common mistakes that make containerized Selenium slow or flaky.
Run Selenium Tests in Docker: What It Solves
Selenium depends on more than your test code. A real run uses a browser, browser driver, operating system libraries, fonts, certificates, display settings, network access, environment variables, timeouts, and sometimes a file system for downloads. When those pieces vary across machines, tests fail for reasons that have nothing to do with the product.
Docker reduces that variation. Instead of saying, "install Chrome, install the matching driver, check your PATH, and hope the CI image has the right shared libraries," you define the browser environment as an image. Everyone pulls the same image. The pipeline starts the same services. A failure becomes easier to reproduce because the environment is no longer a mystery.
Running Selenium in Docker is especially useful when:
- Your team has different operating systems.
- CI runners are rebuilt often.
- Browser and driver versions drift.
- You need multiple browsers in parallel.
- You want Selenium Grid without managing long lived servers.
- You want disposable browser sessions for every job.
- You need screenshots, logs, and videos as build artifacts.
Docker is not a magic flakiness cure. If a test clicks too early, uses brittle XPath, shares accounts with other tests, or asserts unstable UI text, Docker will faithfully reproduce those problems. Use Docker to make the infrastructure reliable, then fix test design separately. For broader automation design, see how to build a test automation framework.
The Main Selenium Docker Patterns
There are three common ways to run Selenium with Docker. Choose based on suite size, team maturity, and CI needs.
| Pattern | Best For | How It Works | Tradeoff |
|---|---|---|---|
| Standalone browser container | Small and medium suites | Test code connects to one Selenium browser container | Simple, but limited parallel scale |
| Selenium Grid in Docker Compose | Cross browser and parallel runs | Hub or router coordinates multiple browser nodes | More moving parts, better scaling |
| Custom all in one image | Legacy projects or isolated runners | Test code and browser live inside one image | Easy to trigger, harder to maintain cleanly |
Most teams should start with the standalone browser container, then move to Grid when parallel execution or cross browser coverage becomes important. Avoid over designing the first version. Your first goal is to make one reliable test run in Docker, publish artifacts, and document the command.
Prerequisites and Mental Model
Before writing a Docker command, understand the network relationship. Selenium test code usually does not "open Chrome" directly when Docker is involved. It sends WebDriver commands to a remote Selenium server. That server controls the browser inside a container.
The flow looks like this:
Test runner process
sends WebDriver commands to
Selenium server in Docker
controls
Chrome or Firefox in the same browser container
opens
Application under test
If your application also runs in Docker, all services should share a network. If the application runs on your host machine, the browser container must know how to reach it. On macOS and Windows, host.docker.internal usually resolves to the host. On Linux, you may need an explicit host gateway or a Compose network.
The most important habit is to treat the browser as remote. That means your code should use RemoteWebDriver or the equivalent remote WebDriver configuration in your language and framework. Local driver creation is the most common reason Docker attempts fail.
Basic Standalone Chrome Setup
The fastest useful setup is a Selenium standalone Chrome container. It exposes a WebDriver endpoint on port 4444. Your test code connects to that endpoint.
docker run --rm -d \
--name selenium-chrome \
-p 4444:4444 \
-p 7900:7900 \
--shm-size="2g" \
selenium/standalone-chrome:latest
In a real project, pin the image version instead of using latest. The example uses latest only to show the shape of the command. Pinning protects your CI pipeline from surprise browser upgrades.
The --shm-size="2g" flag matters. Chrome uses shared memory. Docker's default shared memory can be too small, which causes random browser crashes, blank pages, or session failures. You will see teams add many workarounds for flaky Chrome when the real issue is simply shared memory.
Now your Selenium test should point to:
http://localhost:4444/wd/hub
Some Selenium versions also support:
http://localhost:4444
Use the endpoint expected by your client library and image version.
Example: Java RemoteWebDriver
Here is a minimal Java example. The key idea is that the test creates a remote session instead of starting a local ChromeDriver binary.
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.remote.RemoteWebDriver;
import java.net.URL;
public class DockerChromeExample {
public static void main(String[] args) throws Exception {
ChromeOptions options = new ChromeOptions();
options.addArguments("--window-size=1440,900");
WebDriver driver = new RemoteWebDriver(
new URL("http://localhost:4444/wd/hub"),
options
);
try {
driver.get("https://example.com");
System.out.println(driver.getTitle());
} finally {
driver.quit();
}
}
}
For test frameworks, place the remote URL in configuration. Do not hardcode it in every test. A good framework lets you switch between local and Docker with an environment variable:
SELENIUM_REMOTE_URL=http://localhost:4444/wd/hub
That one configuration point keeps your framework useful for both local debugging and CI execution.
Example: Python Remote WebDriver
The same pattern applies in Python:
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
options = Options()
options.add_argument("--window-size=1440,900")
driver = webdriver.Remote(
command_executor="http://localhost:4444/wd/hub",
options=options,
)
try:
driver.get("https://example.com")
print(driver.title)
finally:
driver.quit()
If your Python project currently imports webdriver.Chrome() directly in fixtures, move that logic into a driver factory. The tests should not care whether the browser is local, Docker, Grid, or cloud. Only the factory should care.
This design also helps if you compare frameworks later. The same discipline applies to Selenium, Playwright, and Cypress. For tool selection context, read Selenium vs Playwright vs Cypress.
Running Your Application and Browser Together
Most real tests do not open public websites. They open your application. If the application is containerized, Docker Compose is cleaner than many independent docker run commands.
A simple Compose file might look like this:
services:
web:
build: .
ports:
- "3000:3000"
environment:
NODE_ENV: test
selenium:
image: selenium/standalone-chrome:4.23.0
shm_size: 2gb
ports:
- "4444:4444"
- "7900:7900"
Inside the browser container, the app is not localhost:3000. Localhost means the container itself. The browser should navigate to:
http://web:3000
That service name works because both services share the Compose network. This small detail prevents many confusing connection errors. If your tests run on the host and only the browser runs in Docker, the test code connects to localhost:4444, and the browser navigates to http://host.docker.internal:3000 or another reachable host address.
Moving from Standalone to Selenium Grid
A standalone container runs one browser service. Selenium Grid lets you run many browser nodes and distribute sessions. Grid is useful when you need:
- Parallel browser sessions.
- Chrome and Firefox in the same suite.
- Separate node resources.
- A dashboard for active sessions.
- Cleaner scaling in CI.
Modern Selenium Grid can run with a router and nodes. A basic Docker Compose setup might include a hub or event bus depending on the Selenium version and image style. Keep the version consistent across router, hub, and node images. Mixing versions creates strange session errors.
The important test code change is usually small. Instead of connecting to a standalone container, connect to the Grid URL:
http://localhost:4444/wd/hub
The Grid decides which browser node receives the session based on requested capabilities. Your tests should request the browser clearly:
ChromeOptions options = new ChromeOptions();
options.setBrowserVersion("stable");
Use Grid when you have a reason. If your suite has 40 tests and runs in six minutes, standalone may be enough. If your suite has 1,200 tests, multiple browsers, and release gates, Grid becomes much more valuable. For a dedicated setup path, connect this article with Selenium Grid tutorial.
CI Pipeline Structure
A good CI job for Selenium in Docker has a predictable rhythm:
- Check out the code.
- Build or start the application.
- Start Selenium browser services.
- Wait until the app and Selenium are healthy.
- Run tests.
- Collect screenshots, reports, logs, and videos.
- Stop containers.
The health check step is not optional. Many teams start a browser container and immediately run tests. The first test fails because Selenium was not ready yet. Add a wait loop that checks the Selenium status endpoint before executing the suite.
until curl -s http://localhost:4444/status | grep -q '"ready":true'; do
echo "Waiting for Selenium..."
sleep 2
done
Also wait for your application. A web server that accepts TCP connections may still be running migrations, building assets, or warming caches. Use a real health endpoint when possible.
For CI design beyond Selenium, see CI/CD for test automation with GitHub Actions. The same ideas apply in Jenkins, GitLab CI, CircleCI, and Buildkite.
Browser Downloads, Uploads, and Files
File behavior changes when the browser lives in a container. If a test downloads a file, the file is saved inside the browser container unless you configure a shared volume or retrieve it through browser APIs. If a test uploads a file, the path must be valid from the browser container's point of view or transferred through WebDriver.
For downloads, create a known directory and mount it:
services:
selenium:
image: selenium/standalone-chrome:4.23.0
shm_size: 2gb
volumes:
- ./artifacts/downloads:/home/seluser/Downloads
Then configure Chrome preferences to use that directory. Save the directory as a CI artifact after the run. Do not assert files by checking your host's default download folder. That may work locally and fail in CI.
For uploads, prefer WebDriver's file upload behavior and keep test files in the test runner workspace. Many Selenium client libraries can transfer the file to the remote node. If your framework does not do this automatically, you may need a LocalFileDetector or equivalent.
Test Data and Environment Isolation
Docker solves browser environment drift, not test data drift. A stable container with unstable data still produces unstable tests.
Use isolated accounts, predictable seed data, and cleanup rules. If every parallel test logs in as the same user, one test can change the user's profile while another test asserts the old value. If one test deletes an entity that another test expects, Docker cannot protect you.
Good patterns include:
- Create data through APIs before the test.
- Use unique identifiers per run.
- Reset the database for isolated environments.
- Avoid shared mutable accounts.
- Keep test data factories close to the tests.
- Separate smoke data from destructive regression data.
When running in CI, store environment specific values as secrets or variables. Do not bake passwords, tokens, or internal URLs into Docker images. Images are copied, cached, and shared more widely than people expect.
Debugging Failures in Docker
Debugging containerized browser tests requires artifacts. A failing CI line that says ElementClickInterceptedException is not enough. Build your framework so every failure captures useful evidence:
- Screenshot of the browser.
- Current URL.
- Page source or DOM snapshot when safe.
- Browser console logs.
- Selenium server logs.
- Network logs if your setup supports them.
- Test runner logs.
- Video for complex end to end flows.
Selenium images often expose a noVNC interface on port 7900. This is helpful for local debugging. You can open the browser visually and watch what the test is doing. Do not depend on VNC for normal CI diagnosis, but use it when you need to understand layout, timing, or popups.
A practical debugging workflow:
- Pull the same image used in CI.
- Start the app and Selenium with the same Compose file.
- Run only the failing test.
- Open the VNC page if needed.
- Compare screenshot, console logs, and server logs.
- Fix the product, test, data, or environment based on evidence.
Avoid adding sleeps as the first response. A sleep may hide the problem and make the suite slower. Use explicit waits tied to product behavior. For example, wait for the order confirmation element to be visible and contain the expected order number.
Performance and Parallel Execution
Parallel Selenium in Docker consumes real CPU and memory. If you run 20 Chrome containers on a small CI runner, failures will look like test flakiness even though the machine is simply overloaded.
Start with a conservative ratio. Measure memory use, CPU saturation, average test duration, and failure patterns. Increase parallelism gradually. Browser automation is heavy because it launches real browsers. Container orchestration does not change that physics.
Use this checklist before increasing threads:
| Area | Question to Ask | Why It Matters |
|---|---|---|
| CPU | Does the runner stay below sustained saturation? | Starved browsers miss timing windows |
| Memory | Is there enough RAM for every browser session? | OOM kills look like random failures |
| Shared memory | Is shm_size large enough for Chrome? | Small shared memory causes crashes |
| Data | Does each test own its data? | Shared data breaks parallel isolation |
| App capacity | Can the test environment handle the load? | UI tests can overload small staging systems |
| Artifacts | Are logs separated by test or worker? | Parallel failures need traceability |
Parallelism is valuable only when failure diagnosis remains clear. A 50 minute suite reduced to 12 minutes is good. A 50 minute suite reduced to 8 minutes but failing randomly every day is not a release gate.
Security and Secrets in Dockerized Selenium
Treat test containers as part of your production adjacent security surface. They may contain credentials, tokens, customer like data, internal URLs, and logs. Keep secrets out of Dockerfiles and committed Compose files.
Use environment variables injected by the CI system. Mask sensitive values in logs. Avoid saving screenshots that expose real personal data. If tests run against staging with production like records, define artifact retention rules and access controls.
Also be careful with browser debugging ports. VNC and Selenium endpoints should not be exposed publicly. In CI, keep them on the job network. Locally, bind only what you need.
Common Mistakes When You Run Selenium Tests in Docker
Mistake 1: Using Local ChromeDriver in a Docker Setup
If your test code still calls local driver binaries, you are not really using the browser container. Switch to remote WebDriver and keep the remote URL configurable.
Mistake 2: Forgetting Container Networking
The browser's localhost is not your laptop's localhost. Use Compose service names, host.docker.internal, or a configured network address depending on where the application runs.
Mistake 3: Leaving Browser Images on latest
Unpinned images change without your consent. Pin Selenium image versions, then upgrade intentionally with a small validation run.
Mistake 4: Ignoring Shared Memory
Chrome crashes caused by tiny shared memory are common. Set --shm-size or shm_size in Compose before investigating imaginary Selenium bugs.
Mistake 5: Running Too Much Parallelism
More containers do not automatically mean faster feedback. If the runner is overloaded, tests slow down, timeouts increase, and debugging becomes painful.
Mistake 6: Not Saving Artifacts
Screenshots and logs should be collected even when the container exits. A Docker job without artifacts forces the team to rerun failures blindly.
Mistake 7: Mixing Test Concerns
Do not combine environment setup, test data creation, browser configuration, and assertions in one messy script. Keep the framework layered so failures point to the right cause.
A Practical Rollout Plan
Start small. Pick a stable smoke test that logs in, opens one important page, and asserts a clear result. Run it against a standalone Chrome container locally. Then move the same setup to CI. Add screenshots and logs. Once one test is reliable, add the rest of the smoke suite.
After that, decide whether you need Grid. If the suite is still small, standalone is fine. If runtime is blocking releases, introduce Grid and parallel workers. If cross browser coverage is required, add Firefox nodes with a small targeted suite before running everything everywhere.
Use QABattle to practice turning automation infrastructure into sharper test design. Open the battle arena, choose an automation challenge, and write the checks you would run locally, in Docker, and in CI. The exercise helps you separate product risk from environment setup.
Final Checklist
Before you call the setup done, verify these points:
- Test code uses remote WebDriver.
- Selenium image versions are pinned.
- Browser containers have enough shared memory.
- The app URL is reachable from inside the browser container.
- CI waits for Selenium and the app to be ready.
- Screenshots, logs, reports, and downloads are saved.
- Secrets are injected at runtime, not baked into images.
- Parallel workers have isolated data.
- The same command can run locally and in CI.
Run Selenium tests in Docker because it makes the browser environment reproducible. Keep using good automation engineering because reproducible infrastructure only helps when the tests themselves are clear, isolated, and meaningful.
FAQ
Questions testers ask
Can Selenium tests run inside Docker?
Yes. Selenium tests can run inside Docker by using browser images such as Selenium standalone Chrome, Selenium Grid containers, or a custom test runner image. The usual pattern is to keep the test code in one container and connect it to a browser container over WebDriver.
Is Docker better than installing browsers locally for Selenium?
Docker is usually better for CI and team consistency because browser versions, drivers, operating system libraries, and environment variables can be pinned. Local browsers are still useful for quick debugging, but Docker reduces setup drift across machines and pipelines.
Should Selenium and the test code be in the same container?
For small suites, a single custom image can work. For maintainable pipelines, keep the test runner separate from browser containers or Selenium Grid. This separation makes browser upgrades, parallel execution, logs, and scaling easier to control.
How do I debug Selenium failures in Docker?
Capture screenshots, browser logs, Selenium server logs, and videos when possible. Run the same image locally with the failing command, expose the Grid UI, use VNC images for visual debugging, and save artifacts from the container after every failed test.
Can Docker make Selenium tests less flaky?
Docker can remove environment drift, but it does not fix weak locators, bad waits, shared data, or unstable assertions. It improves the infrastructure layer. You still need explicit waits, isolated test data, reliable selectors, and clear retry policies.
RELATED GUIDES
Continue the route
Selenium vs Playwright vs Cypress in 2026
Compare Selenium vs Playwright vs Cypress in 2026 for speed, browsers, CI, flakiness, ecosystem, and which web automation framework beginners should choose.
Selenium Grid Tutorial: Run Tests Across Browsers
Selenium Grid tutorial explaining architecture, setup, remote WebDriver, browser capabilities, parallel execution, Docker, CI, and debugging tips.
CI/CD for Test Automation with GitHub Actions
Learn CI/CD for test automation with GitHub Actions: Playwright workflows, reports, sharding, and PR vs nightly pipeline strategies that scale.
Test Automation in DevOps: Practical Pipeline Guide
Learn test automation in DevOps with CI/CD stages, quality gates, ownership, flaky test control, metrics, and release-ready workflows for QA.