DEV Community

Cover image for How to Run Playwright in Docker (and the Browser-Deps Tax It Adds)
Nico Acosta for Grabbit

Posted on • Originally published at grabbit.live

How to Run Playwright in Docker (and the Browser-Deps Tax It Adds)

Playwright in Docker is mostly a solved problem, and the solution is one line: use the official image. The reason people end up here anyway is the failure mode underneath it. Playwright drives a real browser, a real browser links against dozens of native shared libraries, and slim container base images do not ship them. Miss one and you get a launch error that names a .so file instead of anything to do with your test.

This guide covers the working setup, the errors that actually show up in CI, and the honest boundary where containerizing a browser stops being worth it.

Use the official image

Microsoft publishes an image with the browsers and their system dependencies preinstalled:

mcr.microsoft.com/playwright:v1.61.1-noble
Enter fullscreen mode Exit fullscreen mode

Pin the tag to the same Playwright version your project depends on. This is not a style preference. The npm package and the browser binaries are versioned together, and a mismatch produces a launch failure that reads like a corrupted install. If package.json says 1.61.1, the image tag says v1.61.1.

A working Dockerfile for a Node project:

FROM mcr.microsoft.com/playwright:v1.61.1-noble

WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .

CMD ["npx", "playwright", "test"]
Enter fullscreen mode Exit fullscreen mode

Note what is missing: no playwright install, no install-deps, no apt-get block. The image already has the browsers and the libraries. What it does not have is your node_modules, which is why npm ci still runs. That distinction, the browsers are in the image but the package is in your project, is the thing the Stack Overflow threads keep circling.

Run it:

docker build -t my-tests .
docker run --rm --ipc=host my-tests
Enter fullscreen mode Exit fullscreen mode

The --ipc=host flag is not optional in practice. Chromium uses shared memory heavily, Docker's default /dev/shm is 64MB, and the result is a browser that starts fine and then dies partway through a run. If you cannot use --ipc=host, use --shm-size=1gb instead.

Building your own image

Sometimes the official image does not fit: you need a specific base, a different runtime version, or a smaller final layer. Then you own the dependency problem, and the command that solves it is:

RUN npx playwright install --with-deps chromium
Enter fullscreen mode Exit fullscreen mode

--with-deps is the important half. Without it you get the browser binary and none of the system libraries it dynamically links against, which is exactly the error people paste into forums: libnss3.so: cannot open shared object file. Naming a single browser (chromium) rather than installing all three cuts image size substantially when your suite only targets one.

Two things to know before you go this route. Alpine is not supported, because Playwright's browser builds link against glibc and Alpine ships musl, so start from a Debian or Ubuntu base. And installing browsers yourself adds a slow, network-dependent layer to every rebuild that the official image has already paid for.

The errors you will actually hit

The container exits immediately. Usually correct behavior rather than a bug: a container lives as long as PID 1, and if PID 1 is playwright test, the container stops when the run ends. Check the exit code before assuming it crashed. A non-zero code means the tests failed; zero means it worked and finished.

Browser closed unexpectedly, mid-run. Shared memory. Use --ipc=host or --shm-size=1gb.

A missing .so file at launch. Either a custom image without --with-deps, or an Alpine base.

Works locally, fails in CI. Nearly always a version mismatch between the image tag and the installed Playwright package after a dependency bump. Pin both, bump both together.

Screenshots differ between your machine and the container. This is expected, not broken. Font rendering and available font families differ across environments, so pixel comparisons made on a laptop will not match baselines made in a container. The fix is to generate baselines in the same container that verifies them, which is the main reason visual regression suites get containerized at all. There is more on that in visual regression testing in Playwright CI.

When Docker is the right call, and when it is not

Containerizing Playwright buys you a reproducible browser environment. That is worth real setup cost when your suite is doing browser work: navigating multi-step flows, logging in, filling forms, asserting on state, comparing screenshots against baselines that must be byte-stable. In those cases you need the whole browser under your control, and a container is how you make "the whole browser" identical everywhere.

The calculation changes when the browser is incidental. A large share of Playwright-in-Docker setups exist to produce an image of a page: an OG card, a report thumbnail, a nightly capture of a dashboard, a preview for a marketing site. There the container is not giving you determinism you need, it is giving you a browser you are obligated to maintain, so that an HTTP request can come back with a PNG.

If that is the job, the render can move off your infrastructure entirely:

curl -sS https://api.grabbit.live/v1/grabs \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/pricing",
    "width": 1280,
    "full_page": true,
    "delay_ms": 1000,
    "format": "webp"
  }'
Enter fullscreen mode Exit fullscreen mode

The response carries an image_url pointing at the stored capture. No image to build, no browser version to pin, no --shm-size to remember, nothing to rebuild when a base image ships a new glibc.

The parameters map closely to what you would have written in Playwright: width (320 to 1920) pins the viewport the way setViewportSize does, full_page is the equivalent of fullPage: true, delay_ms (0 to 10000) replaces a fixed waitForTimeout after load, selector captures a single element the way a locator screenshot does, and format accepts png, jpeg, or webp.

The honest boundary: if the capture has to happen after a login, a click, or a form submission, none of this helps you and Playwright in a container is the correct tool. An API call renders a URL. It does not drive a session.

Cost, honestly

The relevant comparison is not per-request price against zero, because self-hosting is not free either. A containerized browser costs CI minutes on every build, image storage, and the recurring attention of whoever fixes it when a base image or Playwright version breaks the setup.

Grabbit is $0.002 per live grab, prepaid, with credits that do not reset monthly, which fits bursty work like a capture pipeline that runs hard some weeks and not at all in others. Test-environment keys return placeholder images at no cost, so you can wire the whole flow before any real capture runs. Other providers list lower per-grab rates, so if unit price is the only variable you care about, compare directly.

Where to go next

If you are keeping the container and just want the capture code to be right, how to take screenshots in Playwright covers the full-page, element, and CI cases. If you are weighing whether to keep running a browser at all, Puppeteer alternatives in 2026 walks the same decision from the other library, and the screenshot API page covers the hosted side in depth.


Originally published on the Grabbit blog.

Top comments (0)