Skip to content

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.

Terminal window
# CI mode
pxdiff run --docker -- npx playwright test
# Local mode
pxdiff local --docker -- npx vitest run

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

When you pass --docker, pxdiff:

  1. Detects your Playwright version from node_modules/@playwright/test/package.json
  2. Builds a runner image (if not already cached) based on the matching mcr.microsoft.com/playwright base image — same Chromium, same fonts
  3. Mounts your project into the container at /app
  4. Runs install inside the container to get Linux-native binaries (e.g., esbuild, rollup)
  5. 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.

The CLI auto-detects your package manager from your lockfile:

LockfileInstall command
pnpm-lock.yamlpnpm install --frozen-lockfile
yarn.lockyarn install --frozen-lockfile
package-lock.jsonnpm ci

Pass a custom Docker image if you need additional dependencies or a specific configuration:

Terminal window
pxdiff run --docker my-org/my-runner:latest -- npx playwright test

When using a custom image, Playwright version detection is skipped.

Named Docker volumes persist installed packages across runs:

RunInstall timeWhy
First~20-40sDownloads and links all packages
Subsequent~1snode_modules already populated

A separate cache volume (pxdiff-runner-cache) persists Cypress binaries and other cached downloads.

  • 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/test or playwright in node_modules). The CLI reads the installed version to build the matching runner image.
  • 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-arm on GitHub Actions).
  • Git worktrees: Fully supported. The CLI mounts the main .git directory so branch/commit detection works inside the container.
  • Environment variables: Only PXDIFF_*, CI, NODE_ENV, GITHUB_HEAD_REF, and GITHUB_ACTIONS are forwarded. PATH, HOME, and other host-specific variables are excluded.
.github/workflows/vrt.yml
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 }}

Install Docker Desktop (macOS/Windows) or Docker Engine (Linux) and ensure the daemon is running.

Install @playwright/test in your project, or specify a custom image with --docker <image>.

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 --memory or 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:

Terminal window
# Run browser tests alone
pxdiff local --docker -- npx vitest run --project @my/web-tests
# Run non-browser tests separately
pxdiff local --docker -- npx vitest run --project @my/core --project @my/api

Screenshots 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 Actions
runs-on: ubuntu-24.04-arm

If ARM64 runners aren’t available, you can force emulation locally (slower but deterministic):

Terminal window
docker run --platform linux/amd64 ...

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:

Terminal window
docker volume ls | grep pxdiff-modules
docker volume rm pxdiff-modules-<hash>

Or remove all pxdiff volumes:

Terminal window
docker volume ls -q | grep pxdiff | xargs docker volume rm

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.