Playwright in Docker: A Complete Containerization Guide
Tired of browser drift between laptops and CI? Learn how to containerize Playwright with Docker, from image tags to sharding.
A test that passes on your laptop and fails on the build server is rarely a bug in the test. It is usually a browser or library that differs between the two machines, and a Playwright Docker image freezes both inside one box.
The pain shows up when that box is used casually. A wrong image tag or a missing memory flag turns a stable suite into a source of flaky tests that have nothing to do with the app.
This guide covers the official image, a production Dockerfile, Compose, CI wiring, security, image size, and the errors people actually hit, all checked against the Playwright and Docker docs before it reaches your test automation setup.
Why teams run Playwright in Docker
Playwright needs three browser engines plus dozens of operating system libraries to render pages. On a developer machine those pieces drift slowly. On CI they drift every time the runner image updates. A container freezes all of it at a known state.
What the container actually fixes
The official Playwright documentation describes the image as including the Playwright browsers and the browser system dependencies, with the Playwright package itself installed separately by you. That split is deliberate. The image owns the heavy, slow-changing layer. Your project owns the fast-changing test code.
Three practical wins follow from that split:
- Identical rendering everywhere. Screenshot and visual testing baselines stop drifting between macOS laptops and Linux runners, because every run uses the same Linux build of each browser.
- No dependency install step in CI. The system libraries are already in the image, so jobs skip the install --with-deps step entirely.
- A reproducible bug report. When a test fails, anyone can pull the same tag and get the same environment, which shortens the loop on browser compatibility issues.
Where Docker does not help
Containers do not make a slow suite fast, and they do not fix a locator that depends on timing. They also add a pull step. The compressed image is close to a gigabyte, which matters if your CI runners start cold on every job. Later sections cover how to keep that cost down.
Docker is also the most common container tool by a wide margin, which is why the rest of this guide assumes it. The Stack Overflow Developer Survey 2025 asked developers which cloud and container tools they used in the past year.

Among all respondents in that section, Docker was reported by 71.1%, while Kubernetes came in at 28.5% and Podman at 11.1%. With that much of the industry already on Docker, the natural starting point is the image Microsoft publishes for exactly this job.
Understanding the official Playwright Docker image
The Playwright team publishes images to the Microsoft Artifact Registry under mcr.microsoft.com/playwright for Node.js, with separate images for Python, Java, and .NET. The Dockerfile that builds the Node.js image is public in the Playwright repository, so you can see precisely what you are pulling.
What is inside the image
Reading the noble Dockerfile from the Playwright repository, the image is built from ubuntu:noble and installs:
- Node.js 24 from NodeSource, plus npm and Yarn.
- Git, an OpenSSH client, and zstd, which the Dockerfile notes is used by the GitHub Actions cache action.
- A non-root user named pwuser.
- Chromium, Firefox, and WebKit under /ms-playwright, set through the PLAYWRIGHT_BROWSERS_PATH variable, with each browser in its own layer so pulls can run in parallel.
The Chromium layer also installs the headless shell and ffmpeg, which Playwright uses for video recording. The browser versions bundled with each release are listed in the release notes, and the Playwright 1.63 release notes are a good place to see which Chromium, Firefox, and WebKit builds you are getting.
Image tags: noble, jammy, and resolute
Every release ships with a set of tags. The Playwright docs list the current ones, and the compressed amd64 sizes below come straight from the image manifests on the registry, retrieved on 10 September 2026.
| Tag | Ubuntu base | Compressed size (amd64) |
|---|---|---|
| v1.63.0 or v1.63.0-noble | Ubuntu 24.04 LTS (Noble Numbat) | 912 MB |
| v1.63.0-jammy | Ubuntu 22.04 LTS (Jammy Jellyfish) | 862 MB |
| v1.63.0-resolute | Ubuntu 26.04 LTS (Resolute Raccoon) | 956 MB |
The bare version tag with no suffix resolves to the noble image, according to the Playwright docs. Ubuntu 20.04 (focal) is no longer supported as of Playwright 1.63, and Alpine is not supported at all, because the Firefox and WebKit builds require glibc rather than musl.
Why the version must match your package
This is the rule that causes the most confusion. The Playwright docs state that if the Playwright version in your Docker image does not match the version in your project, Playwright will be unable to locate browser executables.
The browsers inside the image are the exact builds for that release, and a different release looks for different build numbers. The Playwright architecture separates the client library from the browser binaries, so the library asks for one revision and the image holds one.
Pin both to the same number and the problem disappears, which is exactly what the Dockerfile in the next section does.
How to run Playwright tests in Docker step by step
Here is the shortest reliable path from a working local suite to the same suite running inside a container. The steps assume a Node.js project created with the standard Playwright framework setup.
- Check your installed version with npx playwright --version and note the number.
- Create a Dockerfile that starts from the official image with the same version tag.
- Copy package.json and the lockfile first, run npm ci, then copy the rest of the project.
- Add a .dockerignore so node_modules, reports, and traces stay out of the build context.
- Build the image with docker build.
- Run it with --init and --ipc=host, and mount a volume for the report folder.
- Open the report on the host with npx playwright show-report.

