ScreenshotNeo

BlogHow-to

How to Fix No Usable Sandbox in Puppeteer on Docker

Fix Puppeteer’s “No usable sandbox!” error in Docker with the official image, custom-image checks, secure fallbacks, troubleshooting, and runnable code.

By the ScreenshotNeo team30 September 20268 min read

How to Fix No Usable Sandbox in Puppeteer on Docker

“No usable sandbox!” means Chrome started inside your container but could not find a Linux sandbox it can use. It is usually a runtime or container configuration problem, not proof that Puppeteer is missing. The safest fix is to run Chrome with its sandbox enabled, using Puppeteer’s official Docker image or a custom image with the required privileges, user setup, libraries, writable paths, and process handling.

Puppeteer’s documentation warns that “Running without a sandbox is strongly discouraged.” Treat --no-sandbox as an exceptional fallback for absolutely trusted pages, not as the normal Docker solution.

Quick diagnosis

  1. Try the official Puppeteer image with --cap-add=SYS_ADMIN and Docker’s init process.
  2. If you build your own image, run Chrome as a non-root user and verify the host allows the sandbox mechanism.
  3. Check missing shared libraries, writable profile and cache directories, PID 1 handling, and host AppArmor policy.
  4. Only use --no-sandbox when the content is completely trusted and you accept the loss of Chrome’s isolation.

What the error actually means

Chrome uses multiple sandbox layers to isolate web content from the operating system. During startup it tests whether the execution environment can provide those layers. If user namespaces, setuid sandbox support, required capabilities, or related host policy are unavailable, Chrome exits before Puppeteer can create a browser session and reports No usable sandbox!.

Chrome’s sandbox depends on the container runtime, user identity, and host policy.
Chrome’s sandbox depends on the container runtime, user identity, and host policy.

The same message can appear for different underlying causes:

Cause Typical symptom What to check
Sandbox capability unavailable Chrome exits immediately with the exact error Docker capabilities, root/non-root setup, host security policy
Missing libraries Loader errors or a browser that never starts ldd chrome | grep not and current distro dependencies
Read-only filesystem Crashpad, profile, cache, or configuration errors Writable XDG_* paths and userDataDir
Process cleanup failure Orphaned Chrome processes or intermittent jobs Docker --init or an init entrypoint
AppArmor or host policy Works elsewhere, fails on a specific Ubuntu host Active profile and user-namespace restrictions

Preferred fix: Puppeteer’s official Docker image

Puppeteer publishes an image containing Chrome for Testing, the required dependencies, and a matching pre-installed Puppeteer version. It is designed to run Chrome with its sandbox enabled. The documented invocation includes --cap-add=SYS_ADMIN and --init (official Docker guide).

docker run -i --init --cap-add=SYS_ADMIN --rm ghcr.io/puppeteer/puppeteer:latest node -e "$(cat path/to/script.js)"

Replace latest with a version tag that matches your Puppeteer dependency when reproducibility matters. A mutable tag can change the browser build and operating-system packages underneath your deployment.

Minimal script

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({
    headless: true
  });
  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();
})();

Save it as script.js and run it through the container command. Keep the browser and Puppeteer versions aligned; the official image is useful because it supplies both together.

Building a custom image safely

A custom Debian, Ubuntu, or CentOS image can work, but you must reproduce the parts the official image handles for you. Use this sequence.

1. Verify sandbox prerequisites

First confirm that the Docker runtime and host security policy allow Chrome’s sandbox mechanism. The official image documents SYS_ADMIN for its configuration. Add only the capability your deployment requires and review its security impact with your platform team. A container boundary does not automatically replace Chrome’s own sandbox.

2. Run as a non-privileged user

Root execution often causes Chrome to reject its normal sandbox path. Create a dedicated user, make browser directories owned by that user, and launch Puppeteer without root. Puppeteer’s troubleshooting example uses a user named pptruser specifically so it does not need --no-sandbox.

FROM node:22-bookworm

# Install Chrome and its current dependencies using your distro's supported method.
# Keep this list aligned with the Chrome build you install.
RUN useradd --create-home --shell /bin/bash pptruser
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN chown -R pptruser:pptruser /app
USER pptruser
ENV XDG_CONFIG_HOME=/tmp/xdg-config \
    XDG_CACHE_HOME=/tmp/xdg-cache
CMD ["node", "script.js"]

Adapt package installation and paths to your base image. Do not copy an old dependency list blindly: Puppeteer notes that required packages vary and lists can become outdated.

3. Check shared libraries

If Chrome is present but cannot load, inspect the binary inside the image:

ldd /path/to/chrome | grep not

Every unresolved library must be installed from the current dependency guidance for your distribution and Chrome build. A missing library can be mistaken for a sandbox failure because both happen during startup.

4. Provide writable browser paths

Read-only containers still need writable locations for the browser profile, configuration, cache, and crash data. Point configuration and cache variables at writable directories and set Puppeteer’s userDataDir explicitly:

const browser = await puppeteer.launch({
  headless: true,
  userDataDir: '/tmp/puppeteer-profile',
  args: ['--disable-dev-shm-usage']
});

Create and permission the directory before launch, or mount a writable volume owned by the Chrome user. An error such as chrome_crashpad_handler: --database is required can indicate an unwritable profile or crash directory rather than a sandbox defect.

5. Handle shared memory deliberately

Chrome uses shared memory for renderer processes. Docker’s default /dev/shm can be too small for pages with large images or many frames. Prefer a larger shared-memory mount:

docker run --shm-size=1g ...

If your platform cannot provide that, --disable-dev-shm-usage makes Chrome use a temporary directory instead. It can reduce shared-memory pressure but may be slower under heavy concurrency.

6. Reap child processes

