How to Run Puppeteer in a Docker Container
Run Puppeteer with Chrome in Docker using the official image or a custom Dockerfile, with sandbox, dependencies, read-only filesystems, and fixes.

Use Puppeteer’s published image when you want the shortest path: ghcr.io/puppeteer/puppeteer includes Chrome for Testing, required dependencies, and a compatible Puppeteer installation. Run it with Docker’s init process and the capability shown in Puppeteer’s guide so Chrome can keep its sandbox enabled. For a custom image, start from the same dependency model, pin the Node, Puppeteer, and browser versions together, and make Chrome’s profile and cache directories writable.
This guide shows both approaches, explains why Chrome launch failures happen, and covers sandboxing, missing libraries, read-only containers, Alpine Linux, browser downloads, performance, and operational troubleshooting.
1. Choose an installation approach
| Approach | Best for | Trade-offs |
|---|---|---|
| Official image | Fast setup with known browser dependencies | Less control over the base image; tags move unless pinned |
| Custom Debian/Ubuntu image | Production images with your own libraries, users, and process model | You own browser and OS compatibility |
| Alpine-based image | Only when Alpine is a hard platform requirement | Chrome does not support Alpine out of the box; compatibility work is required |
The official Docker guide is the safest starting point because its image, browser, and package set are designed to work together. Read the Puppeteer Docker guide before pinning a production tag. The current system requirements page lists Node 22.12 or newer for current Puppeteer releases and documents supported Chrome for Testing Linux environments; verify the requirements for the exact version you select at build time.
2. Run the official Puppeteer image
Pull the image and run a small script directly through Node:
docker pull ghcr.io/puppeteer/puppeteer:latest
docker run -i --init --cap-add=SYS_ADMIN --rm \
ghcr.io/puppeteer/puppeteer:latest \
node -e '
const puppeteer = require("puppeteer");
(async () => {
const browser = await puppeteer.launch({headless: "new"});
const page = await browser.newPage();
await page.goto("https://example.com", {waitUntil: "networkidle2"});
await page.screenshot({path: "/tmp/example.png", fullPage: true});
await browser.close();
})().catch(error => { console.error(error); process.exit(1); });
'
The documented image is hosted in GitHub Container Registry. The latest tag changes over time; version tags correspond to Puppeteer versions, so pin a tag for repeatable builds. The --init flag supplies an init process that reaps child processes. Puppeteer’s example adds --cap-add=SYS_ADMIN because the image is intended to run Chrome with its sandbox enabled. Capability requirements can differ on managed container platforms, so validate them against your runtime. These details come from the official guide.
Mount a script instead of using node -e
Create capture.js:
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({
headless: 'new',
// Keep the sandbox enabled. Do not add --no-sandbox unless the page is trusted
// and your environment cannot provide a working sandbox.
});
try {
const page = await browser.newPage();
await page.setViewport({width: 1440, height: 900, deviceScaleFactor: 1});
await page.goto(process.env.TARGET_URL || 'https://example.com', {
waitUntil: 'networkidle2',
timeout: 60000
});
await page.screenshot({path: '/work/shot.png', fullPage: true});
} finally {
await browser.close();
}
})().catch(error => {
console.error(error);
process.exitCode = 1;
});
Run it with a writable output directory:
mkdir -p out
docker run --rm -i --init --cap-add=SYS_ADMIN \
-e TARGET_URL=https://example.com \
-v "$PWD/capture.js:/work/capture.js:ro" \
-v "$PWD/out:/work" \
ghcr.io/puppeteer/puppeteer:latest node /work/capture.js
3. Build a custom Docker image
A custom image must align four components: Node, the Puppeteer package, the browser binary, and the operating-system libraries Chrome loads at startup. Use the official Puppeteer Dockerfile as your reference instead of copying an old dependency list from a blog post.

A minimal Debian-based pattern looks like this:
FROM node:22-bookworm
# Install Puppeteer and its browser during the image build.
WORKDIR /app
COPY package*.json ./
RUN npm ci
RUN npx puppeteer browsers install chrome
COPY capture.js ./
# Run as a non-root user in production when your image and sandbox setup allow it.
USER node
ENTRYPOINT ["node", "capture.js"]
Your package.json should pin Puppeteer rather than using a floating range:
{
"private": true,
"dependencies": {
"puppeteer": "25.12.0"
}
}
The exact version above is an example of a pinning pattern; choose a version that matches your application and re-check the current requirements before publishing. If installation scripts are disabled in your build environment, Puppeteer may skip downloading a browser. Check your package-manager configuration and the resulting cache before running the container. Puppeteer documents intentional browser management through its configuration API, including executablePath and download controls.
Use an existing Chrome installation
If your base image installs Chrome or Chromium separately, point Puppeteer at that binary:
const browser = await puppeteer.launch({
executablePath: process.env.PUPPETEER_EXECUTABLE_PATH,
headless: 'new'
});
Do not mix an arbitrary browser build with an unrelated Puppeteer version without checking compatibility. A browser that starts may still fail later on protocol features, fonts, or sandbox behavior.
4. Keep Chrome’s sandbox enabled
Chrome’s sandbox is a security boundary. Puppeteer’s troubleshooting documentation strongly discourages running with --no-sandbox and describes it only as a last resort when the opened content is absolutely trusted. First fix the container permissions and runtime:
- Use the official image’s documented
--cap-add=SYS_ADMINsetting where permitted. - Run with
--initor an init-capable entrypoint. - Prefer a non-root runtime user with the required sandbox support.
- Check whether your orchestrator strips Linux capabilities or applies a restrictive security profile.
If a platform cannot provide a working sandbox, evaluate the risk of the pages you will open before considering --no-sandbox. Treat that flag as an environment exception, not a standard Puppeteer launch option.
5. Handle read-only containers
Chrome writes a profile, configuration files, cache data, and crash-report information. A read-only root filesystem therefore needs writable temporary paths and a writable user-data directory. Puppeteer’s troubleshooting guide recommends putting these locations in /tmp or mounting writable directories owned by the browser user.
const browser = await puppeteer.launch({
headless: 'new',
userDataDir: '/tmp/puppeteer-profile',
args: [
'--disable-dev-shm-usage'
]
});
At runtime, mount a writable temporary directory if your platform does not provide one:
docker run --rm -i --init --cap-add=SYS_ADMIN \
--read-only \
--tmpfs /tmp:rw,nosuid,nodev,size=512m \
ghcr.io/puppeteer/puppeteer:latest node /work/capture.js
--disable-dev-shm-usage makes Chrome use /tmp instead of the container’s often-small shared-memory mount. It can prevent crashes on pages with large render surfaces, but a larger /dev/shm mount is usually preferable when your runtime supports it:
docker run --rm --shm-size=1g --init --cap-add=SYS_ADMIN \
ghcr.io/puppeteer/puppeteer:latest node /work/capture.js
6. Diagnose missing dependencies
When Chrome exits immediately with a missing-library error, inspect the browser executable with ldd. The output identifies shared libraries that are not found. Install the missing packages from the supported Debian or Ubuntu repositories, then rebuild the image.
ldd /path/to/chrome | grep 'not found'
Puppeteer’s Linux troubleshooting page warns that dependency lists can become outdated. Resolve the list against the exact browser and distribution in your image. For Debian or Ubuntu, the browser installation tooling can install dependencies for Chrome when run as root:
npx puppeteer browsers install chrome --install-deps
Run that command during an image build stage with the privileges it requires, then drop privileges for the application process.
7. Alpine Linux and browser compatibility
Chrome does not support Alpine out of the box. Alpine uses musl libc, while many Chrome builds and dependency instructions target Debian or Ubuntu’s glibc environment. If Alpine is mandatory, select a Chromium/Puppeteer combination documented for your exact Alpine release, install every required compatibility package, and verify the executable with ldd. The official Puppeteer troubleshooting guidance treats this as a compatibility task rather than a drop-in image change.
For most teams, a Debian or Ubuntu base reduces maintenance because the supported Chrome for Testing environments and dependency installation instructions target those distributions.
8. Useful Puppeteer capture options
| Need | Example | Why it matters in Docker |
|---|---|---|
| Wait for navigation | waitUntil: 'networkidle2' |
Reduces captures taken before late requests finish; set an explicit timeout. |
| Full page | fullPage: true |
Can create very tall render surfaces and increase memory use. |
| Element screenshot | page.locator('.card').screenshot(...) |
Limits work when only one component is needed. |
| Viewport and retina | setViewport({width, height, deviceScaleFactor}) |
Controls layout and output pixels consistently across runs. |
| Selector readiness | await page.waitForSelector('.app') |
More deterministic than a fixed sleep for client-rendered pages. |
| Network interception | page.setRequestInterception(true) |
Can block analytics or large resources, but may change page behavior. |
Use a selector wait for an application-specific readiness signal, then capture. A fixed delay is useful for simple pages but tends to be either too short under load or unnecessarily slow when the page is fast.
9. Troubleshooting common launch failures
| Symptom | Likely cause | Fix |
|---|---|---|
Failed to launch the browser process |
Missing shared libraries or incompatible browser | Run ldd, install supported packages, and align Puppeteer with the browser version. |
No usable sandbox or setuid errors |
Container permissions, root execution, or stripped capabilities | Use the documented sandbox setup and SYS_ADMIN capability where allowed; avoid disabling the sandbox. |
| Zombie Chrome processes | No init process reaping child processes | Add Docker --init or use a proper init entrypoint. |
| Crashpad or profile errors | Read-only filesystem or wrong directory ownership | Set userDataDir under writable /tmp and mount writable cache/config paths. |
| Browser executable not found | Download skipped or executable path is wrong | Inspect build logs, enable the intended browser download, or set executablePath explicitly. |
| Renderer crashes on large pages | Small shared memory mount or excessive page size | Increase --shm-size, use --disable-dev-shm-usage, close pages promptly, and reduce capture dimensions. |
| Works locally, fails in CI | Different architecture, Node version, security profile, or fonts | Pin the image tag, print versions at startup, and reproduce with the same container runtime. |
10. Reliability, performance, and cost considerations
Reliability
- Pin the container image and Puppeteer dependency instead of using
latestin production. - Log Node, Puppeteer, browser, and kernel/container runtime versions at startup.
- Set navigation and overall job timeouts so a stalled page cannot occupy a worker forever.
- Always close the page and browser in
finallyblocks. - Use a queue or worker limit when processing many URLs; each browser consumes memory and creates child processes.
- Retry only transient navigation failures. Do not blindly retry deterministic errors such as an invalid executable path or missing library.
Performance
Launching a browser for every URL is expensive. Reuse a browser process when jobs are trusted and isolated with separate pages or contexts, but close idle pages and impose concurrency limits. Full-page screenshots, high device scale factors, large PDFs, and pages with many animations increase CPU, memory, and output size. Blocking nonessential requests can help, but blocking fonts, scripts, or APIs may produce an inaccurate image.
Cost
Docker itself does not charge per screenshot, but you pay for compute, memory, storage, registry traffic, and operational time. A larger image can increase cold-start and transfer time. A browser image that already contains compatible dependencies can reduce build maintenance even when its compressed size is greater.
11. Or skip the browser setup
If your goal is a clean website screenshot rather than operating Chrome, ScreenshotNeo provides a website screenshot API and MCP server. It accepts one GET request and returns PNG, JPEG, WebP, or PDF. The API removes cookie and consent banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and each response reports the result through X-Page-Verdict and X-Billed headers.

