# How Do I Run Home Assistant in a Cloud Sandbox to Try It Out?

> Run the official Home Assistant Container image in a Sandboxer sandbox and host port 8123 with sbx host. Write an http block that trusts the sandbox's gateway as a reverse proxy before the first start, or every request through the URL gets 400 Bad Request. Onboarding, the REST API and WebSockets then work.

Source: https://sandboxer.com/guides/run-home-assistant-in-a-sandbox. Updated 2026-10-27.

**Short answer:** Run the official Home Assistant Container image in a Sandboxer sandbox and host port 8123 with sbx host. Write an http block that trusts the sandbox's gateway as a reverse proxy before the first start, or every request through the URL gets 400 Bad Request. Onboarding, the REST API and WebSockets then work.

This is a place to try Home Assistant, build dashboards and test automations without a Raspberry Pi. It is not a replacement for a box at home: the sandbox is in a data center, not on your home network, and it has no USB, Zigbee or Bluetooth radio.

## At a glance

Everything below ran on one `default` sandbox (4 vCPU, 8 GB RAM) on October 27, 2026. Single runs, so treat timings as rough.

| Step | What we saw |
| - | - |
| Pull `ghcr.io/home-assistant/home-assistant:stable` | 2.35 GB image, 45 s |
| Home Assistant version | `2026.10.4` |
| `docker run` to first HTTP response | 5.2 s |
| Hosted URL, default settings | `400: Bad Request` on every page |
| Hosted URL, gateway trusted before first start | `302` to `/onboarding.html`, then `200` |
| RAM in use after onboarding | 281 MiB |
| REST service call through the URL (20 calls) | median 129 ms, p90 131 ms, from our test machine |
| WebSocket API through the URL | `auth_ok`, `state_changed` events arrive |
| `sbx stop --wait` + `sbx resume` | `memoryRestored: true`, same container PID |
| Reboot | Home Assistant answering 3.7 s after boot, helper state restored |

## 1. Create a sandbox and pull the image

```bash
sbx new --name home-assistant --type default
id=sb_kuf6ceh6ps    # the id that sbx new printed

sbx exec $id --timeout 600 "docker pull -q ghcr.io/home-assistant/home-assistant:stable"
```

Docker is preinstalled. Pass `--timeout 600`: `sbx exec` stops waiting after 30 seconds by default, and the pull took 45.

## 2. Why the hosted URL returns 400 by default

