Run Browser Use or Playwright With Real Chrome in a Cloud VM
Updated October 27, 2026 .md
Short answer: Every Sandboxer VM has Google Chrome installed. Install Playwright or Browser Use with uv, point it at that Chrome, and run headless right away. To watch, open the desktop with sbx desktop, then launch Chrome headed on display :1, and it appears in the live stream in your browser.
At a glance#
What we found on a default sandbox (4 vCPU, 8 GB, $0.036/hour) |
|
|---|---|
| Chrome | /usr/bin/google-chrome, Google Chrome 155.0.8059.39, already installed |
| Runtimes | Python 3.12.3, Node 22, uv and npm already installed |
| Browser download | Not needed: Playwright's channel="chrome" uses the installed Chrome |
| Headless | Works as soon as the sandbox is ready |
| Headed (to watch) | Needs the desktop started once with sbx desktop; it runs on display :1 |
| Watching | sbx desktop returns a noVNC URL with a token that works for 10 minutes |
| Stop and resume | The browser, its tabs and cookies kept running with the same process IDs (memory restores only on a machine with the same CPU type; see step 5) |
1. Check Chrome is there#
id=$(sbx new --json | jq -r 'select(.event == "ready") | .id')
sbx exec $id "which google-chrome; google-chrome --version"/usr/bin/google-chrome
Google Chrome 155.0.8059.392. Playwright, headless#
Install Playwright into a virtual environment. You don't need playwright install, because channel="chrome" tells Playwright to use the Chrome that's already on the machine (Playwright docs on browser channels).
# shot.py
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(channel="chrome", headless=True)
page = browser.new_page()
page.goto("https://example.com")
print(browser.version, "|", page.title())
page.screenshot(path="example.png")
browser.close()sbx scp shot.py $id:/home/user/shot.py
sbx exec $id "uv venv -q bu && . bu/bin/activate && uv pip install -q playwright"
sbx exec $id ". bu/bin/activate && python shot.py && file example.png"155.0.8059.39 | Example Domain
example.png: PNG image data, 1280 x 720, 8-bit/color RGB, non-interlacedThe whole run took 1.6 seconds (single run, October 27, 2026). Copy the screenshot back with sbx scp $id:/home/user/example.png ..
The same works from Node. npm i playwright installs the library only, and channel: "chrome" launches the installed Chrome:
// shot.mjs
import { chromium } from "playwright";
const browser = await chromium.launch({ channel: "chrome", headless: !process.env.HEADED });
const page = await browser.newPage();
await page.goto("https://example.com");
console.log(browser.version(), "|", await page.title());
await page.screenshot({ path: "example.png" });
await browser.close();155.0.8059.39 | Example Domain3. Watch it: start the desktop, then run headed#
The desktop (XFCE, streamed over noVNC) starts the first time you open it. Before that there is no X server, and a headed launch fails. This is what we got when we tried it first:
$ sbx exec $id "cd pw && HEADED=1 DISPLAY=:1 node shot.mjs"
browserType.launch: Target page, context or browser has been closed
…ERROR:ui/ozone/platform/x11/ozone_platform_x11.cc:257] Missing X server or $DISPLAYSo open the desktop first. sbx desktop $id opens it in your browser; --no-open only prints the URL. With --json you see the whole response:
sbx desktop $id --no-open --json{"ok":true,"type":"desktop.url","desktopUrl":"https://ocean-curious-panda-6080.box.sandboxer.com/vnc.html?_token=…&autoconnect=true&path=websockify&reconnect=true&resize=remote", …,"message":"WebRTC streaming is not available yet; streaming over VNC.","mode":"vnc", …}The URL carries a secret token: anyone who has it can see and control the screen. It works for ten minutes, and a browser that opens it in that time stays signed in for 8 hours. Without the token the same page returns 401.
Now the screen exists. On the sandboxes we tested it was display :1; check with pgrep -a Xvfb if you script this:
sbx exec $id "pgrep -a Xvfb"760 /usr/bin/Xvfb :1 -screen 0 1920x1080x24 -noreset -nolisten tcp +extension RANDRRun the script headed on that display, in the background so it keeps going after sbx exec returns:
# keep.py: a headed browser with a profile on the disk
import time
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
ctx = p.chromium.launch_persistent_context(
"/data/chrome-agent", channel="chrome", headless=False, no_viewport=True)
page = ctx.pages[0]
page.goto("https://httpbin.org/cookies/set?session=signed-in")
print("cookies:", [c["name"] + "=" + c["value"] for c in ctx.cookies()], flush=True)
while True: # keep the browser open; your agent works here
print(time.strftime("%H:%M:%S"), page.url, flush=True)
time.sleep(5)sbx exec $id "sudo mkdir -p /data && sudo chown user /data"
sbx scp keep.py $id:/home/user/keep.py
sbx exec $id --detach ". bu/bin/activate && DISPLAY=:1 python keep.py > keep.log 2>&1"
sbx exec $id "head -2 keep.log"cookies: ['session=signed-in']
02:24:39 https://httpbin.org/cookiesThe Chrome window is drawn on display :1, the screen the desktop stream shows; a screen grab of :1 a few seconds later showed it open on the httpbin page. To keep a record instead of watching live, grab the screen with ffmpeg -f x11grab -i :1 (see Desktop); use :1, the display you found above.
sbx browser is a different view. It streams a separate Chrome with its own profile (--profile /data/chrome-work), with tabs and address bar only. In our test it ran on its own screen (display :2, at the -6081 address), not the one your script draws on (:1, at -6080). Use sbx desktop to watch a Playwright or Browser Use run, and sbx browser when a person needs a browser to sign in or browse.
4. Browser Use, without an LLM#
Browser Use is a Python library that gives a language model control of a browser. Its agent needs a model key, which we didn't use here, so we ran only its browser part: start a session on the installed Chrome, load a page, and read the page state an agent would be given.
# bu_session.py
import asyncio, base64
from browser_use import BrowserSession
async def main():
session = BrowserSession(
executable_path="/usr/bin/google-chrome", # the installed Chrome
headless=False, # draw it on the desktop
user_data_dir="/data/chrome-agent", # profile kept on the disk
keep_alive=True,
)
await session.start()
await session.navigate_to("https://news.ycombinator.com")
state = await session.get_browser_state_summary(include_screenshot=True)
print("url:", state.url)
print("title:", state.title)
print("clickable elements:", len(state.dom_state.selector_map))
print(state.dom_state.llm_representation()[:400])
with open("bu.png", "wb") as f:
f.write(base64.b64decode(state.screenshot))
await session.kill()
asyncio.run(main())sbx exec $id ". bu/bin/activate && uv pip install -q browser-use" # 0.13.14 in our run
sbx scp bu_session.py $id:/home/user/bu_session.py
sbx exec $id ". bu/bin/activate && DISPLAY=:1 ANONYMIZED_TELEMETRY=false python bu_session.py"INFO [utils] … Downloading Force Background Tab extension...
…
url: https://news.ycombinator.com/
title: news.ycombinator.com
clickable elements: 557
[32]<a image_src=y18.svg />
[37]<a />
Hacker News
[39]<a />
new
…The numbered list is the page as Browser Use describes it to the model. On first start the library downloads a few Chrome extensions, which is the log line at the top; ANONYMIZED_TELEMETRY=false turns off its telemetry. To run a real agent, pass this session to it with your model: Agent(task=..., llm=..., browser_session=session). The run shows up in the desktop stream the same way as the Playwright window.
5. Stop it and come back to the same browser#
Stopping a sandbox saves its memory. We stopped the sandbox above while keep.py was running, waited about 25 seconds, and resumed it:
sbx stop $id --wait # returns once the sandbox is archived
sbx resume $id
sbx exec $id "tail -4 keep.log; pgrep -f 'python keep.py' | head -1; pgrep -o -f 'chrome.*chrome-agent'"02:24:54 https://httpbin.org/cookies
02:25:21 https://httpbin.org/cookies
02:25:27 https://httpbin.org/cookies
02:25:32 https://httpbin.org/cookies
2497
2514The script and Chrome kept their process IDs (2497 and 2514, the same as before the stop), the log picks up after the gap, and a fresh sbx desktop URL loaded again. A screen grab of :1 showed the same window with the session=signed-in cookie on the page. Memory is restored only on a machine with the same CPU type. If none is free within 2 minutes, the resume boots from disk: the profile on /data is kept, but Chrome, the script, open tabs and session-only cookies are gone, so start them again (see snapshots). A stopped sandbox costs nothing for compute. The profile directory /data/chrome-agent is on the disk, so a named snapshot or a fork keeps it. Open tabs and session-only cookies live in memory, so only a stop and resume keeps those.
FAQ#
Do I need to run playwright install in the sandbox?#
No. Launch with channel="chrome" (Python) or channel: "chrome" (Node) and Playwright uses the Google Chrome that's already installed. We never ran playwright install in these tests.
Why does my headed browser fail with "Missing X server or $DISPLAY"?#
The desktop hasn't been started yet, or DISPLAY points at the wrong screen. Run sbx desktop <id> once, then use DISPLAY=:1 (confirm with pgrep -a Xvfb). Headless runs don't need a display.
Can someone else watch the run?#
Yes, if you give them a desktop URL from sbx desktop. Anyone with the URL can also control the screen, so treat it like a password. For an embedded view in your own product, put the URL in an iframe on a page that requires your own sign-in.
Can I run a full Browser Use agent here?#
Yes, with your own model key in the sandbox. We tested the browser session without a model, as shown above; the agent itself calls your LLM provider and is billed by them.
Related#
More guides
For running AI-written code we rank Sandboxer, Freestyle and E2B first: each gives code its own VM with an API or SDK and can pause with memory. Pick Daytona for Windows or GPUs, Modal or Northflank for GPUs at scale, E2B or Northflank to run in your own cloud, and Vercel Sandbox if your app lives on Vercel.
For coding agents we rank Sandboxer first: a full Ubuntu VM with Claude Code and Codex preinstalled, Docker, and stop-and-resume that keeps the agent's session in memory. Freestyle and Amp Orbs also keep memory across pauses. Pick E2B for prebuilt agent templates on open source, Docker Sandboxes for free local runs, and Codespaces if you live in GitHub.
The best Daytona alternatives depend on why you're switching. For a full Linux microVM on every sandbox with memory-saving stop and Docker, we rank Sandboxer first. E2B suits open source and self-hosting, Modal massive short-job fleets, Northflank bring-your-own-cloud with GPUs, and Vercel Sandbox apps on Vercel.
If you're leaving E2B for longer sessions or a fuller machine, we rank Sandboxer first, then Freestyle: both give each sandbox a full Linux VM that keeps memory across pauses, with Docker and no short session cap. Pick Daytona for Windows or GPUs, Northflank for bring-your-own-cloud, and stay on E2B if you need open source.