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.

“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
- Try the official Puppeteer image with
--cap-add=SYS_ADMINand Docker’s init process. - If you build your own image, run Chrome as a non-root user and verify the host allows the sandbox mechanism.
- Check missing shared libraries, writable profile and cache directories, PID 1 handling, and host AppArmor policy.
- Only use
--no-sandboxwhen 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!.

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.

“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.