A production-ready Playwright Dockerfile
The layer order below follows the Docker build cache guidance: expensive, rarely changing steps first, frequently changing steps last. Test edits never trigger a fresh npm ci.
FROM mcr.microsoft.com/playwright:v1.63.0-noble
WORKDIR /app
# Install dependencies in their own cached layer
COPY package.json package-lock.json ./
RUN npm ci
# Copy the rest of the project (filtered by .dockerignore)
COPY . .
# Never wait on an interactive prompt inside CI
ENV CI=true
CMD ["npx", "playwright", "test"]
node_modules
playwright-report
test-results
blob-report
.git
Because the image already contains the browsers at /ms-playwright, you do not run npx playwright install in the Dockerfile. If you did, it would download a second copy for no benefit, since the version in package-lock.json matches the tag.
Build and run the container
docker build -t pw-tests .
docker run --rm --init --ipc=host \
-v "$(pwd)/playwright-report:/app/playwright-report" \
-v "$(pwd)/test-results:/app/test-results" \
pw-tests
The two volume mounts are what make the run useful. Without them, the HTML reporter output disappears when the container exits. With them, you can run npx playwright show-report on the host as if the tests had run locally.
Tip: The Playwright docs recommend --ipc=host because Chromium can run out of memory and crash without it. Docker's default /dev/shm is only 64 MB, and sharing the host IPC namespace lifts that limit. If host IPC is not allowed in your environment, --shm-size=1g is the usual fallback.
Passing options at run time
You will often want to run a subset of tests, a single project, or a different worker count without rebuilding. Anything after the image name replaces the default command.
docker run --rm --init --ipc=host pw-tests \
npx playwright test tests/checkout --project=chromium --workers=4
If you are not sure which workers, retries, and reporters make sense for a container with limited CPU, the Config Generator among TestDino's free tools produces a ready playwright.config.ts from a few choices.
One container running your test runner is the common case. The next step is putting the application under test in a container beside it.
Playwright Docker Compose: testing your app in containers
Most real suites need a running application, and often a database behind it. Docker Compose lets you describe the app, its dependencies, and the test runner as services on one private network, then start and stop them together.
A compose file with the app under test
The file below starts a web application, waits until its health check passes, then runs the tests against it by service name. The init and ipc keys map to the same flags used earlier, and the Compose reference documents init as running a process that forwards signals and reaps processes.
services:
web:
build: ./app
ports:
- "3000:3000"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 5s
timeout: 3s
retries: 20
tests:
build: .
init: true
ipc: host
depends_on:
web:
condition: service_healthy
environment:
CI: "true"
BASE_URL: "http://web:3000"
volumes:
- ./playwright-report:/app/playwright-report
- ./test-results:/app/test-results
The depends_on condition is documented in the Compose reference as waiting until the dependency is reported healthy before the dependent service starts. That removes the sleep-and-hope scripts that many pipelines still carry.
Point the config at the container hostname
Inside the Compose network, the app is reachable as web, not localhost. Reading the base URL from the environment keeps one config working on a laptop, in Compose, and in CI, which is one of the simpler Playwright best practices to adopt.
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
baseURL: process.env.BASE_URL ?? 'http://localhost:3000',
trace: 'on-first-retry',
},
reporter: [['html', { open: 'never' }], ['list']],
});
Run everything with a single command and let the exit code of the test service decide the result:
docker compose up --build --abort-on-container-exit --exit-code-from tests
If your suite seeds data before the run, a Playwright global setup file can talk to the database service the same way, by hostname over the Compose network. With local and Compose runs in place, the same image becomes the foundation of the CI job.
Running Playwright Docker images in CI pipelines
The official CI guide uses the same image on every major platform. The pattern is always the same: run the job inside the container, check out the code, install the project, and run the tests. No browser install step appears anywhere.
Playwright Docker GitHub Actions job
GitHub Actions can run an entire job inside a container using the container key. The Playwright docs pair it with --user 1001, which matches the runner's default user so that files created during checkout stay writable.
name: Playwright Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
playwright:
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.63.0-noble
options: --user 1001 --init --ipc=host
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: lts/*
- name: Install dependencies
run: npm ci
- name: Run Playwright tests
run: npx playwright test
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 14
GitHub's documentation notes that the --network and --entrypoint options are not supported in the container options string, but the flags above are fine. The full walkthrough of Playwright in GitHub Actions covers caching, matrices, and PR annotations on top of this base.
Note: The Playwright docs say caching browser binaries in CI is not recommended, because restoring the cache takes about as long as downloading them. With a Playwright Docker image the question goes away entirely: the browsers arrive with the image, and the only cache worth keeping is the npm cache.
GitLab CI and sharding across containers
GitLab treats the image as the job environment, and its parallel keyword hands each job an index. The Playwright docs show exactly this pattern for sharding.
stages:
- test
tests:
stage: test
image: mcr.microsoft.com/playwright:v1.63.0-noble
parallel: 4
script:
- npm ci
- npx playwright test --shard=$CI_NODE_INDEX/$CI_NODE_TOTAL
artifacts:
when: always
paths:
- blob-report/
Each shard runs in its own container and writes a blob report. A follow-up job merges them with npx playwright merge-reports, which the Playwright sharding guide walks through end to end. If you are on GitLab, the Playwright in GitLab CI guide covers runners and artifact retention in more depth.
Azure Pipelines and everything else
Azure Pipelines accepts the same image through its top-level container key, as shown in the Playwright CI docs, and the Playwright tests in Azure guide has a full pipeline. Jenkins, CircleCI, and TeamCity all support Docker executors and follow the same shape.
Whatever the platform, treat the report artifacts as first-class outputs. Uploading the HTML report and traces on every run, not only on failure, is what makes Playwright CI reports useful for spotting slow drift before it becomes a red build. Once the job is stable, the next question is who the browser runs as.
Security, users, and the Chromium sandbox
By default the official image runs as root, and the Playwright docs are explicit about the consequence: running as root disables the Chromium sandbox, which is not available for that user. For end-to-end tests against your own application, the docs say root is acceptable because you trust the code the browser runs.
When to switch to pwuser
The picture changes for crawling or scraping untrusted sites. For that case the docs recommend two things together: run as the pwuser account that the image already creates, and apply the seccomp profile that ships in the Playwright repository.
That profile is Docker's default profile plus permission for the clone, setns, and unshare syscalls, which Chromium needs to create its sandbox namespaces.
docker run -it --rm --init --ipc=host \
--user pwuser \
--security-opt seccomp=seccomp_profile.json \
mcr.microsoft.com/playwright:v1.63.0-noble /bin/bash
Two details trip people up here. First, files the container writes are owned by pwuser, so mounted report folders must be writable by that user. Second, if you switch users in your own Dockerfile, do it after npm ci so the install step keeps its root permissions.
The SYS_ADMIN escape hatch
The docs list one more option for local development: if Chromium produces strange launch errors, try docker run --cap-add=SYS_ADMIN. Treat it as a diagnostic for your laptop, not a CI setting.
Granting that capability broadly is one of the Playwright mistakes that reviewers should flag, because it hands the container far more power than the sandbox needs.
Teams that use AI coding agents can encode these rules once instead of re-explaining them in every review. The playwright-skill repository bundles CI and Docker guidance in a format that Claude Code, Cursor, and similar tools load automatically. With the security posture settled, the remaining cost is size and speed.
Keeping the image small and builds fast
Nearly a gigabyte of compressed download per fresh runner adds up. There is no magic that shrinks three browser engines, but there are choices that remove the parts you do not use and avoid pulling what you already have.

The measurements above are the sum of compressed layer sizes in each image manifest on the Microsoft registry. The jammy image is the smallest of the three Ubuntu bases, and the Python image is the largest because it carries a Python runtime on top of the same browsers.
Looking back across releases, the noble image grew from 880 MB in 1.60.0 to 912 MB in 1.63.0, so the trend is slow but upward.
Build your own image with one browser
If your suite only runs Chromium, the official image carries two engines you never launch. The Playwright docs show a two-line custom Dockerfile, and restricting it to a single browser is a small change.
FROM node:20-bookworm
RUN npx -y [email protected] install --with-deps chromium
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
CMD ["npx", "playwright", "test", "--project=chromium"]
The version in the install command must match @playwright/test in your lockfile for the same reason the official tag must match. Debian 12 and 13 and Ubuntu 22.04, 24.04, and 26.04 are the Linux versions listed under the Playwright system requirements, so bookworm is a safe base.
Tip: Use a BuildKit cache mount for npm so installs reuse downloaded packages across builds: RUN --mount=type=cache,target=/root/.npm npm ci. Docker's build cache docs describe cache mounts as a persistent location that accumulates across builds, so rebuilds get faster without adding layers.
Reduce pulls, not just bytes
Image size matters less when the image is already on the machine. Self-hosted runners keep pulled layers between jobs, and because the official image splits each browser into its own layer, a version bump only re-downloads the layers that changed. Hosted runners that start cold pay the full pull every time, which is part of the math in any honest CI cost optimization exercise.
The other lever is doing less work per container. Sharding shortens wall-clock time but multiplies pulls, so the right shard count depends on how long the suite takes against how long a pull takes.
The guide on how to reduce Playwright CI runtime works through that trade-off with real numbers. When something still goes wrong, the failures tend to fall into a short list.
Troubleshooting common Playwright Docker errors
Almost every problem reported with Playwright in containers traces back to one of five causes. Each one has a specific signature and a specific fix.
Executable does not exist
The error names a browser path under /ms-playwright that is not there. That is the version mismatch described earlier: your package.json pulls a different Playwright release than the image tag.
Align the two numbers and rebuild. Do not work around it with npx playwright install inside the container, which only hides the drift and doubles the image size.
Chromium crashes or pages hang
A Chromium tab that dies with a crash message, or a suite that stalls under load, usually points to shared memory. Run with --ipc=host as the Playwright docs recommend, or set --shm-size if host IPC is off limits. If the crash persists only on a laptop, try --cap-add=SYS_ADMIN as a diagnostic and then remove it.
Zombie processes and slow shutdowns
Each test can spawn browser processes, and when the container has no init process those children are never reaped. The Playwright docs recommend --init for this reason, and Docker describes the flag as running an init that forwards signals and reaps processes. In Compose the equivalent is init: true.
Permission denied on report folders
When you mount a host folder and run as pwuser or as a CI user like 1001, the container may not be allowed to write there. Create the folder on the host first, or match the user ID in the container to the owner on the host.
On GitHub Actions the --user 1001 option exists precisely so checkout and test steps agree on ownership.
You need to see the browser
Containers have no display, so headed mode does nothing by default. The Playwright docs describe a devcontainer setup with the desktop-lite feature that exposes a noVNC viewer on port 6080, which lets you run codegen and watch tests inside the container.
For most debugging, though, recording a trace and opening it in the Playwright trace viewer on the host is faster than a remote desktop.
Note: Playwright can also run as a server inside Docker while your tests stay on the host. Start it with npx playwright run-server in the container and point the runner at it with PW_TEST_CONNECT_WS_ENDPOINT=ws://127.0.0.1:3000/. The docs describe this for unsupported Linux distributions, and both versions must match.
The difference between headless vs headed runs matters more inside a container than on a laptop, since only headless works without extra setup. Everything else in the Playwright debugging guide applies unchanged, because traces, screenshots, and videos are files, and files come out through the volume mount.

Conclusion
Running Playwright in Docker comes down to a handful of decisions made once:
- Pin the image tag to the exact Playwright version in your lockfile.
- Run with --init and --ipc=host, and mount the report folder.
- Use root for your own app and pwuser with the seccomp profile for anything untrusted.
- Put the app under test in Compose so the tests reach it by hostname.
Do those things and the Playwright Docker image stops being a source of surprises and becomes the most boring part of the pipeline, which is exactly what test infrastructure should be. The suite that passes on a laptop passes on the runner, and when it does not, the trace tells you why.
The remaining work is seeing the results. Containers produce artifacts, and artifacts are only useful when someone reads them across runs.
Good Playwright test reporting turns each container's blob report into a trend, and a Playwright observability platform like TestDino turns that trend into a flaky test list and a root cause before the next pull request lands.
FAQs

Ayush Mania
Forward Development Engineer
