ScreenshotNeo

BlogHow-to

How to Fix Chromium Launch Failures in Puppeteer Docker Containers

Fix Puppeteer Chromium launch errors in Docker by checking dependencies, browser paths, sandboxing, writable storage, users, and image compatibility.

By the ScreenshotNeo team1 October 20269 min read

Most Puppeteer launch failures in Docker are environment problems. The container may lack shared libraries or fonts, the browser may not be installed in the runtime image, Puppeteer may be looking in the wrong cache, Chrome may not have a usable sandbox, or its profile directories may be read-only. Identify the failing layer first, then fix that layer.

The quickest reproducible baseline is Puppeteer’s maintained Docker image, which includes Chrome for Testing and the dependencies Puppeteer expects. If you build your own image, install the browser and libraries together, run as a non-root user, provide a writable profile, and set executablePath when using a system browser.

1. Identify the failing layer

Save the complete Puppeteer exception and Chromium’s stderr. These messages point to different fixes:

Message or symptom Likely cause First fix
Could not find Chrome, browser download/cache errors Browser was not downloaded, is absent from the runtime image, or the cache is not visible to the runtime user. Verify the browser exists; set executablePath or fix the Puppeteer cache.
No usable sandbox! Container capabilities or sandbox configuration do not permit Chrome’s sandbox. Keep the sandbox and configure the container; use --no-sandbox only as an explicit fallback for trusted content.
error while loading shared libraries: ... .so Required Debian/Ubuntu libraries are missing. Install the browser dependencies and fonts in the image.
Failed to launch the browser process with permission errors Runtime user cannot execute the browser or write its profile/cache. Check ownership, permissions, userDataDir, and XDG directories.
chrome_crashpad_handler: --database is required Chrome cannot create its crashpad/profile data, often in a read-only container. Point XDG config/cache and the Puppeteer profile at writable paths.
Launch timeout or immediate exit Browser startup, dependency, sandbox, or resource problem. Increase diagnostic logging, run the image interactively, and inspect stderr.
const puppeteer = require('puppeteer');

(async () => {
  try {
    const browser = await puppeteer.launch({
      dumpio: true,
      timeout: 30000
    });
    console.log('browser started');
    await browser.close();
  } catch (error) {
    console.error(error.stack || error);
    process.exitCode = 1;
  }
})();

Run the same container interactively and inspect the runtime, rather than only the build stage:

docker run --rm -it --init your-image:tag sh
node -p "process.version"
which google-chrome || which chromium || which chromium-browser || true
ls -l /usr/bin/google-chrome /usr/bin/chromium /usr/bin/chromium-browser 2>/dev/null || true
find / -type f \( -name 'chrome' -o -name 'chromium' -o -name 'chrome-headless-shell' \) 2>/dev/null | head

2. Start with the official Puppeteer Docker image

Puppeteer’s troubleshooting documentation describes an official image distributed through the GitHub Container Registry. It is the fastest way to establish a known-good Chrome for Testing and dependency baseline. The documented run pattern uses an init process and SYS_ADMIN so Chrome can use its sandbox. See the Puppeteer troubleshooting guide and current troubleshooting documentation for image names and tags.

docker run -i --init --cap-add=SYS_ADMIN \
  --rm your-puppeteer-image:tag \
  node your-script.js

Pin the image tag in deployments and upgrade it deliberately. This keeps browser and library changes visible instead of allowing an unrelated base-image update to break launches.

3. Build a deterministic Debian or Ubuntu image

A custom image is appropriate when you need a particular runtime, OS packages, fonts, or process layout. Install the browser and its libraries in the same deliberate build path, and ensure the runtime stage contains everything installed during the build.

FROM node:22-bookworm

ENV PUPPETEER_SKIP_DOWNLOAD=true \
    PUPPETEER_EXECUTABLE_PATH=/usr/bin/google-chrome-stable

