How to Fix Puppeteer Could Not Find Chrome in Docker
Fix Puppeteer’s missing Chrome error in Docker with reliable browser installs, cache paths, executable checks, and production diagnostics.
Short answer: “Could not find Chrome” means Puppeteer cannot resolve the browser binary it expects inside the container. Install the browser during the image build and carry its cache into the final image, or install a system Chrome/Chromium and pass its real in-container path with executablePath. If the binary is found but will not start, troubleshoot Linux shared libraries separately.
This guide covers both setup models, multi-stage images, runtime users, version pinning, launch flags, diagnostics, and production trade-offs.
1. Confirm what Puppeteer is trying to launch
Check the dependency and lockfile in the same directory used by the Docker build:
node -p "require('puppeteer/package.json').version"
node -p "require.resolve('puppeteer')"
node -p "require('./package.json').dependencies?.puppeteer || require('./package.json').devDependencies?.puppeteer || 'not installed'"
puppeteer normally downloads a compatible Chrome for Testing browser. puppeteer-core does not download a browser; your image or platform must provide one, and your code must select it. See the official installation guide.
2. Fix the common case: install the managed browser in the image
Package-manager policies can skip Puppeteer’s postinstall script. The package then exists, but its browser does not. Make browser installation an explicit build step after dependencies are present.
FROM node:22-bookworm-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci
RUN npx puppeteer browsers install
COPY . .
ENV NODE_ENV=production
CMD ["node", "server.js"]
docker build -t puppeteer-shot .
docker run --rm --init puppeteer-shot
If your CI uses npm ci --ignore-scripts or disabled package scripts, retain the explicit install step. Run it in the application directory so it sees the intended dependency and configuration.
Make the cache survive the build
By default, Puppeteer stores browsers under ~/.cache/puppeteer. A browser installed as root can be invisible when the container runs as another user, and a browser installed in a discarded build stage is absent from the final stage.
FROM node:22-bookworm-slim
ENV PUPPETEER_CACHE_DIR=/opt/puppeteer-cache
WORKDIR /app
COPY package*.json ./
RUN npm ci
RUN npx puppeteer browsers install
COPY . .
RUN useradd --create-home --uid 10001 appuser \
&& chown -R appuser:appuser /app /opt/puppeteer-cache
USER appuser
CMD ["node", "server.js"]
Use the same PUPPETEER_CACHE_DIR at install and runtime. In a multi-stage build, copy that directory into the final stage.
FROM node:22-bookworm-slim AS build
WORKDIR /app
ENV PUPPETEER_CACHE_DIR=/opt/puppeteer-cache
COPY package*.json ./
RUN npm ci && npx puppeteer browsers install
COPY . .
FROM node:22-bookworm-slim
WORKDIR /app
ENV NODE_ENV=production PUPPETEER_CACHE_DIR=/opt/puppeteer-cache
COPY --from=build /app /app
COPY --from=build /opt/puppeteer-cache /opt/puppeteer-cache
CMD ["node", "server.js"]
3. Use a system Chrome or Chromium deliberately
This path is useful when your base image owns browser updates or when you use puppeteer-core. Installing a package alone does not prove Puppeteer will discover it. Locate the executable in the image and pass that exact path.
FROM node:22-bookworm-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends chromium \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
CMD ["node", "server.js"]
const puppeteer = require('puppeteer-core');
(async () => {
const browser = await puppeteer.launch({
executablePath: process.env.CHROME_PATH || '/usr/bin/chromium',
headless: true
});
const page = await browser.newPage();
await page.goto('https://example.com', {waitUntil: 'networkidle2'});
await page.screenshot({path: 'example.png', fullPage: true});
await browser.close();
})();
docker run --rm -it puppeteer-shot sh
command -v chromium || command -v chromium-browser || command -v google-chrome
ls -l /usr/bin/chromium
When you manage browsers yourself, call puppeteer.launch with executablePath (or a standard channel). Do not mix an arbitrary system browser with a managed cache without checking compatibility.
4. A minimal launch program for testing
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({headless: true});
const page = await browser.newPage();
await page.goto('https://example.com', {waitUntil: 'domcontentloaded', timeout: 30000});
console.log(await page.title());
await browser.close();
})();
If this prints “Could not find Chrome (ver. …)”, fix installation or cache visibility. If it reaches a spawn error, continue with dependency checks.
5. Official Puppeteer Docker image
The official image bundles Chrome for Testing, required dependencies, and a pre-installed Puppeteer version. Its tags follow Puppeteer versions, so pin a tag compatible with your application. The Docker guide says sandbox mode requires the SYS_ADMIN capability and recommends an init process such as --init.
docker run --rm --init --cap-add=SYS_ADMIN \
-v "$PWD:/app" -w /app ghcr.io/puppeteer/puppeteer:24.31.0 \
node smoke.js
Pin image and package versions together. A custom image requires explicit browser installation, cache ownership, OS libraries, sandbox settings, and process cleanup.
6. Separate “browser missing” from “browser will not launch”
CHROME_BIN="$(command -v chromium || command -v google-chrome)"
echo "$CHROME_BIN"
ldd "$CHROME_BIN" | grep not || true
"$CHROME_BIN" --version
Lines ending in not found identify missing libraries. Install the packages required by your distribution and rebuild. Do not treat --no-sandbox as a universal fix; it changes the security model.
7. Diagnostic checklist
- Read the Puppeteer version from the lockfile and runtime package.
- Confirm
puppeteerversuspuppeteer-core. - Check whether install scripts were disabled.
- Run
npx puppeteer browsers installduring the image build. - Check
PUPPETEER_CACHE_DIRandHOMEin build and runtime layers. - Confirm the cache exists in the final image and is readable by the runtime user.
- For system Chrome, run
command -vinside the container and pass that path. - Run a minimal smoke program before the full workload.
- If the error changed to a spawn or library error, run
ldd ... | grep not. - Pin compatible Puppeteer, browser, and image versions.
8. Common errors and fixes
| Error | Cause | Fix |
|---|---|---|
Could not find Chrome (ver. …) |
Download skipped. | Run npx puppeteer browsers install in the image. |
| Build works, runtime fails | Different user, home, or cache. | Set one cache directory, copy it to the final stage, and grant read access. |
puppeteer-core fails |
Core does not download a browser. | Install Chrome/Chromium and set executablePath. |
| System Chrome ignored | Puppeteer checks its managed cache. | Set executablePath or a supported channel. |
spawn ... ENOENT |
Wrong or absent path. | Use command -v inside the image and correct the copy step. |
| Shared-library error | OS dependencies missing. | Run ldd chrome | grep not and install reported libraries. |
| Browser exits as root | Sandbox/capability mismatch. | Use a non-root user and follow the image’s sandbox setup. |
| Multi-stage runtime fails | Cache stayed in discarded stage. | Copy cache and configuration into final stage. |
| Intermittent post-update failures | Version drift. | Pin versions and update them together after a smoke test. |
9. Reliability, performance, and cost notes
- Builds: cache the Docker layer containing browser installation; invalidate it when Puppeteer changes.
- Latency: reuse one browser and create pages when practical; close resources on shutdown.
- Concurrency: bound pages and browser processes to avoid memory and file-descriptor exhaustion.
- Reliability: use
--initor an equivalent init process and run a smoke check in CI. - Security: retain Chrome’s sandbox where possible and run as a dedicated non-root user.
- Cost: self-hosting shifts spend to CI storage, image transfer, CPU, and memory; a managed API removes browser maintenance when you only need rendered images or PDFs.
10. Or skip the browser setup
If you need a screenshot endpoint instead of maintaining Chrome in Docker, ScreenshotNeo accepts one GET request and returns PNG, JPEG, WebP, or PDF. See the API documentation.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
const image = Buffer.from(await res.arrayBuffer());
require('fs').writeFileSync('shot.webp', image);
Cookie banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, failed loads, timeouts, and cache hits are never billed, and response headers report verdict and billing status. An MCP server provides take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.
11. FAQ
Do I need both Puppeteer and Chrome?
You need Puppeteer plus a compatible browser. puppeteer can download one; puppeteer-core requires you to provide and select one.
Why does changing the Docker user break the fix?
The default cache is under the installing user’s home directory. A different runtime user may have a different home and no access.
Should I use the official image?
Use it for a bundled browser and dependencies. Use a custom image for control over the base OS or system browser.
Does --no-sandbox fix “Chrome not found”?
No. It cannot create a missing binary; it only changes launch security after a browser is found.