We first started Home Assistant exactly as its [Linux install page](https://www.home-assistant.io/installation/linux/#install-home-assistant-container) shows and hosted port 8123. The token URL set its cookie, then every page returned this:

```text
withtoken 302 -> https://rare-even-atlas-8123.box.sandboxer.com/
cookie / 400 ->
400: Bad Request
```

The container log says why:

```text
ERROR (MainThread) [homeassistant.components.http.forwarded] A request from a reverse proxy was received from 10.202.0.69, but your HTTP integration is not set-up for reverse proxies
```

The Sandboxer proxy adds `X-Forwarded-For`, `X-Forwarded-Proto` and `X-Forwarded-Host` to each request and connects to the VM from its gateway address (`10.202.0.69` in this sandbox). Home Assistant blocks forwarded requests from any proxy it hasn't been told to trust. See the [HTTP integration](https://www.home-assistant.io/integrations/http/).

Adding an `http:` block to `configuration.yaml` after the first start did not fix it. Since 2026.8, per the [HTTP integration page](https://www.home-assistant.io/integrations/http/), Home Assistant imports that block once, on its first start, and keeps the settings in `.storage/http` from then on. A block added later is ignored. So write it before the first start.

## 3. Write the proxy setting, then start Home Assistant

Save this as `ha-setup.sh` on your computer. It reads the gateway from the sandbox's default route and writes a minimal `configuration.yaml` before the container ever runs.

```bash
# ha-setup.sh
docker rm -f homeassistant >/dev/null 2>&1
sudo rm -rf ~/homeassistant && mkdir -p ~/homeassistant
gw=$(ip route show default | awk '{print $3}')
cat > ~/homeassistant/configuration.yaml <<YAML
default_config:

http:
  use_x_forwarded_for: true
  trusted_proxies:
    - $gw
YAML
cat ~/homeassistant/configuration.yaml
s=$(date +%s%N)
docker run -d --name homeassistant --privileged --restart=unless-stopped --stop-timeout 60 \
  -e TZ=America/New_York -v ~/homeassistant:/config -v /run/dbus:/run/dbus:ro \
  --network=host ghcr.io/home-assistant/home-assistant:stable >/dev/null
until curl -sf -o /dev/null localhost:8123/manifest.json; do sleep 0.5; done
echo "first response after $(( ($(date +%s%N)-s)/1000000 )) ms"
```

```bash
sbx scp ha-setup.sh $id:ha-setup.sh
sbx exec $id --timeout 300 "bash ha-setup.sh"
```

```text
default_config:

http:
  use_x_forwarded_for: true
  trusted_proxies:
    - 10.202.0.69
first response after 5210 ms
```

The `docker run` line is the official one with two additions from the same page: a real time zone and `--stop-timeout 60`, so Home Assistant has time to close its database on a stop. `--restart=unless-stopped` is what brings it back after a cold boot.

Keep the `default_config:` line. Without a `configuration.yaml`, Home Assistant writes one with `default_config:` in it. Because you write the file first, you have to add that line yourself.

After the first start, `.storage/http` held `"trusted_proxies": ["10.202.0.69/32"]` and Home Assistant raised a repair asking you to remove the YAML block. We removed it and restarted: the hosted URL kept working, because the setting now lives in storage. From then on you change it in **Settings > System > Network > HTTP server**.

## 4. Give it a private HTTPS URL

```bash
sbx host $id 8123 --title "Home Assistant"
```

```text
https://rare-even-atlas-8123.box.sandboxer.com?_token=<token>
```

| Request | Status |
| - | - |
| `https://rare-even-atlas-8123.box.sandboxer.com/` (no token) | `401` from the Sandboxer proxy |
| The full URL with `?_token=` | `302` to `/`, sets the `__sbx_auth` cookie |
| `/` with the cookie, before onboarding | `302` to `/onboarding.html` |
| `/onboarding.html` with the cookie | `200`, `<title>Home Assistant` |

Open the token URL in your browser and create the owner account. The first account becomes the owner, so do it before you send the link to anyone. [Give a sandbox port an HTTPS URL](/guides/give-a-sandbox-port-an-https-url) covers public URLs and revoking a link.

We ran onboarding through the API from Node 24 so we could record each response. In a browser you fill in the same four steps:

```text
onboarding/users 200 auth_code received
auth/token 200 Bearer expires_in 1800
onboarding/core_config 200
onboarding/analytics 200
onboarding/integration 200
GET /api/ 200 {"message":"API running."}
GET /api/config 200 2026.10.4 UTC RUNNING
```

`/api/config` reported `UTC` even though the container has `TZ=America/New_York`. The `TZ` variable sets the container's clock. Home Assistant's own time zone, used by automations, is the one you pick during onboarding (we sent an empty `core_config` step, so it stayed `UTC`).

## 5. Two locks: the Sandboxer token and the Home Assistant login

Both checks apply to every request through a private URL:

| Request to `/api/` | Status |
| - | - |
| Home Assistant bearer token, no Sandboxer cookie | `401` (the proxy stops it) |
| Sandboxer cookie, no Home Assistant token | `401` (Home Assistant stops it) |
| Both | `200` |

A WebSocket to `/api/websocket` without the cookie was refused at the handshake. With the cookie it got `auth_required` from Home Assistant 2026.10.4.

The proxy keeps the real client address. We sent a wrong password twice, once with a forged `X-Forwarded-For: 203.0.113.9` header. Both times Home Assistant logged our machine's real public IP, not the forged one. So its login-attempt ban works against the actual caller.

## 6. Try it without devices

The sandbox can't see anything on your home network, so there is nothing to discover. Helpers work well for testing automations. We created a toggle over the WebSocket API (`input_boolean/create`, name "Porch light"), then called a service 20 times through the hosted URL:

```javascript
const t = performance.now();
const r = await fetch(`${base}/api/services/input_boolean/toggle`, {
  method: "POST",
  headers: { Cookie: cookie, Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
  body: JSON.stringify({ entity_id: "input_boolean.porch_light" }),
});
```

```text
input_boolean/create true porch_light
toggle x20 status 200 median 129 ms p90 131 ms
ws: auth_required 2026.10.4
ws: auth_ok
ws: result true
ws: state_changed input_boolean.porch_light off -> on
```

The 129 ms is the whole round trip from our test machine to the sandbox's `eu-central` region and back, so your number depends on where you are. The WebSocket subscription saw the state change made by a REST call, which is how the frontend gets live updates.

Starting the `demo` integration through the config-flow API returned `abort` with reason `not_implemented`, so we used helpers instead.

## 7. Stop, resume and reboot

```bash
sbx exec $id "docker inspect -f pid={{.State.Pid}} homeassistant"
sbx stop $id --wait
sbx resume $id
sbx exec $id "docker inspect -f pid={{.State.Pid}} homeassistant; ip route show default"
```

```text
pid=1341
ready  memoryRestored=true      # from the resume JSON
pid=1341
default via 10.202.0.69 dev eth0
```

Stop and resume each took about 2 seconds. Home Assistant kept running in memory, and the URL, cookie and login kept working.

A resume on a machine with a different CPU type boots from disk instead (`memoryRestored: false`). To test that path we rebooted with `sudo systemctl reboot`:

```text
1.62 3.64                                   # /proc/uptime when exec answered
3.67 9.70                                   # /proc/uptime when Home Assistant answered
pid=561 restarts=0 status=running
default via 10.202.0.69 dev eth0
porch_light 200 on
```

The container came back on its own because of its restart policy. "Porch light" was still `on`, and the refresh token from before still worked. The gateway address was the same after the resume and the reboot.

**If the URL ever starts returning 400 again**, compare `ip route show default` with the trusted proxy in **Settings > System > Network**. You can reach the UI without the proxy, so without `X-Forwarded-For`, through an SSH tunnel:

```bash
sbx forward $id --remote 8123 --local 18123
# then open http://127.0.0.1:18123 on your computer
```

```text
forward localhost:18123/ 200
```

## What fits and what doesn't

| You want to | Fits? |
| - | - |
| Try Home Assistant before buying hardware | Yes |
| Build dashboards, test automations and templates against helpers | Yes |
| Test an integration that talks to a cloud API over the internet | Yes, it has outbound internet |
| Control devices on your home LAN, or use Zigbee, Z-Wave or Bluetooth | No. It isn't on your network and has no radios |
| Apps (the add-on store) | No. Home Assistant's [Linux install page](https://www.home-assistant.io/installation/linux/) says the Container install has no access to apps |
| Run your home 24/7 | No. Automations don't run while the sandbox is stopped |

## FAQ

### Which size should I use?

We used `default`. Home Assistant used 281 MiB of RAM after onboarding, and the image is 2.35 GB. By those numbers `small` (2 vCPU, 4 GB RAM, 12 GB disk) should be enough for a trial, but we didn't run it there.

### Can I add the http block after Home Assistant has started once?

Not in YAML. In 2026.10.4 a block added after the first start had no effect and every proxied request still got `400`. Use **Settings > System > Network**, reached through `sbx forward`, or start over with an empty config folder.

### Should I trust all of 10.0.0.0/8 instead of one address?

No need. Trust only the gateway from `ip route show default`. That is the address the Sandboxer proxy connects from.

### Does the sandbox stop on its own?

Yes. It stops after one hour by default and pauses after a period of inactivity, and no automation runs while it is stopped. Change these with `sbx extend $id --hours 12` and `sbx idle $id 0`. See [idle auto-pause and wake on use](/guides/idle-auto-pause-and-wake-on-use).

### Do I need --privileged?

Home Assistant's own command uses it, and it worked in the sandbox, which runs Docker inside its own VM. We didn't test without it.

## Related

- [Give a sandbox port an HTTPS URL](/guides/give-a-sandbox-port-an-https-url)
- [Run Docker Compose in a sandbox with a URL per service](/guides/run-docker-compose-in-a-sandbox-with-a-url-per-service)
- [Does a cloud sandbox pause itself when idle?](/guides/idle-auto-pause-and-wake-on-use)
- [Hosting](/docs/hosting)