Use Docker’s --init flag or an init entrypoint. Puppeteer starts Chrome and renderer children; an init process forwards signals and reaps exited children. Without it, long-running workers can accumulate zombies and become unreliable.

7. Investigate AppArmor on affected hosts

Puppeteer documents Ubuntu 23.10 and later as a case where an AppArmor profile for Chrome Stable can prevent Puppeteer-downloaded Chrome for Testing from using user namespaces. If the same image works on another host, inspect the active AppArmor profile and follow Chromium’s policy guidance before changing it. Do not assume every Ubuntu host has this cause.

Complete Docker Compose example

services:
  capture:
    image: ghcr.io/puppeteer/puppeteer:25.12.0
    init: true
    cap_add:
      - SYS_ADMIN
    working_dir: /app
    volumes:
      - ./:/app
      - chrome-cache:/tmp/xdg-cache
    environment:
      XDG_CONFIG_HOME: /tmp/xdg-config
      XDG_CACHE_HOME: /tmp/xdg-cache
    command: ["node", "script.js"]
volumes:
  chrome-cache:

Pin the image tag to the Puppeteer version used by your project. Test the exact image, host kernel, Docker runtime, and deployment policy together.

When (and how) to use --no-sandbox

Puppeteer shows this option only with an explicit condition: use it if the content opened in Chrome is absolutely trusted. It removes Chrome’s sandbox protection. Do not use it for arbitrary URLs, user-submitted HTML, advertisements, or pages that can navigate to attacker-controlled content.

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

If you temporarily use it to diagnose a deployment, log that the sandbox is disabled, isolate the worker, restrict outbound access, and keep investigating the proper sandbox configuration. Containerization alone does not make this equivalent to sandboxed Chrome.

Or skip the browser setup

If your goal is a dependable screenshot rather than operating Chrome yourself, ScreenshotNeo provides a website screenshot API and MCP server. One GET request returns PNG, JPEG, WebP, or PDF. Cookie and consent banners are accepted and removed before capture, along with 60+ known consent platforms, newsletter popups, and chat widgets. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers identify the page verdict and billing status.

See the ScreenshotNeo API docs for all options, including full-page and element capture, device presets, retina scale, waits, custom CSS and JavaScript, request blocking, headers and cookies, geolocation, PDF settings, caching, signed links, async jobs, bulk capture, and usage reporting.

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

An MCP server lets Claude, Cursor, and other MCP clients call take_screenshot, get_page_info, and capture_pdf. The Free plan includes 1,000 screenshots each month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.

Troubleshooting checklist

“No usable sandbox!” immediately after launch

Confirm you are using the documented image invocation, including --cap-add=SYS_ADMIN and --init. If using a custom image, verify non-root execution and host policy. Do not jump directly to --no-sandbox.

A hosted capture service can clean consent overlays before returning the image.
A hosted capture service can clean consent overlays before returning the image.

“Running as root without –no-sandbox is not supported”

Create a dedicated user, chown the application and browser directories, and launch as that user. Root plus --no-sandbox removes the protection you are trying to preserve.

ldd reports “not found”

Install the missing shared libraries for the exact Chrome build and distribution. Re-run ldd inside the final image, not on the host.

Crashpad database or profile errors

Set XDG_CONFIG_HOME, XDG_CACHE_HOME, and userDataDir to writable paths. Check volume ownership and filesystem mount flags.

Works locally, fails in CI

Compare kernel, Docker runtime, capabilities, AppArmor or SELinux policy, image tag, and user identity. CI often adds a read-only filesystem or strips capabilities.

Chrome processes remain after jobs finish

Run with --init, close every browser in a finally block, and avoid sharing one temporary profile across concurrent jobs.

let browser;
try {
  browser = await puppeteer.launch();
  // work
} finally {
  if (browser) await browser.close();
}

Performance, reliability, and cost notes

  • Startup: Reuse a browser process for batches, but create isolated pages and profiles where cookies or permissions must not leak.
  • Concurrency: More pages consume CPU, memory, file descriptors, and shared memory. Set a queue limit instead of launching unlimited browsers.
  • Reproducibility: Pin the Puppeteer image and npm version. Record the Chrome build when diagnosing regressions.
  • Filesystem: Temporary profiles prevent cross-job state but require cleanup. Writable volumes improve stability for caches.
  • Security: Keep the sandbox enabled for untrusted pages. Restrict network access and permissions around any exceptional unsandboxed worker.
  • Hosted alternative: ScreenshotNeo bills only clean shots; failed loads, bot checks, blank pages, timeouts, and cache hits are not billed. Its response headers expose the verdict and billing result, which simplifies retry and cost accounting.

FAQ

Is this a Puppeteer installation problem?

Usually no. The error is emitted by Chrome when its execution environment lacks a usable sandbox. Missing libraries and filesystem permissions can produce nearby startup failures and should be checked separately.

Does Docker automatically sandbox Chrome?

No. Docker isolation and Chrome’s sandbox are separate layers. Configure the runtime so Chrome can use its sandbox.

Should I add --no-sandbox in every container?

No. Puppeteer strongly discourages it. Use it only for absolutely trusted content when you cannot provide the sandbox prerequisites.

Why does the official image need SYS_ADMIN?

The Puppeteer Docker guide documents that capability for the image’s sandbox configuration. Use the exact guidance for the image version and deployment policy you run.

Can a read-only root filesystem work?

Yes, if Chrome’s profile, cache, configuration, and crash paths are mounted or redirected to writable locations.

When should I use ScreenshotNeo?

Use it when you need screenshots or PDFs without maintaining Chrome, Docker capabilities, browser dependencies, and sandbox policy. It also helps when AI agents need an MCP-based capture tool.