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
- 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 LinuxContainer
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"]docker build -t my-playwright-tests .docker run my-playwright-testsThat'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-testsSaving 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-testsNow 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-resultsdocker 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/bashDebugging 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_URLCommon issues:
BASE_URLpointing tolocalhost(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 --listTags 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)