Skip to main content
SeleniumDecoded

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.

Selenium 4 Stable Updated 9 Sept 2026 · Verified against Selenium 4.48.0

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.

ImagePurpose
selenium/standalone-chrome, -firefox, -edgeOne container, browser plus Grid; the usual choice for CI and local
selenium/hub + selenium/node-chrome etc.Classic hub-and-node topology
selenium/node-dockerDynamic Grid: starts a browser container per session
selenium/videoSidecar that records a node’s sessions to MP4
selenium/standalone-chromiumARM-friendly Chromium for Apple Silicon and Graviton runners

Start one locally:

Terminal window
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:7900

Then 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.

docker-compose.yml
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+)
Terminal window
docker compose up -d
GRID_URL=http://localhost:4444 mvn test
docker compose down

Video 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.

A disposable browser per test class
Selenium 4 Stable
// 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;
@Testcontainers
class 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 pytest
from testcontainers.selenium import BrowserWebDriverContainer
from 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.fixture
def 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 testcontainers
const { 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: Testcontainers
using 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.internal via Testcontainers.exposeHostPorts(3000); Docker Desktop provides host.docker.internal everywhere.
  • 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-gateway to 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.0
USER root
RUN 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-certificates
USER 1200

Summary

  • 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, LocalFileDetector for uploads, managed downloads for files.

Related lessons