Skip to main content
Back to Blog
Guide
2026-05-05

Testcontainers Selenium Grid — Complete Guide 2026

Master Testcontainers for Selenium Grid. Run browser tests in Docker with Chrome, Firefox, video recording, and CI/CD patterns.

Testcontainers Selenium Grid Complete Guide

Running Selenium browser tests reliably across different machines, operating systems, and browser versions has been a perennial pain point for QA teams. Local Chrome installs drift, Mac M-series chips break older selenium-stand-alone JARs, CI runners may not have a display server, and developers waste hours debugging "works on my machine" failures. Testcontainers solves this by running Selenium browsers in Docker containers, programmatically managed by your test runner, with built-in video recording, a real Chrome or Firefox per test, and one-line setup.

This guide is a hands-on walkthrough of Testcontainers with Selenium for Java in 2026. We cover the BrowserWebDriverContainer module, Chrome and Firefox setup, video recording configuration, headless vs headed mode, network handling for app-under-test access, container reuse, and CI/CD configuration. Every code sample is working Java with JUnit 5 and Selenium 4.


Key Takeaways

  • BrowserWebDriverContainer provides one-line setup for real Chrome or Firefox in Docker
  • VNC video recording captures every test run automatically for debugging
  • Network containers let your test app talk to Selenium without exposing ports
  • Compatible with Selenium 4 WebDriver API and modern locators
  • CI/CD setup is trivial — no need to install browsers on CI runners

Why Use Testcontainers for Selenium

Local Selenium has three big problems. First, browser drift: your local Chrome updates and breaks tests overnight. Second, CI complexity: every CI runner needs Chrome installed, plus the correct ChromeDriver version, plus xvfb if running headless. Third, "works on my machine": developers find different bugs depending on their local browser version.

Testcontainers fixes all three. The browser version is pinned to a specific image tag. CI doesn't need any browser-related setup beyond Docker. Every developer and every CI run gets exactly the same browser.


Installation

Maven:

<dependency>
  <groupId>org.testcontainers</groupId>
  <artifactId>junit-jupiter</artifactId>
  <version>1.20.4</version>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.testcontainers</groupId>
  <artifactId>selenium</artifactId>
  <version>1.20.4</version>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.seleniumhq.selenium</groupId>
  <artifactId>selenium-java</artifactId>
  <version>4.27.0</version>
  <scope>test</scope>
</dependency>

Verify Docker.


Your First Test

import org.junit.jupiter.api.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.testcontainers.containers.BrowserWebDriverContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

import static org.junit.jupiter.api.Assertions.assertTrue;

@Testcontainers
class GoogleSearchTest {

    @Container
    static BrowserWebDriverContainer<?> CHROME = new BrowserWebDriverContainer<>()
        .withCapabilities(new ChromeOptions());

    @Test
    void searchesGoogle() {
        WebDriver driver = new RemoteWebDriver(CHROME.getSeleniumAddress(), new ChromeOptions());
        try {
            driver.get("https://www.google.com");
            assertTrue(driver.getTitle().contains("Google"));
        } finally {
            driver.quit();
        }
    }
}

The first run pulls the selenium/standalone-chrome image (about 1 GB). Subsequent runs are fast.


BrowserWebDriverContainer API Reference

MethodPurpose
Constructor (no-arg)Default image with latest Chrome
Constructor with imageCustom image like selenium/standalone-firefox:4.27.0
.withCapabilities(options)Pass ChromeOptions or FirefoxOptions
.withRecordingMode(mode, dir)Record video of test runs
.withReuse(true)Reuse container across runs
.withNetwork(network)Join custom network

After start:

MethodReturns
getSeleniumAddress()RemoteWebDriver URL
getHost()Hostname
getMappedPort(4444)Selenium port
getMappedPort(5900)VNC port for live viewing

Video Recording

This is the killer feature. Record every test, save failures:

import org.testcontainers.containers.BrowserWebDriverContainer.VncRecordingMode;
import java.io.File;

@Container
static BrowserWebDriverContainer<?> CHROME = new BrowserWebDriverContainer<>()
    .withCapabilities(new ChromeOptions())
    .withRecordingMode(VncRecordingMode.RECORD_FAILING, new File("./videos"));

Modes:

ModeBehavior
RECORD_ALLRecord every test
RECORD_FAILINGOnly record tests that fail
SKIPNo recording (default)

