Playwright in GitHub Actions: Docker, Failure Capture, and Worker Parallelism
TestlandAugust 1, 2026Match the Playwright Docker image tag to your installed version, choose screenshot and video capture deliberately, and set workers for CI instead of the local default.

This post assumes you already have a Playwright suite running in GitHub Actions via native install and gating pull requests on it. Catch up with the GitHub Actions setup guide and the Playwright TypeScript setup guide if you're not there yet. Two moments usually trigger the next round of tuning: a browser that refuses to launch because the hosted runner's system libraries drifted out from under a pinned Docker tag, or a failed run whose only evidence is a trace.zip too slow to open when all you want is a quick look at what broke. Playwright has 93.8k GitHub stars as of August 2026 and documentation to match, but three settings in that documentation get misapplied the same way on hosted runners, and each is corrected by a different wrong instinct: copying the docs' image tag verbatim instead of matching your own, turning on failure capture by reflex instead of reading what each toggle actually costs, and cranking workers up for speed instead of reading what GitHub Actions guidance actually recommends.
Table of contents
Locking the Playwright Docker image tag to your installed version
The documented container job and its already-outdated tag
Playwright's own CI documentation publishes a container-based job for GitHub Actions, and reproducing it is a reasonable starting point:
jobs:
test:
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.62.0-noble
options: --user 1001
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: lts/*
- run: npm ci
- run: npx playwright testThe container: key doesn't wrap a single step: it runs the entire job, checkout, install, and test run, inside the image. Every action after that point executes against whatever browser binaries the image ships, not against whatever npx playwright install would pull natively. That's why version match matters so much: "If the Playwright version in your Docker image does not match the version in your project/tests, Playwright will be unable to locate browser executables." Both numbers need to trace back to the same source: whatever @playwright/test version is pinned in package.json, not whichever tag looked current when the workflow file was written. The tag in that example, v1.62.0-noble, is already a patch behind: the newest published release is v1.62.1, from July 30, 2026. Playwright describes exactly what's in the image: "This image includes the Playwright browsers and browser system dependencies." It's built on Ubuntu 24.04, the "noble" release, but the @playwright/test npm package isn't part of it, so npm ci still runs before the test command.
The missing --init and --ipc=host flags in Playwright's own example
That same container job sets options: --user 1001 with no explaining prose attached to it, and it skips two flags Playwright's own Docker page recommends elsewhere. The first: "Using --init Docker flag is recommended to avoid special treatment for processes with PID=1." The second, specific to Chromium: "Using --ipc=host is recommended when using Chromium. Without it, Chromium can run out of memory and crash." Neither shows up in the documented GitHub Actions example, and neither is explained where it does appear. That's a gap in the docs, not a warning from Playwright: the container example and the flag recommendations live on different pages, and nothing connects them. Carrying both forward into a real job means extending the options line rather than replacing it:
container:
image: mcr.microsoft.com/playwright:v1.62.0-noble
options: --init --ipc=host --user 1001All three flags can sit on the same line. --user 1001 matches the non-root user the image already runs as, --init gives PID 1 proper signal handling, and --ipc=host is the one that matters most for Chromium runs specifically, not Firefox or WebKit.
Screenshot, video, and trace are three independent toggles
Screenshot and video: what each captures and what it costs
screenshot and video capture different things and default to the same value: off. screenshot takes four values: 'off', 'on', 'only-on-failure', and 'on-first-failure', a single image per test at the point Playwright decides the test is done. video takes seven: 'off', 'on', 'retain-on-failure', 'on-first-retry', 'on-all-retries', 'retain-on-first-failure', and 'retain-on-failure-and-retries', a full recording of everything that happened during the test, not just its last moment. "Video files will appear in the test output directory, typically test-results," at a size that defaults to the viewport scaled down to fit 800x800px. Playwright's docs don't state where screenshot files land, only that video's location is documented; don't assume a path for the screenshot output beyond what the docs actually say.
Turning either on for every test, rather than only on failure, doesn't change what's captured: it changes how much of it accumulates. Video in particular records for the full duration of every test, and that cost multiplies across however many workers run in parallel on the runner, whether or not those tests ever fail.
Trace: the only toggle with a documented performance warning
trace defaults to 'off', like the other two, but only one of its values carries a warning in the docs. Setting trace: 'on' means exactly what the docs say it means: "Record a trace for each test. (not recommended as it's performance heavy)". No equivalent caution exists for screenshot: 'on' or video: 'on': the docs don't call either expensive, only that trace recorded for every test is.
Fixing flaky Playwright tests already covers trace: 'on-first-retry' as one recommended setting for chasing down intermittent failures. This post surveys the fuller option space across all three capture toggles together, plus the one documented cost warning that applies specifically to trace: 'on' and nowhere else. A common pairing for CI:
// playwright.config.ts
export default {
use: {
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
};Both capture only on the run that needed the evidence, which is the version of "turn on failure capture" that doesn't accumulate hundreds of screenshots and video files from tests that never failed.
Workers default to half your cores; GitHub Actions guidance says one
The Playwright TypeScript setup guide already ships workers: process.env.CI ? 1 : undefined in its canonical playwright.config.ts, and its "a few things worth noting" list walks through fullyParallel, retries, trace, and forbidOnly in detail. Workers gets the line but not the explanation. Here's the explanation.
The workers option is documented plainly: "Defaults to half of the number of logical CPU cores." It also accepts a percentage, like '50%'. That's the local default, tuned for a laptop where running half the cores in parallel still leaves the machine usable for other work. Playwright's CI documentation recommends something different for hosted runners: "We recommend setting workers to '1' in CI environments to prioritize stability and reproducibility. Running tests sequentially ensures each test gets the full system resources, avoiding potential conflicts."
process.env.CI ? 1 : undefined is exactly those two documented positions encoded in a single line: undefined falls back to the package default (half the cores) on a developer's machine, and 1 overrides it to sequential execution the moment CI is set. Raising workers above 1 in a GitHub Actions job trades that isolation for speed: tests run in parallel and finish faster, but they also compete for the runner's fixed CPU and memory, and a flaky failure gets harder to reproduce when several tests were sharing resources at the moment it happened.
Workers vs shards: parallelism inside a job, not across jobs
Workers and shards solve different scaling problems. Workers parallelize inside a single job, on that job's own CPU cores. Sharding goes further: "In order to achieve even greater parallelisation, you can further scale Playwright test execution by running tests on multiple machines simultaneously. We call this mode of operation 'sharding'." Shards run as separate jobs on separate runners, and the workers setting still applies independently inside each shard's job, so a four-shard workflow with workers: 1 still runs four jobs in parallel, one test file at a time inside each. The GitHub Actions setup guide covers sharding configuration in detail.
Docker, capture, and worker defaults at a glance
Three defaults ship with Playwright out of the box, and GitHub Actions guidance changes only one of the three outright: workers. The other two default to off and only need reading, not overriding, once you know what each toggle actually does. The table below separates what the package or a native install already assumes from what changes on a hosted runner, and what breaks (or just costs more) when the CI value gets ignored:
| Setting | Local or package default | CI guidance and what changes if you deviate |
|---|---|---|
| Docker image tag | none (native install has no image) | Must match the installed Playwright version exactly; a mismatch means Playwright can't find browser executables |
| screenshot | 'off' | Stays 'off' or 'only-on-failure' in practice; capturing every test adds storage with no extra debugging value |
| video | 'off' | Stays 'off' or 'retain-on-failure'; 'on' records every test, and that cost multiplies across however many workers run in parallel |
| trace | 'off' | The only value with a documented performance warning is 'on': "not recommended as it's performance heavy" |
| workers | half of logical CPU cores | GitHub Actions guidance recommends 1, for stability and reproducibility; raising it trades isolation for speed |
Each row corrects a different instinct: match the tag instead of copying it, capture on failure instead of by reflex, and hold workers at 1 instead of cranking them up.
All three corrections share the same shape: a wrong instinct, and one documented fact that fixes it. Match the Docker tag instead of trusting the one in the docs, turn on capture where the docs actually recommend it instead of everywhere, and read the CI guidance on workers before touching the number at all. When a run fails and the fix isn't obvious from a config value, fixing flaky Playwright tests is the next place to look.