Tests that pass locally but fail in CI are often not test bugs: they're environment mismatches between Node versions, browser builds, or missing system dependencies. A Docker container fixes this by bundling Playwright, its browser binaries, and all dependencies into a single image that runs identically on a laptop, a teammate's machine, and CI. This guide covers writing a Dockerfile for Playwright, running tests in a container locally, integrating with GitHub Actions, and using Docker Compose when your tests need the application and a database running alongside them.

Why QA Engineers Need Docker

Without Docker:
  • Your tests pass locally with Node 20, fail in CI with Node 18
  • Chrome version on CI differs from your laptop
  • Environment variables set up differently per machine
  • New team member spends a day getting tests to run
With Docker:
  • Tests run in the same container locally and in CI
  • Everyone uses the same Node, Chrome, and dependency versions
  • New team member: docker compose up — tests run in 2 minutes

Core Concepts

Image

An image is a blueprint — a read-only snapshot of a file system with software installed. Think of it like a class in code.

playwright/playwright:latest — an image with Playwright and browsers pre-installed
node:20-alpine — Node.js 20 on minimal Alpine Linux

Container

A container is a running instance of an image. Multiple containers can run from the same image simultaneously. Like creating multiple objects from the same class.

Dockerfile

Instructions for building a custom image. You start from a base image and add your files and configuration.

Docker Compose

A tool for running multiple containers together. Your tests might need a running web server and a database — Compose starts all of them with one command.

Your First Playwright Dockerfile

Playwright provides official Docker images with browsers pre-installed:

# Dockerfile
FROM mcr.microsoft.com/playwright:v1.44.0-jammy

# Set working directory inside the container
WORKDIR /app

# Copy dependency files first (caching optimization)
COPY package.json package-lock.json ./

# Install Node dependencies
RUN npm ci

# Copy the rest of your project
COPY . .

# Default command: run all tests
CMD ["npx", "playwright", "test"]

Build the image:

docker build -t my-playwright-tests .

Run the tests:

docker run my-playwright-tests

That's it. Chrome, Firefox, and WebKit are all inside the container. No browser installation needed on the host machine.

Running Tests with Docker

Basic run

# Run all tests
docker run my-playwright-tests

# Run specific test file
docker run my-playwright-tests npx playwright test tests/login.spec.ts

# Run with environment variable
docker run -e BASE_URL=https://staging.myapp.com my-playwright-tests

# Mount local directory (so you can see test results)
docker run -v $(pwd)/playwright-report:/app/playwright-report my-playwright-tests

Saving test results

Container file system disappears when the container stops. Use volume mounts to save reports:

docker run \
  -v $(pwd)/playwright-report:/app/playwright-report \
  -v $(pwd)/test-results:/app/test-results \
  my-playwright-tests

Now playwright-report/ appears on your local machine after the run.

Docker Compose for Full Stack Testing

Real testing often means the app under test needs to run too. Compose starts everything together.

# docker-compose.yml
version: '3.8'

services:
  # The app you're testing
  app:
    image: myapp:latest
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=test
      - DATABASE_URL=postgresql://postgres:postgres@db:5432/testdb
    depends_on:
      db:
        condition: service_healthy

  # Database
  db:
    image: postgres:16
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: testdb
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5

  # Playwright tests
  tests:
    build: .
    environment:
      - BASE_URL=http://app:3000
    depends_on:
      - app
    volumes:
      - ./playwright-report:/app/playwright-report
      - ./test-results:/app/test-results

Run everything:

docker compose up --exit-code-from tests

  • Starts the database
  • Waits for it to be healthy
  • Starts the app
  • Runs your tests
  • Returns the tests' exit code (so CI knows pass/fail)
  • --exit-code-from tests — exit with tests container's exit code

Optimizing the Dockerfile

Layer caching makes rebuilds fast. The key rule: put things that change rarely at the top.

FROM mcr.microsoft.com/playwright:v1.44.0-jammy

WORKDIR /app

# 1. Copy dependency files (rarely change)
COPY package.json package-lock.json ./
# This layer is cached until package.json changes
RUN npm ci

# 2. Copy the rest (changes every commit)
COPY . .

CMD ["npx", "playwright", "test"]

If you only changed test files, Docker reuses the npm ci layer from cache. Build time: seconds instead of minutes.

CI/CD Integration

GitHub Actions

# .github/workflows/playwright.yml
name: Playwright Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Build test image
        run: docker build -t playwright-tests .
      
      - name: Run tests
        run: |
          docker run \
            -v ${{ github.workspace }}/playwright-report:/app/playwright-report \
            -e BASE_URL=${{ secrets.STAGING_URL }} \
            playwright-tests
      
      - name: Upload report
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: playwright-report
          path: playwright-report/

GitLab CI

# .gitlab-ci.yml
test:
  image: docker:latest
  services:
    - docker:dind
  script:
    - docker build -t tests .
    - docker run 
        -v $(pwd)/playwright-report:/app/playwright-report
        -e BASE_URL=$STAGING_URL
        tests
  artifacts:
    when: always
    paths:
      - playwright-report/

Useful Docker Commands for QA

# List running containers
docker ps

# List all images
docker images

# Stop all running containers
docker stop $(docker ps -q)

# Remove all stopped containers
docker container prune

# Remove unused images
docker image prune

# Follow container logs
docker logs -f <container-id>

# Open a shell inside a running container (great for debugging)
docker exec -it <container-id> /bin/bash

# Run a container interactively
docker run -it my-playwright-tests /bin/bash

Debugging Inside a Container

When tests fail only in the container:

# Start container with shell instead of tests
docker run -it my-playwright-tests /bin/bash

# Now you're inside the container
# Check what's installed
node --version
npx playwright --version

# Run tests manually
npx playwright test tests/login.spec.ts --headed --debug

# Check environment variables
env | grep BASE_URL

Common issues:

  • BASE_URL pointing to localhost (works on host, fails in container — use service name instead)
  • Missing environment variables
  • File permissions (files created outside container might be owned by root inside)

The Official Playwright Docker Image

# Pull the latest Playwright image
docker pull mcr.microsoft.com/playwright:v1.44.0-jammy

# See what's inside
docker run -it mcr.microsoft.com/playwright:v1.44.0-jammy /bin/bash

# Check installed browsers
npx playwright install --list

Tags to know:

  • v1.44.0-jammy — specific Playwright version on Ubuntu 22.04 (Jammy)
  • latest — latest version (can break when Playwright updates)
  • Always pin to a specific version in production

Summary

| Concept | What it is |

|---------|-----------|

| Image | Blueprint with software pre-installed |

| Container | Running instance of an image |

| Dockerfile | Instructions to build a custom image |

| Docker Compose | Run multiple containers together |

| Volume mount | Share files between host and container |

For Playwright specifically:

1. Use mcr.microsoft.com/playwright as base image — browsers included

2. Mount report directories so results survive container stop

3. Use Compose to start your app + tests together

4. Pin image versions — latest causes surprise breaks

Docker turns "works on my machine" into "works everywhere." For QA, that means your tests are actually testing the software, not fighting environment differences.

→ See also: Docker for Testers: Running Playwright in Containers | CI/CD for QA: GitHub Actions, Jenkins, and GitLab Compared | GitHub Actions for Playwright Tests: The Complete Setup (2026)