Skip to content
ExponaBLOG
PLAYWRIGHT & BROWSER AUTOMATION · DAY 076 / 100

Testing in Docker: Playwright best practices explained

Consistent environments everywhere.

Expona Admin6 min read

Running Playwright tests locally feels fast and reliable, but the moment your CI pipeline kicks in, screenshots mismatch, fonts render differently, and flaky tests multiply. Testing in Docker: Playwright best practices explained is the antidote—it ensures that the environment where you develop, test, and deploy stays perfectly synchronized. In this guide, we'll walk through the practical setup that keeps your end-to-end test suite predictable across every machine, CI runner, and developer's laptop.

Why Docker matters for Playwright tests

Playwright is a powerful browser automation framework, but it's also environment-sensitive. Fonts, system libraries, browser versions, and OS configuration all affect test behavior. When your colleague runs tests on macOS and your CI uses Ubuntu, visual regression tests fail even though the code is identical. Docker eliminates this friction by packaging the exact same OS, system libraries, and browser versions into a reproducible container.

The most common scenario we see: a developer writes a visual regression test that passes locally. They push to main, CI runs it, and it fails on font rendering or button spacing. Six commits later, someone realizes the local machine has a different OS than the CI runner. With Docker, this class of bug vanishes entirely.

Official image — mcr.microsoft.com/playwright with browsers installed

Microsoft maintains an official Playwright Docker image at mcr.microsoft.com/playwright that comes with all major browsers (Chromium, Firefox, WebKit) already installed, along with all system dependencies. This is your starting point—never build Playwright's dependencies from scratch.

Here's a basic Dockerfile for a Node.js project using Playwright:

typescript
FROM mcr.microsoft.com/playwright:v1.40.0-focal

WORKDIR /app

COPY package.json package-lock.json ./
RUN npm ci

COPY . .

CMD ["npm", "run", "test:e2e"]

This approach gives you:

  • Pre-installed browsers — Chromium, Firefox, and WebKit are ready to use immediately.
  • System dependencies — Libraries like libxkbcommon, libpango, and font rendering tools are already there.
  • No version mismatch surprises — The image version locks everything together predictably.

The key insight: use the versioned tag (like v1.40.0-focal), not latest. This prevents unexpected browser updates that could break your tests or change visual output silently.

Match versions — Image version must match the Playwright package

Here's where many teams stumble. The Playwright Docker image version and your npm package version must align. If your package.json specifies Playwright 1.40.0 but your Docker image is v1.39.0, browser binaries will mismatch and tests may fail or behave unexpectedly.

Set up a consistent versioning strategy:

typescript
{
  "devDependencies": {
    "@playwright/test": "1.40.0"
  }
}

Then in your Dockerfile, use the exact same version:

typescript
FROM mcr.microsoft.com/playwright:v1.40.0-focal

A real-world example: we had a team where the Docker image was pinned to v1.38.0 but developers locally had 1.40.0 installed via npm install. Local tests passed; CI tests failed intermittently. A 20-minute investigation revealed the version gap. After aligning both to 1.40.0, all failures disappeared.

To manage this systematically, automate the check in your CI:

typescript
#!/bin/bash
PACKAGE_VERSION=$(grep '"@playwright/test"' package.json | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')
DOCKER_VERSION=$(grep 'playwright:v' Dockerfile | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')

if [ "$PACKAGE_VERSION" != "$DOCKER_VERSION" ]; then
  echo "Playwright version mismatch: package=$PACKAGE_VERSION, docker=$DOCKER_VERSION"
  exit 1
fi

Run your app too — Docker Compose for app, database and tests

Unit tests can run in isolation, but Playwright tests need your actual application running. The cleanest approach is Docker Compose, which orchestrates your app, database, and test container as a coordinated system.

Here's a practical docker-compose.yml:

typescript
version: '3.9'

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile.app
    environment:
      DATABASE_URL: postgresql://postgres:password@db:5432/test_db
      NODE_ENV: test
    ports:
      - "3000:3000"
    depends_on:
      - db
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 5s
      timeout: 3s
      retries: 10

  db:
    image: postgres:15
    environment:
      POSTGRES_DB: test_db
      POSTGRES_PASSWORD: password
    volumes:
      - db_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 3s
      retries: 5

  tests:
    image: mcr.microsoft.com/playwright:v1.40.0-focal
    working_dir: /app
    volumes:
      - .:/app
    environment:
      BASE_URL: http://app:3000
    command: npm run test:e2e
    depends_on:
      app:
        condition: service_healthy
      db:
        condition: service_healthy

volumes:
  db_data:

Key benefits of this setup:

  • Service dependencies — The depends_on with healthchecks ensures the app is truly ready before tests run.
  • Isolated database — Each test run gets a fresh database state, eliminating test pollution.
  • Network isolation — Services communicate via internal Docker networks, mirroring production topology.
  • Local development parity — Developers run docker-compose up and get the exact same stack as CI.

In your playwright.config.ts, use the Docker network URL:

typescript
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  webServer: {
    command: 'npm run start',
    url: 'http://localhost:3000',
    reuseExistingServer: false,
  },
  use: {
    baseURL: process.env.BASE_URL || 'http://localhost:3000',
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
  ],
});

When running in Docker Compose, BASE_URL becomes http://app:3000, and tests communicate with the app container seamlessly.

Same screenshots — Identical fonts and rendering

Visual regression tests are powerful but fragile if rendering isn't identical everywhere. The official Playwright image includes common fonts and font rendering libraries, but you may need additional fonts for your application.

If your app uses custom fonts or relies on system fonts, extend the base image:

typescript
FROM mcr.microsoft.com/playwright:v1.40.0-focal

RUN apt-get update && apt-get install -y \
    fonts-dejavu \
    fonts-liberation \
    fonts-noto-cjk \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app

COPY package.json package-lock.json ./
RUN npm ci

COPY . .

CMD ["npm", "run", "test:e2e"]

A real scenario: we were testing a SaaS dashboard with charts and data tables. Screenshots matched locally but failed in CI because the CI image lacked the Noto Sans font family. Adding fonts-noto-cjk to the Docker image resolved all failures instantly.

Beyond fonts, ensure consistent rendering with these Playwright config settings:

typescript
export default defineConfig({
  use: {
    // Disable locale-specific rendering quirks
    locale: 'en-US',
    // Use a fixed timezone to avoid date-related differences
    timezoneId: 'America/New_York',
    // Disable GPU acceleration to ensure deterministic rendering
    launchArgs: ['--disable-gpu'],
  },
  snapshotConfig: {
    // Allow pixel-level variance for cross-platform tolerance
    maxDiffPixels: 100,
  },
});

Screenshots taken in Docker will now match across all machines running the same image version.

Common mistakes

  • Using latest tags — Browsers update unexpectedly, breaking your tests silently. Always pin to a specific version.
  • Ignoring healthchecks in Docker Compose — Tests may start before the app is truly ready, causing flaky failures. Use healthchecks to guarantee readiness.
  • Running tests without the app in Docker — Testing a local app while running tests in Docker creates network connectivity issues and mismatched environments. Keep everything in containers.
  • Forgetting to install fonts — Visual regression tests fail mysteriously because the Docker image lacks your application's custom fonts. Always list font dependencies explicitly.
  • Not versioning the Playwright image with package.json — This creates a version gap where local and CI tests use different browsers, causing intermittent failures that are hard to debug.
  • Mounting the entire host filesystem without .dockerignore — This can bloat the Docker context and cause slow builds. Use a proper .dockerignore to exclude node_modules and test artifacts.

Key takeaways

  • Use the official mcr.microsoft.com/playwright image as your foundation—it includes all browsers and system dependencies, saving build time and complexity.
  • Pin the Playwright image version to match your npm package version exactly, and automate this check in CI to catch mismatches early.
  • Orchestrate your app, database, and tests with Docker Compose, using healthchecks to ensure services are ready before tests run.
  • Install required fonts in the Docker image and configure Playwright to use fixed locale and timezone settings for pixel-perfect visual regression tests.
  • Test the entire stack in Docker, not just the test runner—this eliminates environment-specific failures and makes CI behavior predictable from day one.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles