Puppeteer runs in Docker without much trouble once the image has Chrome, the libraries Chrome needs, fonts, and a way to run sandboxed. The official image has all of that; a slim image you build yourself is smaller but trips over three errors on the way. This page compares the two with what we measured, then looks at memory, the number that decides how many screenshots one container can take at a time.
How we measured. On 2026-11-08 on Linux with Docker: ghcr.io/puppeteer/puppeteer:25.7.0 and our own build from node:24-slim (Node 24.22, npm 11.19) with Puppeteer 25.7.0, which downloads Chrome 152.0.7977.42 in both. Memory is the proportional set size (PSS) of all Chrome processes, read from /proc inside the container while five public home pages were open.
Two ways to get Chrome into an image
| Official image | node:24-slim + Puppeteer |
|
|---|---|---|
| Image size (uncompressed) | 2.03 GB | 1.1 GB |
| Chrome | 152.0.7977.42 | 152.0.7977.42 (652 MB in the cache) |
Fonts (fc-list) |
125 | 16 |
| Polish and other Latin text | correct | correct |
| Chinese, Japanese, Korean | correct | empty boxes |
| Emoji | outline or empty boxes | empty boxes |
| User | pptruser (non-root) |
root, unless you add one |
| Build time | none: pull | 75 s on our machine |
The official image is the shorter road: pin it to the Puppeteer version in your package.json, copy your script in, and run it. The Puppeteer Docker guide explains the one flag it needs: "The image is meant for running the browser in sandbox mode and therefore, running the image requires the SYS_ADMIN capability."
FROM ghcr.io/puppeteer/puppeteer:25.7.0
WORKDIR /home/pptruser/app
COPY --chown=pptruser:pptruser package*.json shot.mjs ./
RUN npm ci
CMD ["node", "shot.mjs"]
docker run --init --cap-add=SYS_ADMIN my-screenshotter
Neither image has a colour emoji font; add the Debian package fonts-noto-color-emoji (and fonts-noto-cjk to a slim image), as measured in emoji and fonts in Puppeteer.
A slim build, and the three errors on the way
The obvious Dockerfile — npm i puppeteer on node:24-slim — failed three times before it worked:
- "no zip archiver is available." Puppeteer downloads Chrome as a zip in its postinstall step, and the slim image has no
unzip:Install unzip (or tar.exe/Powershell on Windows), or add the optional yauzl dependency. - apt
status=100.npx puppeteer browsers install chrome --install-depsinstalls Chrome's system libraries with apt; with the package lists already deleted by an earlier layer, apt could not find any package. - "Running as root without --no-sandbox is not supported." The container runs as root by default, and Chrome will not start its sandbox as root.
The Dockerfile that worked:
FROM node:24-slim
# The browser download needs unzip; --install-deps needs apt's package lists.
RUN apt-get update && apt-get install -y --no-install-recommends unzip
WORKDIR /app
RUN npm init -y >/dev/null && npm i puppeteer@25.7.0 \
&& npx puppeteer browsers install chrome --install-deps \
&& rm -rf /var/lib/apt/lists/* /root/.npm
COPY shot.mjs ./
For the third error, add a non-root user (useradd, then USER) and run with --cap-add=SYS_ADMIN like the official image. --no-sandbox also makes Chrome start, and Puppeteer's docs strongly discourage it; it removes the wall between a page and your container, which matters most when the URLs come from users. The slim image is half the size, and most of what is left is Chrome itself.
Memory: what a page costs
Five home pages opened one after another in the official image, each captured once and kept open:
| Open pages | Chrome processes | Memory (PSS) |
|---|---|---|
| browser only | 13 | 281–284 MB |
| 1 (python.org) | 14 | 378–390 MB |
| 2 (+ w3.org) | 15 | 422–426 MB |
| 3 (+ nodejs.org) | 16 | 496–502 MB |
| 4 (+ rust-lang.org) | 17 | 542–544 MB |
| 5 (+ debian.org) | 18 | 579–581 MB |
| all 5 closed, 3 s later | 19 | 542–544 MB |
Two full runs, plus a third up to three pages; ranges are the spread. Two things follow:
- The browser is most of the cost. About 280 MB before any page, then 30–100 MB per page for these sites. A container that captures four pages at a time needs well over half a gigabyte for Chrome alone, plus your Node process and the page cache. Heavier pages — maps, video, large single-page apps — cost much more.
- Closing pages does not give memory back. After closing all five pages, memory dropped by under 40 MB and the process count still rose. Closing the browser ends its processes and with them all of that memory. A long-running worker should restart its browser every so many captures, not just close pages.
PSS splits memory shared between processes fairly. Summing each process's RSS instead gave 780 MB for the bare browser in the same container, almost three times as much: shared libraries were counted once per process.
Other flags that matter
--initreaps Chrome's zombie processes.- Docker gives containers a 64 MB
/dev/shm; launch Chrome with--disable-dev-shm-usage(we did) or run with--shm-size=1g, or large pages crash the tab. - Two of our four memory runs failed during a screenshot, one of them on the fourth page with
Protocol error (Page.captureScreenshot): Not attached to an active page: the page was replaced under the script. Retry a failed capture once with a fresh page; see execution context destroyed for the related navigation errors.
Or no container at all
If the container exists only to take screenshots, the same capture is one HTTP request to a screenshot API: Chrome, fonts (colour emoji included), sandboxing, restarts and an egress proxy that keeps user URLs away from your network are run for you. A capture costs 1 render of the quota, a full page 2 (pricing). Keep your own container for pages behind a login, internal sites and scripted flows.