# How to Run Storybook in a Sandbox With a Shareable URL

> Run storybook dev on port 6006 in a sandbox, add .box.sandboxer.com to core.allowedHosts, set Vite's hmr.clientPort to 443, then run sbx host <id> 6006 --localhost. Keep that dev URL private. For a link anyone can open, run storybook build, serve storybook-static and host that port with --public.

Source: https://sandboxer.com/guides/run-storybook-with-a-shareable-url. Updated 2026-10-27.

**Short answer:** Run storybook dev on port 6006 in a sandbox, add .box.sandboxer.com to core.allowedHosts, set Vite's hmr.clientPort to 443, then run sbx host <id> 6006 --localhost. Keep that dev URL private. For a link anyone can open, run storybook build, serve storybook-static and host that port with --public.

## What we ran

Everything below ran on prod on 2026-10-27, on one `small` sandbox (2 vCPU, 4 GB), with Storybook 10.6.4, Vite 8.3.5 and React 19 on the sandbox's Node.js 22. Tokens are replaced with `<token>`. For how hosted URLs, tokens and cookies work in general, see [Give a sandbox port an HTTPS URL](/guides/give-a-sandbox-port-an-https-url). This page covers what Storybook needs on top of that.

| Step | Result |
| - | - |
| Vite scaffold and `npm install` | 10.3 s |
| `storybook init` | 25.7 s |
| Dev server start (systemd restart to first answer) | 1.6 to 2.4 s |
| Dev URL, default config | `502`, then `403 Invalid host` |
| Dev URL with `allowedHosts` | `200`, 11 stories in `index.json`, about 0.19 s per request |
| Hot reload through the URL | Broken until `hmr.clientPort: 443`; then the edit showed in 176 ms, no page reload |
| `storybook build` | 3.6 s, 8.5 MB, 63 files |
| Static build on a public URL | `200`, Button story drawn in 4.5 s in headless Chromium |
| After `sbx stop --wait` and `sbx resume` | `memoryRestored: true`, same PIDs, both URLs answered at once |

## Dev server or static build?

| | Dev server (`storybook dev`) | Static build (`storybook build`) |
| - | - | - |
| Who it's for | You, or a reviewer pairing with you | Designers, PMs, anyone with the link |
| Hot reload | Yes, over WebSockets | No, rebuild to update |
| What a visitor can do | Storybook warns that the dev server lets clients write files in your project through its WebSocket channel | Read the built HTML, JS and CSS |
| Hosted access | Private only | Public is fine if the components aren't secret |
| Server | Node.js, about 2 s to start | Any static file server |

## 1. Install Storybook

Storybook's [install docs](https://storybook.js.org/docs/get-started/install) add it to an existing project. We made a Vite React app first, then ran the initializer without prompts:

```bash
sbx new --name storybook --type small
sbx exec sb_dt3tuszvq5 --timeout 600 '
  npm create vite@latest sb-demo -- --template react-ts --no-interactive
  cd sb-demo && npm install
  npx -y storybook@latest init --yes --no-dev --features docs --disable-telemetry --no-agent'
```

```text
added 28 packages in 9s
…
◇  Storybook was successfully installed in your project!
```

`--no-dev` stops `init` from starting a server inside your `exec`. `--features docs` skips the test addon, which would also pull in Vitest and browser binaries. The default `--timeout` of `sbx exec` is 30 s, and `init` took 25.7 s, so raise it.

## 2. Run the dev server as a service

`npm run storybook` runs `storybook dev -p 6006`, which listens on `127.0.0.1` only. A systemd unit keeps it running and starts it again after a cold boot. Run this inside the sandbox (over `sbx ssh`, or save it as a script and pass it to `sbx exec`). It writes the unit with your user, home and npm path filled in:

```bash
NPM=$(command -v npm)
sudo tee /etc/systemd/system/storybook-dev.service >/dev/null <<UNIT
[Unit]
Description=Storybook dev server
After=network-online.target
[Service]
User=$USER
WorkingDirectory=$HOME/sb-demo
Environment=PATH=$(dirname $NPM):/usr/bin:/bin
Environment=STORYBOOK_DISABLE_TELEMETRY=1
ExecStart=$NPM run storybook -- --no-open --ci
Restart=always
[Install]
WantedBy=multi-user.target
UNIT
sudo systemctl daemon-reload && sudo systemctl enable --now storybook-dev
ss -ltn | grep 6006
```

```text
LISTEN 0      511        127.0.0.1:6006      0.0.0.0:*
```

It answered on `localhost` 2.4 s after the start.

## 3. Host it, and the two errors you will hit

A plain `sbx host` can't reach a server bound to `127.0.0.1`:

```bash
sbx host sb_dt3tuszvq5 6006 --title "Storybook dev"
```

```text
https://direct-noble-squid-6006.box.sandboxer.com?_token=<token>
```

Without the token, the URL returns `401`. With it, you get the 30-day cookie and then `502` with the page "Nothing is answering". Host the same port again with `--localhost` so the sandbox relays to the loopback address. The URL and token stay the same:

```bash
sbx host sb_dt3tuszvq5 6006 --localhost --title "Storybook dev"
```

Now Storybook itself answers, and refuses the request:

```text
/ with cookie: 403  Invalid host
/index.json with cookie: 403  Invalid host
```

Storybook checks the `Host` header, and `direct-noble-squid-6006.box.sandboxer.com` isn't on its list. Add the hosted domain to [`core.allowedHosts`](https://storybook.js.org/docs/api/main-config/main-config-core#allowedhosts). A leading dot allows every subdomain, so the same file works in any sandbox:

```typescript
// .storybook/main.ts
import type { StorybookConfig } from '@storybook/react-vite';
import { mergeConfig } from 'vite';

const config: StorybookConfig = {
  stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
  addons: ['@storybook/addon-docs'],
  framework: '@storybook/react-vite',
  // Accept requests for the hosted URL (*.box.sandboxer.com).
  core: { allowedHosts: ['.box.sandboxer.com'] },
  // The browser reaches the sandbox on 443, not on 6006.
  viteFinal: (config) => mergeConfig(config, { server: { hmr: { clientPort: 443 } } }),
};
export default config;
```

After `sudo systemctl restart storybook-dev` (1.6 s to the first answer):

```text
/ with cookie: 200 in 0.195 s
/index.json with cookie: 200 in 0.191 s
11 entries: configure-your-project--docs, example-button--docs, example-button--large, example-button--primary, …
```

A request without the token still gets `401` from the proxy, so Storybook never sees it.

Storybook offers another route: `--host 0.0.0.0` and a plain `sbx host`. We tried that only on a local machine, not on prod. There, Storybook 10.6.4 printed a warning that it was listening on all interfaces, that anyone who can reach it can read your stories and source files and write files in your project through the WebSocket channel and the MCP server, and to set `core.allowedHosts` to keep host validation on. The `--localhost` route above keeps validation on.

## 4. Fix hot reload behind the URL

`allowedHosts` alone gets you a working Storybook, but edits don't show up. We opened the token link in headless Chromium (Playwright), edited `Button.tsx` in the sandbox, and waited for the change. It never came. The browser log showed why:

```text
[vite] connecting...
WebSocket connection to 'wss://direct-noble-squid-6006.box.sandboxer.com:6006/?token=<token>' failed: net::ERR_CONNECTION_REFUSED
[vite] failed to connect to websocket (Error: WebSocket closed without opened.).
```

