Chromium Sandbox and --no-sandbox, Explained

The Chromium sandbox isolates page code from your server. What --no-sandbox turns off, why Docker asks for it, and how to keep the sandbox on instead.

By Simon Chiu · Updated

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:

  1. 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.
  2. 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-sandbox helper.

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-sandbox from 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.

The security best practices guide covers the API side. To capture untrusted URLs without running Chrome yourself, create a free account.

Frequently asked questions

Is it safe to run Chrome with --no-sandbox?
Only when every page you load is trusted, for example your own HTML templates in a throwaway container. For arbitrary URLs from users, the sandbox is one of the main defences between a browser exploit and your server.
Why does Chrome say running as root without --no-sandbox is not supported?
The Linux sandbox is not designed to run as root, so Chrome refuses to start. Run the browser as a normal user in the container instead of adding --no-sandbox.
Does Playwright disable the sandbox?
For Chromium, yes by default: its chromiumSandbox launch option defaults to false, and we saw --no-sandbox on the command line. Pass chromiumSandbox: true to turn it on.

Related in this topic

Try RenderScreenshot free

150 free credits every month, no credit card required.