The Chromium sandbox is the set of operating-system restrictions that Chrome puts on its renderer processes, so that code from a web page, even code that exploits a browser bug, cannot read files, start programs or reach the rest of the machine.
The --no-sandbox flag turns those restrictions off. It appears in a lot of Docker and CI examples, usually without a word about what it costs. This entry is part of the screenshot glossary.
What it means in plain terms
Chrome splits itself into processes. The browser process has normal rights: it reads the profile, writes downloads and talks to the network. Each renderer process parses HTML, runs JavaScript and paints pixels for a site. Renderers handle untrusted input from the web, so Chrome locks them down. If an attacker finds a bug in the JavaScript engine, they control only a renderer that cannot do much on its own.
For a screenshot service this matters more than for a desktop user. You load pages you did not write, often URLs chosen by your own users, on a server that holds API keys and database credentials.
How it works
flowchart LR subgraph Host["Your server or container"] B["Browser process<br/>(normal rights)"] subgraph SB["Sandbox: namespaces + seccomp-bpf on Linux"] R1["Renderer: site A"] R2["Renderer: site B"] G["GPU / utility processes"] end end B <-->|IPC only| R1 B <-->|IPC only| R2 B <-->|IPC only| G
On Linux, Chrome builds the sandbox from two layers:
- Namespaces. Each renderer gets its own view of the file system, process list and network, usually through unprivileged user namespaces. If those are not available, Chrome can use a small setuid helper binary (
chrome-sandbox) instead. - seccomp-bpf. A filter blocks the system calls a renderer never needs.
macOS and Windows use their own OS mechanisms for the same goal.
--no-sandbox removes these layers for every child process. The page still renders the same, which is why the flag is so tempting: nothing looks wrong.
Why Docker asks for --no-sandbox
Two errors push people to the flag:
- "Running as root without --no-sandbox is not supported." The container runs as root, and Chrome refuses. The fix is a non-root user.
- "No usable sandbox!" The kernel or its policy blocks unprivileged user namespaces. Recent Ubuntu releases restrict them with AppArmor, and Docker's default seccomp profile can block them too. The fix is to allow user namespaces for the Chrome binary, or to install the setuid
chrome-sandboxhelper.
A minimal non-root setup for a Node image:
FROM node:22-bookworm-slim # Keep the browser download inside /app so the non-root user can read it. ENV PUPPETEER_CACHE_DIR=/app/.cache/puppeteer WORKDIR /app COPY package*.json ./ # --install-deps adds Chrome's system libraries (needs root, so do it before USER). RUN npm ci && npx puppeteer browsers install chrome --install-deps COPY . . RUN chown -R node:node /app # Run as the non-root user that the node image already has. USER node CMD ["node", "capture.mjs"]
If the container still prints "No usable sandbox!", the host blocks user namespaces. Fix that on the host (or with a seccomp profile that allows them) before you reach for the flag.
The setting in Puppeteer, Playwright and RenderScreenshot
The two libraries have opposite defaults. We printed the command line each one used:
| Library | Default | Command line we saw |
|---|---|---|
| Puppeteer 24.43.1 | Sandbox on | no --no-sandbox |
| Playwright 1.64.1 | Sandbox off (chromiumSandbox: false) |
--headless --no-sandbox |
Playwright with chromiumSandbox: true |
Sandbox on | --headless |
Puppeteer: add the flag only if you understand the trade, never as a default:
import puppeteer from 'puppeteer'; const trusted = process.env.ONLY_TRUSTED_HTML === '1'; const browser = await puppeteer.launch({ // Keep the sandbox for arbitrary URLs. Drop it only for your own templates. args: trusted ? ['--no-sandbox'] : [], }); try { const page = await browser.newPage(); await page.goto('https://example.com'); await page.screenshot({ path: 'sandboxed.png' }); } finally { await browser.close(); }
Playwright: turn the sandbox on explicitly, and check it:
import { chromium } from 'playwright'; const server = await chromium.launchServer({ chromiumSandbox: true }); try { const flags = server.process().spawnargs.filter((f) => f.includes('sandbox')); console.log(flags.length === 0 ? 'sandbox on' : flags); } finally { await server.close(); }
That printed sandbox on. Without chromiumSandbox: true the same script prints [ '--no-sandbox' ].
RenderScreenshot runs the browser in Cloudflare's managed browser rendering service, outside your infrastructure, and gives each request a fresh browser session. There is no sandbox flag to set, and a page that attacks the browser does not land on your server:
curl -G https://api.renderscreenshot.com/v1/screenshot \ -H "Authorization: Bearer $RS_API_KEY" \ --data-urlencode "url=https://example.com" \ --output example.png
Tested on 2026-10-15 on macOS 27 (arm64) with Node 22.18.0, [email protected] (Chrome 148.0.7778.97) and [email protected] (chrome-headless-shell 156.0.8078.4). The Dockerfile and the curl request were not run for this page.
Common mistakes
- Copying
--no-sandboxfrom a tutorial. Ask first: does the container run as root? Is user namespace support blocked? Fix the cause. - Not knowing your library's default. In Playwright, Chromium runs without the sandbox unless you set
chromiumSandbox: true. - Thinking the sandbox is the only defence. It does not stop a page from making requests to your internal network. Block private address ranges too; see SSRF.
- Sharing one long-lived browser across users. Even with a sandbox, use a fresh browser context per job so cookies and storage do not leak between captures.
Related terms
The security best practices guide covers the API side. To capture untrusted URLs without running Chrome yourself, create a free account.