RUN apt-get update && apt-get install -y --no-install-recommends \
    ca-certificates \
    fonts-liberation \
    libasound2 \
    libatk-bridge2.0-0 \
    libatk1.0-0 \
    libc6 \
    libcairo2 \
    libcups2 \
    libdbus-1-3 \
    libdrm2 \
    libgbm1 \
    libgtk-3-0 \
    libnspr4 \
    libnss3 \
    libpango-1.0-0 \
    libx11-6 \
    libx11-xcb1 \
    libxcb1 \
    libxcomposite1 \
    libxdamage1 \
    libxext6 \
    libxfixes3 \
    libxrandr2 \
    wget \
  && rm -rf /var/lib/apt/lists/*

# Install a Chrome/Chromium package using your organization's pinned source.
# Verify the resulting path before deploying.

WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .

RUN useradd --create-home --shell /bin/bash appuser \
  && chown -R appuser:appuser /app
USER appuser

CMD ["node", "capture.js"]

The package list varies by browser and distribution. Treat the image as a tested artifact: launch the exact built image, verify the executable path, and keep browser/Puppeteer versions compatible. If you let Puppeteer download its supported browser instead, do not set PUPPETEER_SKIP_DOWNLOAD; confirm the downloaded browser and cache are present in the final runtime image.

4. Set the executable path and verify it

When using a system Chrome or Chromium, configure the path explicitly. An environment variable can keep deployment configuration outside application code; an explicit option is useful when several binaries exist.

const puppeteer = require('puppeteer');

const browser = await puppeteer.launch({
  executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || '/usr/bin/google-chrome-stable',
  headless: true,
  dumpio: true
});
echo "$PUPPETEER_EXECUTABLE_PATH"
"${PUPPETEER_EXECUTABLE_PATH:-/usr/bin/google-chrome-stable}" --version
namei -l "${PUPPETEER_EXECUTABLE_PATH:-/usr/bin/google-chrome-stable}"

If a package manager blocks install scripts, Puppeteer’s postinstall browser download may never run. Confirm the browser exists in the runtime image and that the runtime user can read and execute it. Puppeteer’s documentation also notes that placing the cache under node_modules can mitigate cache lookup problems when postinstall did not run; choose a cache location that is copied into the final image and readable by the runtime user.

5. Keep Chrome’s sandbox enabled when possible

Chrome uses multiple Linux sandbox layers. Prefer a non-root user and configure the container so the sandbox works. The official image example adds SYS_ADMIN at runtime. Check your container runtime, seccomp profile, user namespace settings, and capability policy if Chrome reports No usable sandbox!.

docker run --rm -it --init --cap-add=SYS_ADMIN your-image:tag

Puppeteer documents --no-sandbox as a fallback only when you absolutely trust the content opened in Chrome. It reduces isolation and should be an explicit threat-model decision, not the default Docker fix.

const browser = await puppeteer.launch({
  args: ['--no-sandbox', '--disable-setuid-sandbox']
});

Use this only when your deployment cannot provide a usable sandbox and the pages are trusted. Do not silently add these flags to every environment.

6. Make read-only containers writable where Chrome needs them

Chrome creates profile, configuration, cache, and crash data during startup. A read-only root filesystem can fail before a page opens. Set writable XDG directories and a writable Puppeteer profile, then give ownership to the runtime user.

ENV XDG_CONFIG_HOME=/tmp/.chromium \
    XDG_CACHE_HOME=/tmp/.chromium \
    PUPPETEER_CACHE_DIR=/tmp/.puppeteer-cache

RUN mkdir -p /tmp/.chromium /tmp/.puppeteer-cache /tmp/.puppeteer-profile \
  && chown -R appuser:appuser /tmp/.chromium /tmp/.puppeteer-cache /tmp/.puppeteer-profile
const browser = await puppeteer.launch({
  userDataDir: '/tmp/.puppeteer-profile'
});

In a hardened environment, mount writable temporary storage at these paths and verify free space. A writable directory that is owned by root is still unusable to a non-root browser process.

7. Check base-image compatibility

Approach Dependency effort Browser path Sandbox and upgrade risk Best use
Official Puppeteer image Lowest; browser and expected dependencies supplied. Use image defaults. Follow the documented runtime configuration and image updates. Reproducible baseline and diagnosis.
Custom Debian/Ubuntu Install and maintain libraries and fonts. Often set executablePath or an environment variable. You control package and browser pinning. Teams needing OS/runtime control.
Alpine Highest; compatibility must be assembled. Usually an explicit Chromium path. Chromium/Puppeteer mismatch risk is higher. Small images only after compatibility testing.

Chrome does not support Alpine out of the box. If you choose Alpine, install compatible system dependencies, use the newest Chromium package that matches the Puppeteer-supported browser version, and test the resulting image. Do not copy a Debian Dockerfile unchanged.

8. Use an init process and account for platform runtime behavior

Chrome creates child processes. Running the container with --init, or using an equivalent init process in your orchestrator, helps reap orphaned processes.

docker run --rm --init your-image:tag

On Google Cloud Run, work started only after an HTTP response can be delayed because CPU may be disabled after the response. Launch and finish Puppeteer work before responding, or configure continuous CPU allocation. During local diagnosis, try the documented SYS_ADMIN capability when a non-privileged user fails at sandbox startup.

9. A complete minimal capture program

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({
    executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || undefined,
    headless: true,
    dumpio: true,
    timeout: 30000,
    userDataDir: process.env.PUPPETEER_USER_DATA_DIR || '/tmp/.puppeteer-profile'
  });

  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: '/tmp/shot.png', fullPage: true });
  } finally {
    await browser.close();
  }
})();

Use a per-job userDataDir when jobs run concurrently. Close the browser in a finally block, set navigation timeouts, and remove temporary profiles after successful and failed jobs.

10. Troubleshooting checklist

  1. Capture stderr: enable dumpio: true and preserve the complete error.
  2. Check the final image: the browser and libraries must exist in the runtime stage, not only the builder stage.
  3. Check the path: run the binary with --version and set executablePath if needed.
  4. Check the user: verify the browser is executable and profile/cache directories are writable by that UID.
  5. Check the sandbox: retain it with appropriate runtime capabilities; use no-sandbox only for trusted content when unavoidable.
  6. Check storage: set writable XDG paths and userDataDir in read-only containers.
  7. Check versions: pin and test matching Puppeteer, Chrome/Chromium, and base-image versions.
  8. Check process limits: use an init process and inspect memory, shared memory, and temporary disk limits.
  9. Reproduce interactively: run the exact production image with a shell before changing application code.

11. Performance, reliability, and cost considerations

  • Startup: launching one browser per request is expensive. Reuse a controlled browser process when workload isolation permits, and create/close pages per job.
  • Concurrency: cap parallel pages according to available CPU, memory, and temporary storage. Excessive concurrency produces timeouts and renderer crashes that look like launch failures.
  • Reliability: pin image and browser versions, run a smoke capture in CI, and log Puppeteer, browser, image, and kernel details with failures.
  • Isolation: keep sandboxing enabled for untrusted pages. A no-sandbox deployment trades security for startup compatibility.
  • Cost: container cost comes from CPU, memory, runtime, browser startup, and retries. Avoid retrying deterministic errors such as a missing executable or library; retry transient navigation failures with a limit.

12. Or skip the browser setup

If your goal is a reliable website screenshot rather than maintaining Chrome in a container, ScreenshotNeo provides a GET API at https://api.screenshotneo.com/v1/shot. See the ScreenshotNeo API documentation for options and response details.

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}`);

ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed; response headers identify the page verdict and whether it was billed. Its MCP server lets Claude, Cursor, and other MCP clients use take_screenshot, get_page_info, and capture_pdf. The Free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 shots.

Create a free ScreenshotNeo account to get started.

13. FAQ

Why does Puppeteer work locally but fail in Docker?

Your workstation already has browser libraries, fonts, a writable home directory, and a compatible sandbox. The image must provide each of those explicitly.

Should I always add --no-sandbox?

No. Keep Chrome’s sandbox and configure the container first. Use no-sandbox only when the content is trusted and the runtime cannot provide a usable sandbox.

Can I use a system Chromium with Puppeteer?

Yes. Install it in the runtime image, verify the binary and dependencies, and set executablePath or PUPPETEER_EXECUTABLE_PATH.

Why does a read-only container fail before navigation?

Chrome needs writable profile, cache, configuration, and crash data. Set writable XDG paths and userDataDir, then grant ownership to the runtime user.

Is Alpine supported automatically?

No. Chrome does not support Alpine out of the box. Treat Alpine as a separate, version-matched compatibility track and test the finished image.