The "works on my machine" problem is a class of bug that disappears the moment everyone runs the same OS, the same Python, and the same browser binaries — which is exactly what Docker gives you. Microsoft publishes official Playwright Python images with the matching browsers and system libraries pre-installed; you build a thin layer on top with your dependencies and tests, and the whole environment is reproducible from any developer laptop, any CI runner, any Kubernetes pod. This lesson covers the official image, a minimal Dockerfile, Docker Compose for tests-plus-app workflows, and the GitLab/GitHub patterns for running tests inside containers.
The official Playwright Python image
Microsoft publishes versioned images at mcr.microsoft.com/playwright/python:
docker pull mcr.microsoft.com/playwright/python:v1.44.0-jammyThe tag has two parts:
v1.44.0— the Playwright version. Pins the bundled browsers to known-good versions.-jammy— the Ubuntu base.jammyis 22.04,nobleis 24.04. Pick the OS your team uses elsewhere.
What's inside:
- Ubuntu base.
- Python 3.x.
- Playwright (the Python package).
- Chromium, Firefox, and WebKit binaries pre-downloaded.
- All the system libraries (
libnss3,libatk1, etc.) the browsers need.
You don't run playwright install inside this image — the browsers are already there.
Running tests in the official image — one command
The fastest path: mount your project into the image and run pytest:
docker run -it --rm \
-v $(pwd):/work -w /work \
mcr.microsoft.com/playwright/python:v1.44.0-jammy \
pytest tests/ --browser chromiumWhat each flag does:
-it— interactive, attaches your terminal so you see output.--rm— removes the container after it exits (no clutter).-v $(pwd):/work— mounts your current directory into/workinside the container.-w /work— sets/workas the working directory.
The container starts, runs pytest, exits. Test results appear in your local tests/ folder via the mount. Zero config on your laptop — no Python install, no playwright install, no apt packages. As long as Docker runs, the tests run.
A custom Dockerfile
For a real project you want a versioned image you can reuse — installs your dependencies, copies your code:
FROM mcr.microsoft.com/playwright/python:v1.44.0-jammy
WORKDIR /app
# Install dependencies first — caches well across rebuilds
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy the test code last — this layer rebuilds on every code change
COPY . .
CMD ["pytest", "tests/", "--browser", "chromium"]Build:
docker build -t my-playwright-tests .Run:
docker run --rm my-playwright-testsThe requirements.txt-first pattern is the canonical Docker layering trick. Docker caches each layer; your dependencies rarely change, so most rebuilds skip the pip install and only re-copy the code. Build times drop from minutes to seconds.
Mounting test artefacts back to the host
Tests run inside the container, but you want the reports on your laptop. Mount the output directory:
docker run --rm \
-v $(pwd)/test-results:/app/test-results \
-v $(pwd)/reports:/app/reports \
my-playwright-testsNow test-results/ and reports/ written by pytest inside the container appear in your project root after the run. The same flags work for CI — your GitHub Actions workflow mounts the runner's working directory and uploads the result.
Docker Compose for tests + app
When the app under test runs in a container too, use Compose to orchestrate both:
# docker-compose.yml
services:
app:
build: ./app
ports: ["3000:3000"]
tests:
build: .
depends_on:
- app
environment:
- BASE_URL=http://app:3000
volumes:
- ./reports:/app/reports
command: pytest tests/ --base-url http://app:3000Run both:
docker compose up --abort-on-container-exit --exit-code-from testsWhat this does:
appservice builds and starts the application container, exposing port 3000.testsservice builds the test image, waits forapp(depends_on), then runspytestagainsthttp://app:3000.--abort-on-container-exitstops the whole stack whentestsfinishes.--exit-code-from testsuses the test container's exit code as the overall exit code.
The two containers communicate via the Compose-managed Docker network — no localhost weirdness, no port conflicts.
GitLab CI with the official image
GitLab CI runs jobs in containers natively. Use the Playwright image directly — no Dockerfile required for simple cases:
playwright:
image: mcr.microsoft.com/playwright/python:v1.44.0-jammy
script:
- pip install -r requirements.txt
- pytest tests/ --browser chromium
artifacts:
when: always
paths:
- test-results/
- reports/
expire_in: 30 daysGitLab pulls the image, runs your script inside it, and uploads test-results/ and reports/ as artefacts. No playwright install --with-deps needed — the image already has everything.
GitHub Actions with container:
Same idea on GitHub:
jobs:
test:
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright/python:v1.44.0-jammy
steps:
- uses: actions/checkout@v4
- run: pip install -r requirements.txt
- run: pytest tests/ --browser chromium
- uses: actions/upload-artifact@v4
if: always()
with:
name: results
path: test-results/The container: block tells GitHub to run all subsequent steps inside the named image. You skip the setup-python and playwright install --with-deps steps because the image has both.
Local vs Docker — the comparison
Running Playwright Python locally vs inside Docker
Local (your machine)
Python version: whatever you have installed (often differs across the team)
Browser binaries: managed by playwright install — version drift possible
OS libs: macOS / Linux / Windows — different rendering, different fonts
Pros: fastest iteration, full IDE integration. Cons: 'works on my machine' bugs
Docker container
Python version: pinned by image tag — same across every developer and CI
Browser binaries: pre-installed in the image, version-locked to Playwright
OS libs: Ubuntu jammy/noble — identical font rendering everywhere
Pros: reproducible everywhere, identical CI = local. Cons: slower for hot-loop dev work
The pragmatic split most teams arrive at: develop locally (faster, IDE-integrated), run CI in Docker (consistent, reproducible). For visual tests specifically — where pixel-level rendering matters — generate baselines inside Docker so the team's macOS-vs-Linux differences don't cause false failures.
A complete local + Compose workflow
The shape that scales: one Dockerfile for the test image, one docker-compose.yml for the orchestration, a Makefile for the muscle memory.
# Makefile
.PHONY: test test-local test-docker
test-local:
pytest tests/
test-docker:
docker compose up --abort-on-container-exit --exit-code-from tests --build
test:
$(MAKE) test-local
clean:
docker compose down --volumes
rm -rf test-results reportsmake test runs locally for fast iteration; make test-docker runs the full container stack for pre-merge verification. CI calls test-docker. The team has one mental model — "the way we run tests" — that's the same on every machine.
Coming from Playwright TypeScript?
The TypeScript course's Docker chapter uses mcr.microsoft.com/playwright:v1.44.0-jammy (the Node-flavoured image). The Python equivalent is mcr.microsoft.com/playwright/python:v1.44.0-jammy — same registry, same tag scheme, same OS base. Everything else (Compose, GitLab, GitHub container:) is identical. Switching between TS and Python projects mostly means switching the image tag.
⚠️ Common mistakes
- Pinning the Playwright Python version differently from the image tag. If your
requirements.txtsaysplaywright==1.45.0and the image isv1.44.0-jammy, the pip install upgrades Playwright but the browsers stay at 1.44 — and Playwright refuses to launch them. Always match the image tag to the requirements pin. - Mounting the source tree without excluding
.venv/and__pycache__/. A-v $(pwd):/workmount shadows your container's/workwith the host directory. If your host has a.venv/from local Python, the container tries to use it (wrong arch, wrong OS) and fails. Either move tests into a subdirectory you mount specifically, or add a.dockerignore. - Forgetting
--exit-code-fromin Compose. Without it,docker compose upexits with code 0 even if your tests failed — CI thinks the run passed. Always specify the service whose exit code matters:--exit-code-from tests.
🎯 Practice task
Containerise your test suite. 30-40 minutes.
-
Create
Dockerfileat the project root:FROM mcr.microsoft.com/playwright/python:v1.44.0-jammy WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . CMD ["pytest", "tests/", "--browser", "chromium"] -
Build:
docker build -t my-playwright-tests .. The first build downloads the ~1 GB image; subsequent builds reuse the cache and finish in seconds. -
Run:
docker run --rm my-playwright-tests. The same tests you ran locally run inside the container with the bundled Chromium. -
Mount results back:
docker run --rm \ -v $(pwd)/test-results:/app/test-results \ -v $(pwd)/reports:/app/reports \ my-playwright-testsConfirm
test-results/andreports/appear in your project after the run. -
Try the one-shot pattern. Without building a custom image, run:
docker run -it --rm -v $(pwd):/work -w /work \ mcr.microsoft.com/playwright/python:v1.44.0-jammy \ bash -c "pip install -r requirements.txt && pytest tests/"Useful for quick experiments — no Dockerfile, no build step.
-
Add Compose if you have a local app to test against. Create
docker-compose.ymlwith bothappandtestsservices.docker compose up --abort-on-container-exit --exit-code-from testsruns both. -
Stretch: convert your GitHub Actions workflow to use the
container:block instead ofsetup-python+playwright install --with-deps. Compare the workflow timings — the container approach is typically 30-60s faster because the browsers are pre-installed.
You've got reproducible test environments. The last lesson of this chapter ties everything together — generating Allure reports in CI and publishing them where the team can see.