# 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](/docs/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 ```ts check import { 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 path await 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")); ``` ```python check from withruntime import Sandbox LIBS = ("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. ```ts check import { 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](/docs/images) and link the binary onto `PATH`, which is what the sample above assumes: ```ts check import { 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", ], }, }); ``` ```python check from withruntime import Runtime runtime = 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: ```ts check import { 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 client await 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](/docs/editors#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](/integrations/puppeteer) or [Playwright](/integrations/playwright). For an agent that looks at the screen and clicks, see [a browser automation agent](/use-cases/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.