Vite's client in the preview iframe connects to the dev server's port, 6006, on the hosted name. The hosted URL only serves 443. Storybook's own channel (`/storybook-server-channel`) used 443 and connected. The `viteFinal` line in the file above sets Vite's [`server.hmr.clientPort`](https://vite.dev/config/server-options#server-hmr) to 443. With it, the same test passed:

```text
first response: 200
[vite] connecting...
[vite] connected.
Primary story rendered after 4113 ms, button text: Button
[vite] hot updated: /src/stories/Button.tsx
edited Button.tsx in the sandbox: new class showed up after 176 ms
page reloaded: false
```

The browser had only the cookie from the token link. The WebSocket handshakes carried that cookie, so hot reload works on a private URL without extra steps.

## 5. Publish a static build on a public URL

For a link you can paste in a PR or a chat, build Storybook and serve the files. The [publishing docs](https://storybook.js.org/docs/sharing/publish-storybook) describe the build; any static server works, and Python is already in the sandbox:

```bash
sbx exec sb_dt3tuszvq5 --timeout 600 "cd ~/sb-demo && npm run build-storybook && du -sh storybook-static"
```

```text
◇  Output directory: /home/user/sb-demo/storybook-static
8.5M	storybook-static
```

The build took 3.6 s. A second unit serves it on port 6007, bound to all interfaces:

```ini
# /etc/systemd/system/storybook-static.service
[Unit]
Description=Static Storybook
After=network-online.target

[Service]
User=user
ExecStart=/usr/bin/python3 -m http.server 6007 --directory /home/user/sb-demo/storybook-static
Restart=always

[Install]
WantedBy=multi-user.target
```

```bash
sbx exec sb_dt3tuszvq5 "sudo systemctl daemon-reload && sudo systemctl enable --now storybook-static"
sbx host sb_dt3tuszvq5 6007 --public --title "Storybook static"
```

```text
/: 200 in 0.197 s
/index.json: 200 in 0.192 s
/iframe.html?id=example-button--primary&viewMode=story: 200 in 0.291 s
/storybook-server-channel: 404 in 0.192 s
```

There is no write channel to reach: `/storybook-server-channel` is a plain `404`. Headless Chromium drew the Button story from `https://direct-noble-squid-6007.box.sandboxer.com/?path=/story/example-button--primary` in 4.5 s, with no WebSockets opened. To update the link, run `npm run build-storybook` again. The server picks up the new files without a restart.

## 6. Stop and resume

```bash
sbx stop sb_dt3tuszvq5 --wait
sbx resume sb_dt3tuszvq5
```

While stopped, both URLs returned `404`. The resume took 1.3 s and reported `"memoryRestored": true`. Both servers kept their PIDs, the public URL answered `200` on the first try, and the dev URL answered `200` with the cookie set before the stop.

Memory comes back only when the resume lands on a machine with the same CPU type. Otherwise the sandbox boots from disk: files, hosted routes and tokens are kept, and processes start fresh. Both units are `enabled`, so systemd starts them at boot. We didn't force a cold boot on this sandbox; [Run code-server in the browser](/guides/run-code-server-vs-code-in-the-browser) shows the same systemd pattern surviving one, tested with a fork.

## FAQ

### Why does my hosted Storybook say "Invalid host"?

Storybook rejects `Host` headers it doesn't know. Add `'.box.sandboxer.com'` to `core.allowedHosts` in `.storybook/main.ts` and restart the dev server.

### Storybook loads but my edits don't appear. Why?

Vite's hot reload client tries to connect on port 6006, which the hosted URL doesn't serve. Set `server.hmr.clientPort` to 443 in `viteFinal`. The browser console shows `[vite] connected.` when it works.

### Can I make the dev server URL public?

Don't. Storybook's own warning says clients of the dev server can read source files and write files in your project. Keep the dev port private and share a static build with `--public` instead.

### Do reviewers need a Sandboxer account to open the private link?

No. Anyone with the full link gets a 30-day cookie and can use Storybook, hot reload included. Treat the link like a password. Switching the port to public and back to private makes a new token and cuts off the old link.

## Related

- [Give a sandbox port an HTTPS URL](/guides/give-a-sandbox-port-an-https-url)
- [Get a preview URL for every pull request](/guides/get-a-preview-url-for-every-pull-request)
- [Run code-server (VS Code in the browser)](/guides/run-code-server-vs-code-in-the-browser)
- [Hosting](/docs/hosting)
