# Run Puppeteer in Docker: the official image vs a slim build, with memory measured

> Run Puppeteer in Docker: the official image or your own slim build, the three errors a slim build hits, fonts, and how much memory each page used.

Published 2026-11-08 · https://shareglint.com/en/guides/run-puppeteer-in-docker

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](https://pptr.dev/guides/docker) 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."

```dockerfile
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"]
```

```bash
docker run --init --cap-add=SYS_ADMIN my-screenshotter
```

Neither image has a colour emoji font; add the Debian package [`fonts-noto-color-emoji`](https://packages.debian.org/bookworm/fonts-noto-color-emoji) (and `fonts-noto-cjk` to a slim image), as measured in [emoji and fonts in Puppeteer](https://shareglint.com/en/guides/puppeteer-emoji-and-fonts).

## 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:

1. **"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.`
2. **apt `status=100`.** `npx puppeteer browsers install chrome --install-deps` installs Chrome's system libraries with apt; with the package lists already deleted by an earlier layer, apt could not find any package.
3. **"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:

```dockerfile
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

- `--init` reaps 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](https://shareglint.com/en/guides/puppeteer-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](https://shareglint.com/en/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](https://shareglint.com/en/pricing)). Keep your own container for pages behind a login, internal sites and scripted flows.

## FAQ

### What is the easiest way to run Puppeteer in Docker?

The official image, ghcr.io/puppeteer/puppeteer, pinned to your Puppeteer version. It has Chrome, its libraries and 125 fonts, runs as a non-root user, and needs --cap-add=SYS_ADMIN because Chrome runs sandboxed. Add --init so Chrome's child processes are reaped.

### Why does Puppeteer fail to install in node:slim?

In our build three things failed in turn: the browser download needs an unzip tool the slim image does not have ("no zip archiver is available"), --install-deps needs apt's package lists (deleted lists gave apt status 100), and Chrome refuses to start as root with its sandbox ("Running as root without --no-sandbox is not supported"). Install unzip, keep the lists until the deps are in, and run as a non-root user.

### How much memory does Puppeteer use in Docker?

In our test, Chrome's processes took about 280 MB after launch and grew to about 580 MB with five open pages (proportional set size, which splits shared memory fairly). Closing the pages gave back only about 40 MB; closing the browser freed it all. Budget per concurrent page and restart browsers regularly.
