# Run OpenHands in a Cloud VM with Its UI on an HTTPS URL

> Create a Sandboxer VM, run OpenHands' own docker run command inside it (Docker is preinstalled and the socket mount stays inside your VM), then sbx host 3000 for a private HTTPS link. In one test run on October 27, 2026, the UI answered about 40 seconds after sbx new, on the default size.

Source: https://sandboxer.com/guides/run-openhands-in-a-cloud-vm. Updated 2026-10-27.

**Short answer:** Create a Sandboxer VM, run OpenHands' own docker run command inside it (Docker is preinstalled and the socket mount stays inside your VM), then sbx host 3000 for a private HTTPS link. In one test run on October 27, 2026, the UI answered about 40 seconds after sbx new, on the default size.

## At a glance

| Topic | What we measured (single run, October 27, 2026) |
| - | - |
| OpenHands version | `docker.openhands.dev/openhands/openhands:1.8` (API reports `1.8.0`), agent server `1.26.0-python` |
| Size used | `default`: 4 vCPU, 8 GB RAM, 50 GB disk, $0.036/hour. OpenHands [asks for at least 4 GB RAM](https://docs.openhands.dev/openhands/usage/run-openhands/local-setup) |
| Time to a working UI | 3.2 s `sbx new` + 24 s image pull and start + 12 s until `/` returned `200` |
| Memory with the UI idle | 810 MB used of 7.9 GB |
| Disk | 1.29 GB app image, plus the 3.73 GB agent-server image OpenHands pulls on its own; root disk went from 5.5 GB to 11 GB used |
| URL | `https://<subdomain>-3000.box.sandboxer.com`, private with a token by default |
| After stop and resume | Same container, same PID, UI answering again (memory restores only on a machine with the same CPU type; see step 4) |

We did not have an LLM key, so we didn't run an agent task. Everything below is the sandbox side: install, start, host, reach the UI and its API, stop and resume.

## Why run it in a VM

OpenHands' Docker setup mounts `/var/run/docker.sock` so the app can start the containers its agents work in. On your laptop that gives the app control of your Docker daemon. In a Sandboxer sandbox, the daemon belongs to that VM: Docker is preinstalled, the default `user` is in the `docker` group, and the socket only reaches containers inside the VM. Delete the sandbox and everything the agent made goes with it.

## 1. Create the VM

```bash
sbx new --name openhands --type default --ttl 7200 --no-env
```

```text
{"event":"ready","id":"sb_pdv3gf22hp","state":"ready",…,"url":"https://zealous-serene-atlas.box.sandboxer.com",…}
real    0m3.190s
```

`--no-env` keeps your account's secrets out of the VM. You'll paste your LLM key into the OpenHands UI later. `--ttl 7200` gives it two hours before auto-stop instead of one.

Check Docker and resources:

```bash
sbx exec sb_pdv3gf22hp "docker version --format {{.Server.Version}}; id; nproc; free -g | head -2"
```

```text
29.9.0
uid=1000(user) gid=1000(user) groups=1000(user),27(sudo),993(docker),995(kvm)
4
               total        used        free      shared  buff/cache   available
Mem:               7           0           6           0           0           7
```

## 2. Start OpenHands with its own Docker command

This is the `docker run` from [OpenHands' local setup docs](https://docs.openhands.dev/openhands/usage/run-openhands/local-setup). We made one change: `-d` instead of `-it --rm`, so it runs in the background. The docs also offer `uv tool install openhands --python 3.12` and `openhands serve`.

```bash
sbx exec sb_pdv3gf22hp --timeout 600 "docker run -d --pull=always \
  -e AGENT_SERVER_IMAGE_REPOSITORY=ghcr.io/openhands/agent-server \
  -e AGENT_SERVER_IMAGE_TAG=1.26.0-python \
  -e LOG_ALL_EVENTS=true \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v ~/.openhands:/.openhands \
  -p 3000:3000 \
  --add-host host.docker.internal:host-gateway \
  --name openhands-app \
  docker.openhands.dev/openhands/openhands:1.8"
```

```text
1.8: Pulling from openhands/openhands
…
Status: Downloaded newer image for docker.openhands.dev/openhands/openhands:1.8
c1ccc2162e2a6c991629f66f6dfad69e09c17eaa329bb5e67c878794e4e2929f
real    0m24.112s
```

Pass `--timeout`. Our first try without it was cut off at 30 seconds, mid-pull, before the container existed. The image tags change with each release, so copy the current command from OpenHands' docs.

Wait until it answers:

```bash
sbx exec sb_pdv3gf22hp --timeout 300 'until curl -s -o /dev/null -w "%{http_code}" localhost:3000 | grep -q 200; do sleep 1; done; docker logs openhands-app 2>&1 | tail -3'
```

```text
INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:3000 (Press CTRL+C to quit)
INFO:     172.17.0.1:44826 - "GET / HTTP/1.1" 200 OK
```

That took 12 seconds after the container started. While it ran, OpenHands also pulled the 3.73 GB `agent-server` image in the background.

## 3. Put the UI on an HTTPS URL

OpenHands listens on `0.0.0.0:3000`, so plain `sbx host` works. Keep it private: anyone with the UI can run commands in the VM.

```bash
sbx host sb_pdv3gf22hp 3000 --title OpenHands
```

```text
{"ok":true,"type":"port.hosted","sandboxId":"sb_pdv3gf22hp","port":3000,"url":"https://zealous-serene-atlas-3000.box.sandboxer.com?_token=…","access":"private","isProtected":true,"localhost":false}
```

Open that URL in your browser. The first visit sets a cookie and redirects to `/`, so the token leaves the address bar. Here's what we saw with curl:

```bash
URL='https://zealous-serene-atlas-3000.box.sandboxer.com?_token=…'
BASE=https://zealous-serene-atlas-3000.box.sandboxer.com

curl -sS -I "$URL" | grep -i -E '^(HTTP|location|set-cookie)'
curl -sS -L -c jar -b jar -o page.html -w '%{http_code}\n' "$URL"; grep -o '<title>[^<]*</title>' page.html
curl -sS -o /dev/null -w '%{http_code}\n' "$BASE/"          # no token, no cookie
curl -sS -b jar "$BASE/health"
curl -sS -b jar "$BASE/api/v1/app-conversations/count"
curl -sS -b jar "$BASE/api/v1/settings"
```

```text
HTTP/2 302
location: /
set-cookie: __sbx_auth=…; Path=/; Max-Age=2592000; HttpOnly; Secure; SameSite=Lax
200
<title>OpenHands</title>
401
"OK"
0
{"error":"Settings not found"}
```

The UI and its API answer through the hosted URL, and a request without the token gets `401`. `Settings not found` means no LLM has been configured yet. In the UI, open settings, choose a provider and model, and paste your key. OpenHands keeps settings and conversations in `~/.openhands` on the VM's disk, through the `-v ~/.openhands:/.openhands` mount.

To share the UI with a teammate, send the private URL. Use `--public` only for a throwaway sandbox with no keys in it.

## 4. Stop it and pick up later

Stopping saves the VM's memory as well as its disk, and a stopped sandbox costs nothing for compute (its disk and memory image count toward your plan's [included storage](/docs/billing#storage)). We noted the container's PID, stopped, resumed and checked again:

```bash
sbx exec sb_pdv3gf22hp 'docker inspect -f "{{.State.Pid}} {{.State.StartedAt}}" openhands-app'
sbx stop sb_pdv3gf22hp --wait     # returns once the stop's snapshot is done
sbx resume sb_pdv3gf22hp
sbx exec sb_pdv3gf22hp 'docker inspect -f "{{.State.Pid}} {{.State.StartedAt}}" openhands-app; curl -s -o /dev/null -w "%{http_code}\n" localhost:3000/health'
```

```text
691 2026-10-27T02:34:37.799958911Z
…
691 2026-10-27T02:34:37.799958911Z
200
```

The same container kept running, and the private URL and cookie from before the stop still worked (`/health` returned `200` in 0.2 s). Hosted routes and tokens are kept across stop and resume.

The exception: memory is restored only when the resume lands on a machine with the same CPU type. If none is free within 2 minutes, the sandbox [boots from disk instead](/docs/snapshots#resume) and shows `memoryRestored: false` with `resumeReason: "cpu_mismatch"`. Your files and `~/.openhands` are kept, but the container is not running, so run the same `docker run` again. To wait for a matching machine instead (up to 30 minutes), use `sbx resume --require-memory`. A resume at a new size (`--type large`) always boots from disk. In our test `memoryRestored` read `null` after the memory restore; it currently reads `null` after a resize cold boot too, so check the container (`docker inspect`, as above) rather than the field.

## Size

| Size | When |
| - | - |
| `default` (4 vCPU, 8 GB) | What we used. The idle app used 810 MB, which leaves room for the agent's runtime container and a typical repo's build. |
| `large` (8 vCPU, 16 GB, 125 GB disk) | Big repos, heavy test suites, or several OpenHands conversations running at once. It uses machine time at 2× the `default` rate. |

You can change size later with `sbx resume <id> --type large`. That cold-boots, so restart the container afterwards.

## FAQ

### Is it safe to mount the Docker socket into the OpenHands container here?

The socket gives OpenHands full control of the Docker daemon. Here that daemon runs inside your own VM, not on your laptop or a shared host. The worst it can reach is that VM, and with `--no-env` the VM holds none of your account's secrets.

### Why does the private URL return 401?

Private hosted URLs need the `_token` query parameter on the first visit. That visit sets a 30-day cookie, so later requests and the UI's own API calls work without it. Open the full URL that `sbx host` printed, or run `sbx host <id> 3000` again: it returns the same URL and token.

### Did you test an agent task?

No. We had no LLM key for this test, so we didn't start a conversation. We verified the install, the hosted UI, the `/health` and conversation API through the HTTPS URL, and the stop and resume. We didn't verify the agent's runtime containers starting for a task.

### Can I use `openhands serve` instead of `docker run`?

Yes. OpenHands' docs recommend `uv tool install openhands --python 3.12` and then `openhands serve`, which checks Docker and pulls the images for you. We tested the `docker run` path because it needs nothing but the Docker that's already in the VM.

## Related

- [Give a sandbox port an HTTPS URL](/guides/give-a-sandbox-port-an-https-url)
- [Docker and KVM inside an agent sandbox](/guides/docker-and-kvm-inside-an-agent-sandbox)
- [Pause and resume a sandbox with its memory](/guides/pause-and-resume-a-sandbox-with-memory)
- [Hosting](/docs/hosting)
