PDFPipe

Managed containers / Docker

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.

What is actually wrong

Two separate omissions that arrive one after the other. A slim base image has none of the twenty or so libraries Chromium links against, so the browser cannot start. Once those are installed the browser starts and the image still has no fonts, so the font matcher has nothing to match and every glyph falls back to a box.

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 Puppeteer

Install the browser's dependencies and at least one font family that covers the scripts your documents use. Then handle the two container-specific problems: Chromium's sandbox needs kernel privileges a default container does not have, and the process you started is PID 1 so it will not reap the browser's orphaned children.

dockerfile
FROM node:20-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
      libnss3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 \
      libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 \
      libxrandr2 libgbm1 libasound2 libpangocairo-1.0-0 \
      fonts-liberation fonts-noto-color-emoji \
      dumb-init \
 && rm -rf /var/lib/apt/lists/*

# Not root, so the browser sandbox has something to drop to.
USER node
# An init that reaps orphaned renderer processes.
ENTRYPOINT ["dumb-init", "--"]

What still hurts after that

The font list is the part that keeps costing you. fonts-liberation covers Latin and nothing else, so the first invoice with a customer name in Chinese, Arabic or Devanagari comes out as boxes, in production, from a code path that was never touched. Every font family the documents might use has to be in the image, and the image grows accordingly. Running with the sandbox disabled to avoid the privilege question means rendering untrusted HTML in an unsandboxed browser, which is a security decision that deserves to be made explicitly.

What Puppeteer is genuinely good at

it renders exactly what a browser renders, which means modern CSS, web fonts and client-side JavaScript all behave the way they do in a tab. That is worth keeping in view: the problem on this page is where it runs, not what it produces. Puppeteer is a Node library that drives a real Chromium, and downloads that Chromium into node_modules when it is installed. It needs the browser binary, a long list of shared libraries the browser links against, fonts, and enough memory for a browser process that is not accounted for by your runtime's heap settings.

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 Puppeteer run on Docker at all?

Install the browser's dependencies and at least one font family that covers the scripts your documents use. Then handle the two container-specific problems: Chromium's sandbox needs kernel privileges a default container does not have, and the process you started is PID 1 so it will not reap the browser's orphaned children. 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.

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.

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.

Puppeteer on AWS Lambda: the 250 MB package limit

The deploy fails before the function ever runs, with an error naming the unzipped size of the code and layers.

Puppeteer on Vercel: a bundler that cannot see the browser

The build succeeds. The function either exceeds the size limit at deploy time, or deploys and then fails on the first request because the browser executable is not where the library expects it.

Puppeteer on Netlify Functions: esbuild does not copy binaries

The function deploys and then fails at runtime saying the Chromium revision could not be found.

Puppeteer on Google Cloud Run: 64 MB of shared memory

Renders succeed in testing and fail under load with "Target closed", "Protocol error (Page.navigate): Session closed" or a tab that simply disappears.

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.