Docker Containers for Playwright Python

8 min read

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-jammy

The tag has two parts:

  • v1.44.0 — the Playwright version. Pins the bundled browsers to known-good versions.
  • -jammy — the Ubuntu base. jammy is 22.04, noble is 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 chromium

What 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 /work inside the container.
  • -w /work — sets /work as 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-tests

The 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-tests

Now 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:3000

Run both:

docker compose up --abort-on-container-exit --exit-code-from tests

What this does:

  • app service builds and starts the application container, exposing port 3000.
  • tests service builds the test image, waits for app (depends_on), then runs pytest against http://app:3000.
  • --abort-on-container-exit stops the whole stack when tests finishes.
  • --exit-code-from tests uses 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 days

GitLab 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 reports

make 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.txt says playwright==1.45.0 and the image is v1.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):/work mount shadows your container's /work with 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-from in Compose. Without it, docker compose up exits 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.

  1. Create Dockerfile at 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"]
  2. Build: docker build -t my-playwright-tests .. The first build downloads the ~1 GB image; subsequent builds reuse the cache and finish in seconds.

  3. Run: docker run --rm my-playwright-tests. The same tests you ran locally run inside the container with the bundled Chromium.

  4. Mount results back:

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

    Confirm test-results/ and reports/ appear in your project after the run.

  5. 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.

  6. Add Compose if you have a local app to test against. Create docker-compose.yml with both app and tests services. docker compose up --abort-on-container-exit --exit-code-from tests runs both.

  7. Stretch: convert your GitHub Actions workflow to use the container: block instead of setup-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.

// tip to track lessons you complete and pick up where you left off across devices.