July 27, 2026
How to Run Playwright Tests in Docker
Learn how to run Playwright tests in Docker, install browser dependencies, structure a Playwright container, and avoid common CI failures with browser tests.
Running browser tests in containers sounds simple until the first CI job fails with a missing library, a sandbox error, or a browser that works on one runner image and crashes on another. If you want to run Playwright tests in Docker reliably, the real task is not just “put tests in a container.” It is understanding browser dependencies, image choice, runtime permissions, and how your CI environment handles storage, fonts, and network isolation.
This guide is a practical walkthrough for developers, SDETs, and DevOps engineers who need Docker browser tests that are repeatable, debuggable, and maintainable. I will show the common setup patterns, why Playwright’s official Docker images exist, when a custom Playwright container makes sense, and which failure modes show up most often in production pipelines.
Why run Playwright tests in Docker at all
Containerizing browser tests gives you a controlled environment. That matters because browser automation failures often come from the environment, not the test logic.
A container helps you:
- pin the browser and OS dependencies,
- make local and CI runs closer to each other,
- avoid “works on my machine” differences,
- isolate test tooling from the rest of the build system,
- standardize execution across teams and pipelines.
The tradeoff is that a container adds another layer to debug. If you do not need it, do not add it. If you do need it, treat the container as part of the test system, not as a throwaway wrapper.
Playwright itself documents Docker support and publishes images designed for browser execution, which is the safest starting point for most teams, especially in CI. See the official Playwright intro and the Docker documentation for the underlying platform model.
A common mistake is to use a generic Node image, install browsers manually, then treat the resulting failures as “Playwright problems.” In practice, this usually becomes an OS dependency problem.
The simplest working setup
If your goal is to run Playwright tests in Docker with the least friction, start with Playwright’s official image. It already includes browser binaries and the system dependencies needed by those browsers.
A minimal project structure looks like this:
text . ├── Dockerfile ├── package.json ├── package-lock.json ├── playwright.config.ts └── tests └── example.spec.ts
A basic test file might look like this:
import { test, expect } from '@playwright/test';
test('homepage loads', async ({ page }) => {
await page.goto('https://example.com');
await expect(page).toHaveTitle(/Example/);
});
Using the official Playwright Docker image
The simplest Dockerfile often uses the official image as a base:
dockerfile FROM mcr.microsoft.com/playwright:v1.55.0-jammy
WORKDIR /app COPY package*.json ./ RUN npm ci COPY . .
CMD [“npx”, “playwright”, “test”]
A few details matter here:
mcr.microsoft.com/playwrightgives you the browser dependencies Playwright expects.npm ciis better thannpm installin CI because it uses the lockfile exactly.- copy
package*.jsonfirst so Docker can reuse cached layers when only test files change. CMDshould be explicit so the container runs tests by default.
Build and run it like this:
docker build -t playwright-tests .
docker run --rm playwright-tests
If your tests need environment variables, pass them through at runtime:
docker run --rm \
-e BASE_URL=https://staging.example.com \
-e CI=true \
playwright-tests
Why official images are usually the right default
Browser automation inside a container is mostly a dependency management problem. The official images solve the hardest part: they bundle the browser binaries and the OS libraries those browsers need.
That avoids several failure modes:
- missing
libnss3,libatk,libgbm, or similar runtime libraries, - mismatched browser versions after a base-image update,
- inconsistent behavior between developers and CI runners,
- fragile setup scripts that break when package repositories change.
If your team owns a custom container, someone also owns the upgrade path. Browser images age quickly enough that unattended maintenance turns into an operational task, not a one-time setup.
Running the Playwright test runner inside Docker
In most cases, you want to run the Playwright test runner inside the container, not on the host with only browsers in Docker. That keeps the Node version, dependencies, and browser runtime together.
A playwright.config.ts might include a project definition like this:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({ testDir: ‘./tests’, retries: process.env.CI ? 2 : 0, use: { baseURL: process.env.BASE_URL || ‘http://localhost:3000’, trace: ‘on-first-retry’, }, projects: [ { name: ‘chromium’, use: { …devices[‘Desktop Chrome’] } }, ], });
A few practical choices are visible here:
retriesin CI helps expose flaky tests without hiding them completely.trace: 'on-first-retry'gives debugging artifacts without paying the cost on every pass.baseURLfrom an environment variable lets the same container run locally or in CI.
Docker Compose for application plus tests
Many teams want more than a test container. They want the app under test and the browser test runner to start together, especially for integration or end-to-end checks.
Docker Compose is a good fit when you need multiple services:
services:
web:
build: .
ports:
- "3000:3000"
tests: build: context: . dockerfile: Dockerfile depends_on: - web environment: BASE_URL: http://web:3000 command: npx playwright test
This is useful, but do not confuse depends_on with readiness. It only starts containers in order. It does not guarantee your app is ready to serve traffic.
A better pattern is to wait for the app to become healthy before launching tests. For example, use a health check in the app container or a small wait script in the test container.
If the app is not ready, browser tests fail for the wrong reason. That is not test coverage, that is a startup race.
Handling browser dependencies correctly
This is the part that usually breaks first when teams move from a local machine to Docker.
Playwright browsers need system packages beyond Node.js. If you build from a generic image, you may need to install those dependencies yourself. That is possible, but it is easy to get wrong and hard to maintain.
If you absolutely need a custom image, use a known-good base and install only what you must:
dockerfile FROM node:22-bookworm
WORKDIR /app COPY package*.json ./ RUN npm ci RUN npx playwright install –with-deps chromium COPY . . CMD [“npx”, “playwright”, “test”]
This works, but there is a tradeoff:
- you gain control over the base image,
- you also take on responsibility for browser dependencies and updates.
For many teams, the official Playwright image is the right balance. Use a custom image only when you have a clear reason, such as base-image standards, enterprise OS requirements, or a need to align with a broader build platform.
Common Docker browser test failures and what they usually mean
1. Sandbox or permission errors
A frequent issue is Chromium sandbox failures in containers that run with restrictive permissions. Depending on your runtime and security posture, you may need to adjust user permissions or container flags.
The first step is not to disable security blindly. Check whether the image, container user, and CI runner allow the browser process to start as intended.
2. Missing shared libraries
If a browser crashes immediately or fails to launch, missing libraries are a common cause. This is why generic images often become a maintenance trap. The dependency gap only shows up when the browser starts.
3. Fonts and rendering differences
Visual assertions and text rendering can vary if the container lacks fonts your app expects. This can affect screenshots, layout measurements, and accessibility checks.
If you test localized content or UI components with specific fonts, install the required fonts in the image and keep them versioned.
4. Storage and workspace issues
CI runners sometimes give containers small ephemeral disks. Traces, videos, screenshots, and browser caches can consume space quickly, especially on retry-heavy suites.
If your test output is large, configure artifact retention deliberately. Keep only what helps debug failures.
5. Network assumptions
Containerized tests often run on isolated networks. Hardcoded localhost assumptions, DNS issues, or external API dependencies can create failures that do not happen on a developer machine.
When possible, stub external services, use test doubles, or isolate third-party integrations behind contract tests rather than full browser flows.
A practical GitHub Actions example
Here is a compact CI job that builds the image and runs tests inside it:
name: playwright
on: push: pull_request:
jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: docker/setup-buildx-action@v3 - run: docker build -t playwright-tests . - run: docker run –rm -e CI=true playwright-tests
This is enough for many teams. If you need artifacts, mount a volume or copy them out after the run.
For example, if traces are stored in playwright-report, you might mount a directory from the host:
docker run --rm \
-e CI=true \
-v "$PWD/playwright-report:/app/playwright-report" \
playwright-tests
Keeping tests debuggable in a container
Running tests in Docker should not turn your suite into a black box. The container needs to be easy to inspect.
A few practices help:
- emit Playwright traces on retries,
- save screenshots only when needed,
- mount reports to the host in CI,
- keep logs from app startup and test startup separate,
- avoid over-abstracting the Dockerfile.
When a test fails in CI, you want enough context to answer three questions quickly:
- Did the app start?
- Did the browser launch?
- Did the assertion fail, or did the environment fail first?
If those answers are not obvious from logs and artifacts, the pipeline will waste engineering time.
When a custom Playwright container is justified
Not every team should use the official image forever. A custom container can be justified when:
- your organization requires a standard base image,
- you need additional system libraries or fonts,
- you are co-locating test execution with a broader internal runtime,
- you want to cache dependencies in a specific way for large suites.
The key is to treat the container like production infrastructure. Version it, review it, and update it on purpose. Browser images and OS packages drift. If no one owns that drift, flaky tests will eventually own your time.
When Docker is the wrong abstraction
Docker is useful, but it is not the only way to run browser automation. If your team spends more time maintaining containers than writing useful tests, that is a signal.
Some teams should move to a managed test platform instead of carrying browser images, Playwright dependencies, and CI setup themselves. For example, Endtest vs Playwright is worth a look if your team wants browser coverage without owning a framework, browser containers, or the surrounding setup. Endtest is a managed platform with agentic AI and low-code workflows, so the tradeoff is less infrastructure ownership and more reliance on the platform’s model.
That does not make Docker wrong. It means the cost profile matters. If your team wants custom code and total control, Playwright in Docker is reasonable. If your team wants less maintenance and human-readable test steps instead of framework code, a managed option can be the more operationally sane choice.
A decision checklist for teams
Use this checklist before standardizing on a Playwright container:
- Do we need browser tests in CI, or only locally?
- Can we accept owning browser and OS dependency updates?
- Do we need the official Playwright image, or a custom base image?
- Who will debug sandbox, font, and library issues when they happen?
- Where will traces, screenshots, and videos live?
- Are we testing app behavior, or just verifying that the container starts?
- Would a managed platform reduce ownership cost for this team?
If you cannot answer the ownership questions clearly, the setup will accumulate friction.
A concise recommendation
If you need to run Playwright tests in Docker, start with the official Playwright image, keep the Dockerfile small, and let the test runner own the browser execution. Add Compose only when you need multiple services. Avoid custom images until a concrete requirement forces them.
The practical goal is not containerization for its own sake. The goal is a test system that behaves the same way on every run, fails for understandable reasons, and does not consume too much engineering time to keep alive.
If your team would rather not maintain browser containers and Playwright dependencies at all, evaluate whether a managed platform fits better than a self-hosted test stack. That decision is not about ideology, it is about who owns the operational burden.