Docker and Testcontainers
Run browsers in the official Selenium Docker images, compose a Grid with video recording, and use Testcontainers to start a disposable browser per test from Java, Python, .NET or Node.
The official docker-selenium images are the easiest way to get a known browser version with a matching driver, on any machine, with optional VNC and video. Testcontainers goes one step further: it starts and stops those containers from inside your test code, so a test suite carries its own browser infrastructure and needs nothing installed but Docker.
The Official Images
Published to Docker Hub as selenium/* and, since April 2026, mirrored to ghcr.io/seleniumhq/* for when Docker Hub rate limits bite. Tags follow the Selenium version (4.48.0) with date-stamped variants. Chrome, Edge and Firefox images ship for versions from 95 up to the current release, which lets you test against an exact browser build without installing it.
| Image | Purpose |
|---|---|
selenium/standalone-chrome, -firefox, -edge | One container, browser plus Grid; the usual choice for CI and local |
selenium/hub + selenium/node-chrome etc. | Classic hub-and-node topology |
selenium/node-docker | Dynamic Grid: starts a browser container per session |
selenium/video | Sidecar that records a node’s sessions to MP4 |
selenium/standalone-chromium | ARM-friendly Chromium for Apple Silicon and Graviton runners |
Start one locally:
docker run -d --name selenium -p 4444:4444 -p 7900:7900 --shm-size=2g \ -e SE_NODE_MAX_SESSIONS=4 -e SE_VNC_NO_PASSWORD=1 \ selenium/standalone-chrome:4.48.0
# Grid console: http://localhost:4444 noVNC (watch the browser): http://localhost:7900Then point RemoteWebDriver at http://localhost:4444. --shm-size=2g prevents Chrome’s shared-memory crashes; SE_NODE_MAX_SESSIONS sets parallel slots.
Docker Compose With Video
A reproducible local Grid with a Chrome node, a Firefox node, and video recording for each.
services: selenium-hub: image: selenium/hub:4.48.0 ports: ["4442:4442", "4443:4443", "4444:4444"]
chrome: image: selenium/node-chrome:4.48.0 shm_size: 2gb depends_on: [selenium-hub] environment: SE_EVENT_BUS_HOST: selenium-hub SE_EVENT_BUS_PUBLISH_PORT: 4442 SE_EVENT_BUS_SUBSCRIBE_PORT: 4443 SE_NODE_MAX_SESSIONS: 2 SE_SCREEN_WIDTH: 1366 SE_SCREEN_HEIGHT: 768
firefox: image: selenium/node-firefox:4.48.0 shm_size: 2gb depends_on: [selenium-hub] environment: SE_EVENT_BUS_HOST: selenium-hub SE_EVENT_BUS_PUBLISH_PORT: 4442 SE_EVENT_BUS_SUBSCRIBE_PORT: 4443 SE_NODE_MAX_SESSIONS: 2
chrome-video: image: selenium/video:ffmpeg-7.1-20260827 volumes: ["./videos:/videos"] depends_on: [chrome] environment: DISPLAY_CONTAINER_NAME: chrome SE_VIDEO_FILE_NAME: auto # one file per session, named by session id SE_UPLOAD_FAILURE_SESSION_ONLY: "true" # keep only sessions that fired a failure event (4.41+)docker compose up -dGRID_URL=http://localhost:4444 mvn testdocker compose downVideo files land in ./videos. With SE_UPLOAD_FAILURE_SESSION_ONLY, only sessions where your test called fireSessionEvent("test:failed", ...) are kept; see CI/CD Pipelines.
Testcontainers: Browsers From Test Code
Testcontainers wraps Docker in your language. For Selenium it provides a BrowserWebDriverContainer that starts a standalone image, waits for it to be ready, exposes the Grid URL, and optionally records video. When the test ends, the container is removed. No docker compose, no ports to manage, and every developer and CI job runs the identical browser.
// build.gradle: testImplementation "org.testcontainers:selenium:1.21.0", "org.testcontainers:junit-jupiter:1.21.0"import org.testcontainers.containers.BrowserWebDriverContainer;import org.testcontainers.containers.VncRecordingContainer.VncRecordingFormat;import org.testcontainers.junit.jupiter.Container;import org.testcontainers.junit.jupiter.Testcontainers;
@Testcontainersclass CheckoutIT {
@Container static BrowserWebDriverContainer<?> browser = new BrowserWebDriverContainer<>("selenium/standalone-chrome:4.48.0") .withCapabilities(new ChromeOptions()) .withRecordingMode(BrowserWebDriverContainer.VncRecordingMode.RECORD_FAILING, new File("target/videos"), VncRecordingFormat.MP4) .withSharedMemorySize(2L * 1024 * 1024 * 1024);
private RemoteWebDriver driver;
@BeforeEach void setUp() { driver = new RemoteWebDriver(browser.getSeleniumAddress(), new ChromeOptions()); driver.setFileDetector(new LocalFileDetector()); }
@AfterEach void tearDown() { driver.quit(); }
@Test void placesOrder() { // The app under test must be reachable from inside the container: // use host.testcontainers.internal instead of localhost driver.get("http://host.testcontainers.internal:3000/checkout"); // ... }}# pip install testcontainers[selenium]import pytestfrom testcontainers.selenium import BrowserWebDriverContainerfrom selenium.webdriver import ChromeOptions
@pytest.fixture(scope="session")def browser_container(): with BrowserWebDriverContainer(ChromeOptions(), image="selenium/standalone-chrome:4.48.0") as container: yield container
@pytest.fixturedef driver(browser_container): d = browser_container.get_driver() # a RemoteWebDriver pointed at the container yield d d.quit()
def test_places_order(driver): # Reach the host from inside Docker driver.get("http://host.docker.internal:3000/checkout") ...// npm i -D testcontainersconst { GenericContainer, Wait } = require('testcontainers');const { Builder } = require('selenium-webdriver');
describe('checkout', function () {this.timeout(120000);let container, driver;
before(async () => { container = await new GenericContainer('selenium/standalone-chrome:4.48.0') .withExposedPorts(4444) .withSharedMemorySize(2 * 1024 * 1024 * 1024) .withWaitStrategy(Wait.forHttp('/status', 4444).forResponsePredicate((b) => b.includes('"ready": true'))) .start();});after(async () => { await container.stop(); });
beforeEach(async () => { const url = `http://${container.getHost()}:${container.getMappedPort(4444)}`; driver = await new Builder().usingServer(url).forBrowser('chrome').build();});afterEach(async () => { await driver.quit(); });
it('places an order', async () => { await driver.get('http://host.docker.internal:3000/checkout');});});// NuGet: Testcontainersusing DotNet.Testcontainers.Builders;using DotNet.Testcontainers.Containers;
[TestFixture]public class CheckoutTests{ private IContainer _container = null!; private IWebDriver _driver = null!;
[OneTimeSetUp] public async Task StartBrowser() { _container = new ContainerBuilder() .WithImage("selenium/standalone-chrome:4.48.0") .WithPortBinding(4444, true) .WithEnvironment("SE_NODE_MAX_SESSIONS", "2") .WithWaitStrategy(Wait.ForUnixContainer().UntilHttpRequestIsSucceeded(r => r.ForPort(4444).ForPath("/status"))) .Build(); await _container.StartAsync(); }
[OneTimeTearDown] public async Task StopBrowser() => await _container.DisposeAsync();
[SetUp] public void SetUp() { var url = new Uri($"http://{_container.Hostname}:{_container.GetMappedPublicPort(4444)}"); _driver = new RemoteWebDriver(url, new ChromeOptions()); }
[TearDown] public void TearDown() => _driver.Quit();
[Test] public void PlacesOrder() => _driver.Navigate().GoToUrl("http://host.docker.internal:3000/checkout");}Testcontainers Java’s Selenium module is the most complete (video recording modes, automatic host exposure). In other languages you use the generic container and a wait strategy on /status.
Reaching Your App From the Container
The browser runs inside Docker, so localhost means the container. Options:
- Testcontainers host aliasing: Java exposes
host.testcontainers.internalviaTestcontainers.exposeHostPorts(3000); Docker Desktop provideshost.docker.internaleverywhere. - Run the app in a container too, on the same Docker network, and use its service name.
- On Linux CI, add
--add-host=host.docker.internal:host-gatewayto the container.
Uploads and Downloads Through Containers
File uploads need LocalFileDetector on the RemoteWebDriver so the file is transferred to the browser container. Downloads land inside the container; either enable Grid’s managed downloads (SE_NODE_ENABLE_MANAGED_DOWNLOADS=true and the se:downloadsEnabled capability, then fetch via the /session/{id}/se/files endpoint), or mount a volume and read from it. Selenium 4.48 extended managed file transfer to Kubernetes and relay sessions.
Building Your Own Image
When you need extra fonts, a corporate CA certificate, or a specific extension, extend the official image rather than starting from scratch:
FROM selenium/standalone-chrome:4.48.0USER rootRUN apt-get update && apt-get install -y --no-install-recommends fonts-noto-cjk \ && rm -rf /var/lib/apt/lists/*COPY corp-root-ca.crt /usr/local/share/ca-certificates/RUN update-ca-certificatesUSER 1200Summary
- The official images give you exact browser versions, VNC and video with one
docker run; use--shm-size=2g. - Compose a Grid with video for local debugging; keep only failed-session videos with the 4.41 session event flag.
- Testcontainers starts browsers from test code, making the suite self-contained across laptops and CI.
- Remember the browser is inside Docker: use host aliases for your app,
LocalFileDetectorfor uploads, managed downloads for files.