Run headless and headed browsers inside OpenComputer sandboxes — for scraping, logged-in workflows, and AI-driven automation.
OpenComputer sandboxes are full Linux VMs, which means you can run a real Chromium browser inside them the same way you would on a laptop. This guide walks through the setup that makes it actually work: the right Chromium flavor, the system libraries you need, the OpenComputer-specific networking quirks, and how to persist browser state across runs.The examples use libretto, an AI-friendly CLI + library on top of Playwright. Everything here applies equally to raw Playwright, Puppeteer, browser-use, or Browserbase — libretto is just a convenient default.
GitHub Repository
Runnable reference implementation of everything in this guide.
libretto Docs
CLI + library reference for the browser-automation tool used in the examples.
The target has no API (flight aggregators, airline sites, most SaaS admin UIs).
The target needs a real logged-in session (cookies + localStorage + JS-challenge cookies).
You want AI-driven interaction — describe a task in English, let the agent figure out the clicks.
You need screenshots / visual artefacts for verification.
If the site has a good API, use the API. A browser is slower, heavier, and flakier. But when you need it, OpenComputer gives you real VMs — not containers — so you can run full Chromium with no Docker-flavored limitations.
Step 1: Build a snapshot with Chromium pre-installed
Browser setup is heavy (apt packages + Chromium binary is ~500MB). Bake it into a named snapshot once, launch sandboxes from it in seconds.
build-snapshot.ts
import { Image, Snapshots } from "@opencomputer/sdk/node";// Runtime deps Chromium links against on Ubuntu 22.04. Matches Playwright's// published dependency list.const CHROMIUM_DEPS = [ "libnss3", "libnspr4", "libatk1.0-0", "libatk-bridge2.0-0", "libcups2", "libdrm2", "libxkbcommon0", "libxcomposite1", "libxdamage1", "libxfixes3", "libxrandr2", "libxext6", "libgbm1", "libpango-1.0-0", "libcairo2", "libasound2", "fonts-liberation", // libnss3-tools gives us `certutil` — needed at runtime to trust OC's // egress-proxy CA in Chromium (Chromium ignores SSL_CERT_FILE). "libnss3-tools",];const image = Image.base() .aptInstall(CHROMIUM_DEPS) .workdir("/home/sandbox") .runCommands( "cd /home/sandbox && npm init -y >/dev/null", // Install libretto + AI adapter locally so `npx libretto` resolves without // a cold npm fetch at runtime. "cd /home/sandbox && npm install --no-audit --no-fund libretto @ai-sdk/anthropic", // Playwright downloads its own Chromium build — we want headless-shell // for lightweight scraping, or full chromium for headed/VNC scenarios. "cd /home/sandbox && npx --yes playwright install chromium-headless-shell", );const snapshots = new Snapshots();await snapshots.create({ name: "browser", image });
Don’t apt-install chromium-browser on Ubuntu 22.04. That package is a snap shim that won’t run in a minimal VM. Either install Google Chrome via its apt repo (google-chrome-stable), or rely on Playwright’s bundled Chromium (recommended — it’s purpose-built for automation).
This is the single most common gotcha. A sandbox without a secretStore attached has no outbound egress at all — every HTTPS request gets a 407 from the internal proxy.
OpenComputer routes all outbound traffic through a secrets-injection proxy. The proxy only accepts traffic from sandboxes that have at least one sealed secret registered, because session registration happens as part of sealing secrets.The workaround is to create a SecretStore with a wildcard egress allowlist and at least one (possibly dummy) entry:
import { SecretStore } from "@opencomputer/sdk/node";const stores = await SecretStore.list();let store = stores.find((s) => s.name === "browser-egress");if (!store) { store = await SecretStore.create({ name: "browser-egress", egressAllowlist: ["*"], // wildcard — scope this down for production });}await SecretStore.setSecret(store.id, "PLACEHOLDER", "not-used-just-triggers-session");const sandbox = await Sandbox.create({ snapshot: "browser", secretStore: "browser-egress",});
With the store attached, outbound traffic flows normally and the opensandbox-proxy.crt we trusted earlier lets Chromium validate the MITM-rewritten certs.
With the snapshot and per-boot setup in place, driving the browser is a normal libretto session:
import { execFile } from "node:child_process";import { promisify } from "node:util";const run = promisify(execFile);// Open a page in a named session — libretto persists cookies/localStorage// per session name in .libretto/sessions/<name>/.await run("npx", ["libretto", "open", "https://example.com", "--session", "demo", "--headless"], { cwd: "/home/sandbox",});// AI snapshot — Claude analyzes the page and returns a summary + selectors.const { stdout } = await run("npx", [ "libretto", "snapshot", "--session", "demo", "--objective", "Describe the main content and interactive elements.", "--context", "Freshly loaded page",], { cwd: "/home/sandbox", maxBuffer: 8 * 1024 * 1024 });console.log(stdout);
The snapshot command requires an ANTHROPIC_API_KEY (or an OpenAI / Gemini / Vertex key — configure via .libretto/config.json). It’s the AI-driven feature that makes libretto different from raw Playwright.
Step 4: Run a headed browser (for interactive login)
For workflows where the user needs to log in themselves, you can render the browser to a virtual display and expose it via VNC. This lets you embed the running browser in a web UI.Install the VNC stack at runtime (same no-rebuild pattern as per-boot setup):
The VNC WebSocket is available on port 6080 — get its preview URL with sandbox.getPreviewDomain(6080) and embed <iframe src="https://<id>-p6080.<domain>/vnc.html?autoconnect=true"> in your UI. Users click/type directly in the real browser.
The profile lives on the data disk, which survives sandbox hibernation. For cross-sandbox persistence, snapshot the sandbox after login and launch future sandboxes from that warm snapshot — they’ll boot already logged in.
Memory per VM — default is ~1GB. Each headless Chromium uses ~300-500MB; headed full Chromium uses ~500-800MB. Pass memoryMB: 16384 to Sandbox.create for browser-heavy work.
File descriptors — the default nofile limit of 1024 is too low for Chromium (which opens hundreds of FDs). Launch the server process under sudo bash -c "ulimit -n 65535 && exec ..." to raise it.
Concurrency — with default memory, run 2-3 browsers concurrently per sandbox. With 16GB and proper limits, 5+ works cleanly.
407 from proxy after CONNECT
Your sandbox isn’t registered with the secrets proxy. Attach a secretStore with at least one secret entry.Chromium exits with status 1 on launch
Missing shared library. Check .libretto/sessions/<name>/logs.jsonl for the specific .so missing. Add it to CHROMIUM_DEPS in the snapshot or install at runtime.net::ERR_CERT_AUTHORITY_INVALID
Chromium doesn’t trust OC’s egress-proxy CA. Run the certutil NSS-import step from “Per-boot setup”.ENOTFOUND localhost
The guest’s /etc/hosts is missing the localhost entry. Run the grep … /etc/hosts step from “Per-boot setup”.EAGAIN / uv_thread_create / Killed
Out of memory or leaked browser processes from prior runs. Reap stale Chromium processes between sessions (pkill -9 -f chrome-headless-shell) and reduce MAX_CONCURRENT_BROWSERS.Preview URL shows “Waiting for server”
Your server is bound to 127.0.0.1 instead of 0.0.0.0. OC’s edge forwards from the worker interface, not loopback. Set hostname: "0.0.0.0" when calling your web server’s listen().SSE / streaming responses return 502
OC’s preview-URL edge buffers response bodies — streaming chunks don’t reach the client. Use short-polling (POST /api/job → GET /api/job/:id) instead of SSE or WebSockets for progress updates. WebSockets (for VNC, etc.) do pass through as upgrade connections; buffering only affects regular HTTP responses.