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:
# 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.
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.
// 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.
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:
services:
visual-tests:
build: .
volumes:
- .:/app
- /app/node_modules
environment:
- CI=true
command: npx playwright test --update-snapshotsUse 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