PDFPipe

Managed containers / Docker

Playwright on Docker: browsers downloaded outside the image layer

The image builds, and at runtime the browser is missing, or the container downloads it again on every start.

What is actually wrong

Playwright installs browsers into a cache in the home directory of whichever user ran the install. Install as root in a build step and then run as a non-root user and the cache is in the wrong home. Install after switching users and the browser download becomes part of a layer that gets invalidated by every source change.

What Docker gives you to work with

an image you build, which means every system library and font is present only because you put it there.

  • Packaging: no limit, but image size is a real cost: it is pulled on every node that runs it and it is rebuilt on every deploy.
  • Filesystem: fully writable. The constraint that surprises people is /dev/shm, which defaults to 64 MB and is not the same thing as the container's memory limit.
  • Processes: full control, with one catch: the process you start is PID 1, and PID 1 does not reap orphaned children unless it is written to. Browser processes accumulate as zombies.

The fix, if you are keeping Playwright

Pin the cache to an explicit path, install as part of a layer that only depends on the lockfile, and make sure the runtime user can read it. Ordering matters here as much as the commands do.

dockerfile
FROM node:20-slim
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright

WORKDIR /app
# Lockfile first, so the browser download is cached across source changes.
COPY package*.json ./
RUN npm ci && npx playwright install --with-deps chromium \
 && chmod -R 755 /ms-playwright

COPY . .
USER node

What still hurts after that

The Playwright library version and the browser build it expects are coupled, so a version bump in the lockfile invalidates the layer and redownloads several hundred megabytes. The dependency installer needs root, so it cannot run in a build that has already dropped privileges, which constrains the order of the Dockerfile in a way that is easy to break during a refactor.

What Playwright is genuinely good at

a better API than Puppeteer for waiting on real page state, and a first-party dependency installer that knows which system packages the browser needs. That is worth keeping in view: the problem on this page is where it runs, not what it produces. Playwright is a browser automation library that installs its browsers outside node_modules, into a shared cache directory, at install time. It needs the same browser and shared libraries as Puppeteer, plus an install step that runs separately from the package install and writes to a location your deployment probably does not copy.

The other shape of the answer

Every fix above keeps the renderer inside your deployment, which means the constraint stays yours: the package size, the shared libraries, the fonts, the memory, the patching. The alternative is an HTTP call from Docker to something that already has a browser in it, which turns all of the above into a request and a response. That is what this API is, and it is the reason these pages exist. Whether that trade is right depends on whether rendering is a thing you want to operate.

Frequently asked

Can Playwright run on Docker at all?

Pin the cache to an explicit path, install as part of a layer that only depends on the lockfile, and make sure the runtime user can read it. Ordering matters here as much as the commands do. Whether that is worth doing is a separate question from whether it is possible, and the section above on what still hurts is the honest answer to it.

Why does it work locally?

Because a development machine has root, a package manager, several gigabytes of memory, a full font set and no packaging limit. Every one of those is a thing Docker constrains. A renderer that works locally and fails on deploy has not regressed: it has met the first environment that does not give it everything.

Related failures

The same platform with a different renderer, and the same renderer on other platforms.

Puppeteer on Docker: an image with no libraries and no fonts

The launch fails with "error while loading shared libraries: libnss3.so", and once that is fixed every character in the PDF is an empty rectangle.

wkhtmltopdf on Docker: the apt package is the wrong build

It installs cleanly, it renders, and the header, the footer and the page numbers are silently missing.

WeasyPrint on Docker: a Pango version that does not match

It installs and runs, and the text is subtly wrong: incorrect kerning, broken ligatures, or an exception about a symbol that is not found in a shared library.

Playwright on AWS Lambda: browsers that are not in your bundle

At runtime, not at deploy time: "Executable doesn't exist at /home/sbx_user1051/.cache/ms-playwright/chromium-.../chrome-linux/chrome".

Playwright on Vercel: an install step the build never runs

Deploy succeeds, first request fails with "Executable doesn't exist at ...ms-playwright/chromium-...".

Playwright on Google Cloud Run: a two gigabyte image on a cold start

Everything works, and the first request after a quiet period takes many seconds before any of your code runs.

Playwright on Kubernetes: an image that slows every scale-up

Autoscaling responds to load and the new pods take a long time to become ready, so the spike is over before the capacity arrives.

Every renderer, on every platform

The full grid, grouped by what kind of environment the platform is.

What goes wrong when HTML becomes a PDF

Once it deploys, the next set of problems is in the output rather than the infrastructure.

If the render moves off this platform, none of the above is your problem to keep solving. 100 free documents a month, and a playground that needs no signup.