See the ScreenshotNeo API documentation for all options. A minimal request is:
cURL
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
-o shot.webp
Python
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
Node.js
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://stripe.com'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
require('fs').writeFileSync('shot.webp', Buffer.from(await res.arrayBuffer()));
ScreenshotNeo also supports full-page captures with lazy images loaded, CSS-selector element shots, dark mode, device presets and custom viewports, retina scale, PDF paper sizes and page ranges, custom CSS and JavaScript, clicks, selector or network-idle waits, request/resource blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, configurable caching, signed links, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, usage reporting, and an OpenAPI specification. An MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.
There are 1,000 free shots per month with no card. Paid plans start at $5 for 3,000 shots; every feature is available on every plan. Create a free ScreenshotNeo account.
12. FAQ
Can Puppeteer run in a read-only Docker container?
Yes, if Chrome’s profile, cache, and temporary files point to writable paths such as a mounted or tmpfs-backed /tmp directory. Set an explicit userDataDir and verify ownership.
Do I always need --cap-add=SYS_ADMIN?
The official image’s documented command uses it for sandboxed Chrome. Your runtime may provide equivalent permissions or enforce a different security policy; verify the platform rather than assuming the flag is universal.
Is --no-sandbox a valid fix?
It can bypass a sandbox failure, but Puppeteer strongly discourages it. Fix the container sandbox configuration first and use the flag only for absolutely trusted content when no safer setup is possible.
Why does a screenshot differ between my laptop and Docker?
Viewport size, device scale factor, fonts, browser version, timezone, locale, GPU behavior, and missing assets can all change rendering. Pin the image and explicitly set the page environment that matters to your comparison.
Should I use Alpine to make the image smaller?
Only when you can maintain the browser compatibility work. Chrome does not support Alpine out of the box, so Debian or Ubuntu is usually the lower-maintenance choice.


