Docker for consistent rendering
When you capture screenshots on macOS locally and Linux in CI, font rendering differences between Core Text and FreeType produce false-positive diffs — even with identical fonts installed. The --docker flag solves this by running your tests inside a consistent Linux container.
Quick start
Section titled “Quick start”# CI modepxdiff run --docker -- npx playwright test
# Local modepxdiff local --docker -- npx vitest runThe first run builds the runner image and installs dependencies (~1-2 min). Subsequent runs are near-instant thanks to Docker layer caching and persistent node_modules volumes.
How it works
Section titled “How it works”When you pass --docker, pxdiff:
- Detects your Playwright version from
node_modules/@playwright/test/package.json - Builds a runner image (if not already cached) based on the matching
mcr.microsoft.com/playwrightbase image — same Chromium, same fonts - Mounts your project into the container at
/app - Runs
installinside the container to get Linux-native binaries (e.g., esbuild, rollup) - Executes your command with pxdiff environment variables forwarded
Session lifecycle (session ID, completion, GitHub check runs) stays on the host — only test execution moves into Docker.
Package manager support
Section titled “Package manager support”The CLI auto-detects your package manager from your lockfile:
| Lockfile | Install command |
|---|---|
pnpm-lock.yaml | pnpm install --frozen-lockfile |
yarn.lock | yarn install --frozen-lockfile |
package-lock.json | npm ci |
Custom image
Section titled “Custom image”Pass a custom Docker image if you need additional dependencies or a specific configuration:
pxdiff run --docker my-org/my-runner:latest -- npx playwright testWhen using a custom image, Playwright version detection is skipped.
Performance
Section titled “Performance”Named Docker volumes persist installed packages across runs:
| Run | Install time | Why |
|---|---|---|
| First | ~20-40s | Downloads and links all packages |
| Subsequent | ~1s | node_modules already populated |
A separate cache volume (pxdiff-runner-cache) persists Cypress binaries and other cached downloads.
Requirements
Section titled “Requirements”- Docker must be installed and the daemon running. The CLI checks this upfront and shows a clear error if Docker is unavailable.
- Playwright must be installed in your project (
@playwright/testorplaywrightinnode_modules). The CLI reads the installed version to build the matching runner image.
Architecture considerations
Section titled “Architecture considerations”- ARM64 vs AMD64: Chromium produces different sub-pixel rendering on each architecture. Use the same architecture in both local and CI environments. Mac-heavy teams should use ARM64 runners in CI (e.g.,
ubuntu-24.04-armon GitHub Actions). - Git worktrees: Fully supported. The CLI mounts the main
.gitdirectory so branch/commit detection works inside the container. - Environment variables: Only
PXDIFF_*,CI,NODE_ENV,GITHUB_HEAD_REF, andGITHUB_ACTIONSare forwarded.PATH,HOME, and other host-specific variables are excluded.
CI example
Section titled “CI example”jobs: visual-tests: runs-on: ubuntu-24.04-arm steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: node-version: 24 cache: pnpm - run: pnpm install --frozen-lockfile - run: pnpm pxdiff run --docker -- npx playwright test env: PXDIFF_API_KEY: ${{ secrets.PXDIFF_API_KEY }}Troubleshooting
Section titled “Troubleshooting””Docker is not available”
Section titled “”Docker is not available””Install Docker Desktop (macOS/Windows) or Docker Engine (Linux) and ensure the daemon is running.
”Could not detect Playwright version”
Section titled “”Could not detect Playwright version””Install @playwright/test in your project, or specify a custom image with --docker <image>.
Slow first run
Section titled “Slow first run”The first run builds the runner image and installs dependencies. Subsequent runs reuse cached layers and volumes. If you’re seeing slow runs every time, check that Docker volumes aren’t being pruned between runs.
Browser crashes / “connection was closed”
Section titled “Browser crashes / “connection was closed””If you see errors like Browser connection was closed while running tests or rpc is closed, the Chromium process inside the container is being killed — usually due to insufficient memory.
The --docker flag sets --shm-size=2g for shared memory, but the container’s total memory is limited by your Docker configuration. Browser-mode test suites (Vitest Browser Mode, Playwright) can consume significant memory — especially when multiple test files run in parallel, each opening its own browser page.
Fix: Increase Docker’s memory limit:
- Docker Desktop (macOS/Windows): Open Settings > Resources > Memory. 16 GB is recommended for projects with browser-mode tests. The default (8 GB) may not be enough for large test suites.
- Docker Engine (Linux): Memory is unlimited by default. If you’ve set limits via
--memoryor cgroup constraints, increase them. - CI: Most CI providers allocate sufficient memory by default, but self-hosted runners may need configuration.
If increasing memory isn’t an option, reduce peak usage by running browser-mode test projects separately:
# Run browser tests alonepxdiff local --docker -- npx vitest run --project @my/web-tests
# Run non-browser tests separatelypxdiff local --docker -- npx vitest run --project @my/core --project @my/apiScreenshots differ between local Docker and CI
Section titled “Screenshots differ between local Docker and CI”Even with --docker, screenshots can differ if the CPU architecture doesn’t match. Chromium produces different subpixel rendering on ARM64 vs AMD64.
Fix: Match architectures. If your team uses Apple Silicon Macs (ARM64), use ARM64 CI runners:
# GitHub Actionsruns-on: ubuntu-24.04-armIf ARM64 runners aren’t available, you can force emulation locally (slower but deterministic):
docker run --platform linux/amd64 ...Stale node_modules volume
Section titled “Stale node_modules volume”On macOS, pxdiff uses a named Docker volume to cache Linux-native node_modules. If you upgrade dependencies or switch branches with different lockfiles, the cached volume may be stale.
Fix: Delete the volume and let it rebuild:
docker volume ls | grep pxdiff-modulesdocker volume rm pxdiff-modules-<hash>Or remove all pxdiff volumes:
docker volume ls -q | grep pxdiff | xargs docker volume rmPermission errors on mounted files
Section titled “Permission errors on mounted files”If you see EACCES or permission denied errors inside the container, it’s usually because the container runs as root but the mounted files have restrictive host permissions.
Fix: Ensure your project files are readable. On Linux, files created by your user are typically readable by root. On macOS, Docker Desktop handles permissions transparently — if you’re seeing issues, try restarting Docker Desktop.