Videos are saved as FLV format with timestamps in filenames.


Firefox Setup

import org.openqa.selenium.firefox.FirefoxOptions;

@Container
static BrowserWebDriverContainer<?> FIREFOX = new BrowserWebDriverContainer<>(
    DockerImageName.parse("selenium/standalone-firefox:4.27.0")
).withCapabilities(new FirefoxOptions());

The two-argument constructor takes a specific image tag.


Testing Your App: Network Bridge Pattern

When your application runs in another container or on the host, Selenium needs to reach it. The cleanest pattern is to put both on a shared network:

import org.testcontainers.containers.Network;

static Network network = Network.newNetwork();

@Container
static GenericContainer<?> APP = new GenericContainer<>("my-app:latest")
    .withNetwork(network)
    .withNetworkAliases("app")
    .withExposedPorts(8080);

@Container
static BrowserWebDriverContainer<?> CHROME = new BrowserWebDriverContainer<>()
    .withCapabilities(new ChromeOptions())
    .withNetwork(network);

@Test
void testsApp() {
    WebDriver driver = new RemoteWebDriver(CHROME.getSeleniumAddress(), new ChromeOptions());
    driver.get("http://app:8080");  // Selenium reaches app via network alias
}

For Spring Boot tests with @LocalServerPort, use Testcontainers' Testcontainers.exposeHostPorts:

@LocalServerPort
private int port;

@BeforeEach
void setup() {
    org.testcontainers.Testcontainers.exposeHostPorts(port);
}

@Test
void test() {
    driver.get("http://host.testcontainers.internal:" + port);
}

host.testcontainers.internal is a special hostname that Selenium can use to reach the host.


Live Debugging with VNC

Connect a VNC viewer to watch tests run in real time:

System.out.println("VNC: vnc://localhost:" + CHROME.getMappedPort(5900));

Use a VNC client like RealVNC or Remmina. Useful for debugging flaky tests.


Per-Test vs Per-Class Container

By default, with @Container static, the container is shared across all tests in the class. To get a fresh browser per test:

@Container
BrowserWebDriverContainer<?> chrome = new BrowserWebDriverContainer<>();

(Drop the static keyword.) This is slower but provides full isolation. For most tests, per-class is fine because each test can quit and recreate the WebDriver.


Selenide Integration

Selenide users can configure remote URL:

import com.codeborne.selenide.Configuration;

@BeforeAll
static void setup() {
    Configuration.remote = CHROME.getSeleniumAddress().toString();
    Configuration.browser = "chrome";
}

Selenide then uses the Testcontainers Chrome for all open() calls.


Headless vs Headed

Testcontainers Chrome runs headed inside the container (with a virtual display). You can also force headless:

ChromeOptions options = new ChromeOptions();
options.addArguments("--headless=new");

@Container
static BrowserWebDriverContainer<?> CHROME = new BrowserWebDriverContainer<>()
    .withCapabilities(options);

Headed is slightly slower but matches production user behavior more closely.


Container Reuse

@Container
static BrowserWebDriverContainer<?> CHROME = new BrowserWebDriverContainer<>()
    .withCapabilities(new ChromeOptions())
    .withReuse(true);

Enable in ~/.testcontainers.properties:

testcontainers.reuse.enable=true

CI/CD Configuration

name: test
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: 21
      - run: ./mvnw test
      - uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: selenium-videos
          path: videos/

The upload-artifact step captures recorded videos when tests fail.


Common Pitfalls

Stale WebDriver references. Each test should create a fresh WebDriver and quit it in @AfterEach. Reusing WebDriver across tests causes state leakage.

Window size. Containers default to 1360x1020. Tests for mobile layouts need explicit window resizing.

Network connectivity. If your app is on the host, use host.testcontainers.internal not localhost.

Image size. Selenium images are 1+ GB. Cache aggressively in CI.

Browser version drift. Pin to specific tags like selenium/standalone-chrome:4.27.0 not latest.


Conclusion

Testcontainers with Selenium fixes the worst pain points of browser testing. Pinned browser versions, no CI installation overhead, automatic video recording, easy network setup, and consistent behavior across machines. For Java teams running browser tests in 2026, this is the right default.

Explore the QA skills directory for related browser automation patterns, or read our Selenide vs Selenium guide for an alternative Java browser automation library.

Testcontainers Selenium Grid — Complete Guide 2026 | QASkills.sh