How to run headless Chrome in a sandbox without Puppeteer
Download chrome-headless-shell with npx @puppeteer/browsers, then call it with --print-to-pdf, --screenshot or --dump-dom and read the file.
On Runtime, a PDF or a screenshot of any page is one command in a throwaway microVM. No automation library, no browser profile to clean up: Chrome's own command line writes the file, and your code copies it out. Every sandbox is a Firecracker microVM with its own kernel, so the page you render cannot reach your machine, and it bills the CPU Chrome uses, about $0.08 an hour for 2 vCPU and 4 GiB at full load and $0.03125 while idle (pricing).
Which headless Chrome
Google split headless Chrome in two. On its Chrome for Developers pages, read 25 September 2026:
| Binary | What it is | Get it |
|---|---|---|
chrome --headless |
"New" headless mode since Chrome 112: the real browser without a window | npx @puppeteer/browsers install chrome@stable |
chrome-headless-shell |
The old headless mode, a lighter wrapper that needs no X11, Wayland or D-Bus; only a separate binary from Chrome 132.0.6793.0 onwards | npx @puppeteer/browsers install chrome-headless-shell@stable |
For printing and screenshots the shell is enough. For behaviour that must
match a person's browser, use chrome --headless.
Print a page to PDF
TypeScriptimport { writeFile } from "node:fs/promises";import { Sandbox } from "withruntime";await using sbx = await Sandbox.create({ diskMiB: 8192, timeoutSeconds: 900 });const slow = { check: true, timeoutMs: 600_000 } as const;await sbx.exec( "sudo apt-get update -qq && sudo apt-get install -y -qq fonts-liberation libasound2t64 " + "libatk-bridge2.0-0t64 libcups2t64 libgbm1 libgtk-3-0t64 libnss3 libxss1", slow,);const install = await sbx.exec( "npx --yes @puppeteer/browsers install chrome-headless-shell@stable " + "--path /workspace/browsers --format '{{path}}'", slow,);const bin = install.stdout.trim(); // the executable's absolute pathawait sbx.exec( [bin, "--print-to-pdf=/workspace/page.pdf", "--no-pdf-header-footer", "https://example.com"], { check: true, timeoutMs: 120_000 },);await writeFile("page.pdf", await sbx.files.read("/workspace/page.pdf"));Pythonfrom withruntime import SandboxLIBS = ("fonts-liberation libasound2t64 libatk-bridge2.0-0t64 libcups2t64 " "libgbm1 libgtk-3-0t64 libnss3 libxss1")with Sandbox.create(disk_mib=8192, timeout_seconds=900) as sbx: sbx.exec(f"sudo apt-get update -qq && sudo apt-get install -y -qq {LIBS}", check=True, timeout_ms=600_000) shell = sbx.exec("npx --yes @puppeteer/browsers install chrome-headless-shell@stable " "--path /workspace/browsers --format '{{path}}'", check=True, timeout_ms=600_000).stdout.strip() sbx.exec([shell, "--print-to-pdf=/workspace/page.pdf", "--no-pdf-header-footer", "https://example.com"], check=True, timeout_ms=120_000) sbx.files.download("/workspace/page.pdf", "page.pdf")--format '{{path}}' makes the installer print only the executable's path.
The library names are Ubuntu 24.04's, with the t64 suffix the release added,
checked on packages.ubuntu.com on 25 September 2026. The apt step and the
download take longer than a command's 60-second default, hence timeoutMs.
The flags that do the work
From Chrome's headless command-line reference, read 25 September 2026:
| Flag | What it writes |
|---|---|
--dump-dom |
The page's DOM after scripts ran, to stdout |
--print-to-pdf |
A PDF of the page; --no-pdf-header-footer drops the date and URL line |
--screenshot |
A PNG; set its size with --window-size=1280,800 |
--timeout=5000 |
Stops waiting for the page after that many milliseconds |
--virtual-time-budget=N |
Runs the page's timers forward N ms of virtual time before capturing |
--dump-dom is the quickest way to read what a JavaScript-built page renders:
the result is in run.stdout, up to 64 KiB, or redirect it to a file and read
it with files.readText for larger pages.
TypeScriptimport { Sandbox } from "withruntime";await using sbx = await Sandbox.create({ image: "chrome-shell" });const dom = await sbx.exec( "chrome-headless-shell --dump-dom --virtual-time-budget=5000 https://example.com > dom.html", { check: true, timeoutMs: 120_000 },);const html = await sbx.files.readText("/workspace/dom.html");console.log(dom.exitCode, html.length);Start every sandbox with the shell installed
Build it into a custom image and link the binary onto PATH,
which is what the sample above assumes:
TypeScriptimport { Runtime } from "withruntime";const runtime = new Runtime();await runtime.images.build({ name: "chrome-shell", recipe: { apt: [ "fonts-liberation", "libasound2t64", "libatk-bridge2.0-0t64", "libcups2t64", "libgbm1", "libgtk-3-0t64", "libnss3", "libxss1", ], commands: [ 'ln -s "$(npx --yes @puppeteer/browsers install chrome-headless-shell@stable ' + "--path /opt/browsers --format '{{path}}')\" /usr/local/bin/chrome-headless-shell", ], },});Pythonfrom withruntime import Runtimeruntime = Runtime()runtime.images.build(name="chrome-shell", recipe={ "apt": ["fonts-liberation", "libasound2t64", "libatk-bridge2.0-0t64", "libcups2t64", "libgbm1", "libgtk-3-0t64", "libnss3", "libxss1"], "commands": [ "ln -s \"$(npx --yes @puppeteer/browsers install chrome-headless-shell@stable " "--path /opt/browsers --format '{{path}}')\" /usr/local/bin/chrome-headless-shell", ],})A build is free and uses no trial hours. A stored image is charged on its size; the free trial keeps three free.
Debug the page from your own DevTools
--remote-debugging-port opens the DevTools Protocol inside the sandbox.
Since Chrome 136 it must come with --user-data-dir, Google's blog says. Start
the browser with spawn so it keeps running, then forward the port:
TypeScriptimport { Sandbox } from "withruntime";await using sbx = await Sandbox.create({ image: "chrome-shell", timeoutSeconds: 1800 });await sbx.spawn( "chrome-headless-shell --remote-debugging-port=9222 --user-data-dir=/workspace/profile about:blank",);const devtools = await sbx.forwardPort(9222, { localPort: 9222 });const version = await fetch("http://127.0.0.1:9222/json/version").then((r) => r.json());console.log(version); // webSocketDebuggerUrl: hand it to any CDP clientawait devtools.close();The protocol's /json/version endpoint returns the browser's
webSocketDebuggerUrl, which Puppeteer's connect, Playwright's
connectOverCDP and other CDP clients accept. The forward goes through
Runtime's API with your key, and nothing on the internet can reach port 9222
(forward ports).
Limits
- Output size. A command result keeps 64 KiB of stdout; write large DOM dumps to a file.
- Disk. Give the sandbox
diskMiB: 8192. The default 4 GiB disk had about 2.5 GiB free on 24 September 2026, and the trial allows up to 10 GiB. - Ports. Trial sandboxes open pages on ports 80 and 443; paid sandboxes reach any port.
For a library on top of the same browser, see Puppeteer or Playwright. For an agent that looks at the screen and clicks, see a browser automation agent.
Sources
- Chrome for Developers, headless mode, https://developer.chrome.com/docs/chromium/headless, read 25 September 2026
- Chrome for Developers, headless command line, https://developer.chrome.com/docs/automation-and-testing/headless-cli, read 25 September 2026
- Chrome headless shell announcement, https://developer.chrome.com/blog/chrome-headless-shell, read 25 September 2026
- Chrome 136 remote debugging change, https://developer.chrome.com/blog/remote-debugging-port, read 25 September 2026
- @puppeteer/browsers, https://pptr.dev/browsers-api, read 25 September 2026
- Chrome DevTools Protocol, https://chromedevtools.github.io/devtools-protocol/, read 25 September 2026
Facts on this page were checked on 25 September 2026.