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