# Run Browser Use or Playwright With Real Chrome in a Cloud VM

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

Source: https://sandboxer.com/guides/run-browser-use-or-playwright-with-real-chrome. Updated 2026-10-27.

**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

```bash
id=$(sbx new --json | jq -r 'select(.event == "ready") | .id')
sbx exec $id "which google-chrome; google-chrome --version"
```

```text
/usr/bin/google-chrome
Google Chrome 155.0.8059.39
```

## 2. 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](https://playwright.dev/python/docs/browsers#google-chrome--microsoft-edge)).

```python
# 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()
```

```bash
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"
```

```text
155.0.8059.39 | Example Domain
example.png: PNG image data, 1280 x 720, 8-bit/color RGB, non-interlaced
```

The 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:

```javascript
// 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();
```

```text
155.0.8059.39 | Example Domain
```

## 3. 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:

```text
$ 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 $DISPLAY
```

So 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:

```bash
sbx desktop $id --no-open --json
```

```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:

```bash
sbx exec $id "pgrep -a Xvfb"
```

```text
760 /usr/bin/Xvfb :1 -screen 0 1920x1080x24 -noreset -nolisten tcp +extension RANDR
```

Run the script headed on that display, in the background so it keeps going after `sbx exec` returns:

```python
# 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)
```

```bash
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"
```

```text
cookies: ['session=signed-in']
02:24:39 https://httpbin.org/cookies
```

The 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](/docs/desktop#record-the-screen)); 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](https://docs.browser-use.com) 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.

```python
# 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())
```

```bash
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"
```

```text
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:

```bash
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'"
```

```text
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
2514
```

The 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](/docs/snapshots#resume)). A stopped sandbox costs nothing for compute. The profile directory `/data/chrome-agent` is on the disk, so a [named snapshot](/docs/snapshots) 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

- [Desktop](/docs/desktop)
- [Pause and resume a sandbox with its memory](/guides/pause-and-resume-a-sandbox-with-memory)
- [Best sandboxes for browser and computer-use agents](/guides/best-sandboxes-for-browser-and-computer-use-agents)
- [Run a long job in a cloud sandbox](/guides/run-a-long-job-in-a-cloud-sandbox)
