Infrastructure

Docker setup for deterministic visual testing

Screenshots differ between local and CI because environments differ. Docker gives you control over fonts, browsers, and rendering—everywhere.

Why Docker for visual testing

Visual tests that pass locally but fail in CI are usually environment problems. Different fonts, different browser versions, different rendering engines. Docker eliminates these variables.

Font consistency

Different operating systems have different system fonts and render the same fonts differently. Docker lets you control exactly which fonts are available.

Browser version pinning

Browser updates change rendering. Docker images let you lock to specific browser versions across all environments.

Rendering engine parity

macOS, Windows, and Linux render differently. Running tests in Linux containers ensures CI matches regardless of developer machine.

Reproducible environments

Every developer and every CI run uses the same environment. No more "works on my machine" for visual tests.

The goal is identical rendering conditions regardless of where tests run. Docker makes this achievable.

Font configuration

Fonts are the most common source of visual test flakiness. Different operating systems render fonts differently, and missing fonts trigger fallbacks that look completely different.

Use web fonts exclusively

Web fonts load the same everywhere. If your app uses web fonts, ensure they're loaded before screenshots.

Install specific system fonts

Install the exact fonts you need in your Docker image. Don't rely on whatever fonts happen to be available.

Control fallback stacks

If a font is missing, browsers use fallbacks. Make sure your container has the same fallbacks your design expects.

Avoid system-ui and generic fonts

system-ui, sans-serif, and similar generic families resolve differently per OS. Specify explicit font families.

When using web fonts, ensure they're loaded before capturing screenshots. Use document.fonts.ready or explicit font-loading waits.

Example Dockerfile

Here's a minimal Dockerfile for Playwright visual testing with custom fonts:

Dockerfile
# Use Playwright's official image as base
FROM mcr.microsoft.com/playwright:v1.40.0-focal

# Set working directory
WORKDIR /app

# Install additional fonts (example: Google fonts)
RUN apt-get update && apt-get install -y \
    fonts-inter \
    fonts-liberation \
    fonts-noto-cjk \
    && rm -rf /var/lib/apt/lists/*

# Copy custom fonts if needed
COPY ./fonts /usr/local/share/fonts/custom
RUN fc-cache -f -v

# Set timezone for consistent date rendering
ENV TZ=UTC
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime

# Set locale
ENV LANG=en_US.UTF-8
ENV LC_ALL=en_US.UTF-8

# Copy package files and install dependencies
COPY package*.json ./
RUN npm ci

# Copy test files
COPY . .

# Run tests
CMD ["npx", "playwright", "test"]

Adjust the font packages based on what your application uses. The key is explicit installation rather than relying on defaults.

Browser version pinning

Browser updates change rendering. A Chrome update can shift pixels enough to fail your visual tests. Pin your browser versions.

playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  use: {
    // Use a specific browser channel for consistency
    channel: 'chrome',

    // Or specify browser version via Docker image tag
    // The Playwright Docker image pins browser versions

    // Fixed viewport
    viewport: { width: 1280, height: 720 },

    // Disable animations
    reducedMotion: 'reduce',
  },

  // Run tests inside the container
  projects: [
    {
      name: 'chromium',
      use: { browserName: 'chromium' },
    },
  ],
});

The Playwright Docker image tags (like v1.40.0-focal) include specific browser versions. Use explicit tags, not latest.

GPU and headless rendering

Browsers render differently with GPU acceleration vs software rendering. CI environments often lack GPUs, causing differences from local machines.

  • Disable GPU: Force software rendering for consistency. Slower but deterministic.
  • Run headless: Headless mode removes window chrome variability.
  • Use Xvfb: If you need headed mode, use a virtual framebuffer.
Chromium flags for consistency
// In playwright.config.ts
use: {
  launchOptions: {
    args: [
      '--disable-gpu',
      '--disable-dev-shm-usage',
      '--no-sandbox',
    ],
  },
},

Timezone and locale

Date formatting and text direction depend on locale settings. Set them explicitly in your container.

Environment variables
ENV TZ=UTC
ENV LANG=en_US.UTF-8
ENV LC_ALL=en_US.UTF-8

# In playwright.config.ts
use: {
  locale: 'en-US',
  timezoneId: 'America/New_York',
},

Match your target audience's locale, or use a neutral one (like en-US) for consistency.

CI optimization tips

Docker adds overhead. These strategies keep your visual test pipeline fast:

Use multi-stage builds

Separate build dependencies from runtime. Your final image should contain only what's needed to run tests.

Cache browser binaries

Playwright downloads browsers on install. Cache the browser directory between CI runs to avoid repeated downloads.

Use official browser images

Playwright provides pre-built Docker images with browsers included. These are tested and optimized.

Layer your Dockerfile strategically

Put rarely-changing layers (OS, fonts, browsers) first. Put frequently-changing layers (your code) last for better caching.

For more on CI pipeline design, see visual testing in CI pipelines.

Local development with Docker

Running tests in Docker locally ensures you catch environment issues before CI:

docker-compose.yml
services:
  visual-tests:
    build: .
    volumes:
      - .:/app
      - /app/node_modules
    environment:
      - CI=true
    command: npx playwright test --update-snapshots

Use the same Docker configuration locally and in CI. If tests pass locally in Docker, they should pass in CI.

The tradeoff

Docker adds complexity. You're maintaining container configuration alongside your tests. For small teams or simple apps, this overhead may not be worth it.

Consider Docker when:

  • Local vs CI differences are causing real pain
  • Your team uses diverse development machines
  • You need cross-browser testing with consistent results
  • Visual test stability is business-critical

If flakiness is your main problem, see reducing visual testing flakiness for strategies that don't require Docker.

Deterministic rendering checklist

  • Use a consistent base image (e.g., mcr.microsoft.com/playwright)
  • Install all required fonts explicitly in the Dockerfile
  • Pin browser versions (don't use 'latest' tags)
  • Set fixed viewport dimensions in test configuration
  • Configure timezone and locale consistently
  • Disable GPU rendering or ensure consistent GPU emulation
  • Cache browser binaries in CI for faster builds
  • Test the Docker image locally before relying on it in CI

Related guides

Less flakiness, more confidence—join the waitlist

Get early access