# How Do I Run Bun in a Cloud Sandbox?

> A new sandbox has Bun 1.4.3 in /usr/local/bin, which bun upgrade can't overwrite without root. Bun's install script puts 1.4.4 in ~/.bun/bin, already first on PATH. Elysia installed 219 packages in 0.4 seconds from an empty cache, a migrated pnpm project in 1 second, and bun build --compile cross-compiled a server for macOS.

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

**Short answer:** A new sandbox has Bun 1.4.3 in /usr/local/bin, which bun upgrade can't overwrite without root. Bun's install script puts 1.4.4 in ~/.bun/bin, already first on PATH. Elysia installed 219 packages in 0.4 seconds from an empty cache, a migrated pnpm project in 1 second, and bun build --compile cross-compiled a server for macOS.

We ran every command on this page on prod on 2026-10-27, on `small` sandboxes (2 vCPU, 4 GB) in eu-central. The main project is [elysiajs/elysia](https://github.com/elysiajs/elysia), a Bun-first web framework, at commit [`e037eca`](https://github.com/elysiajs/elysia/commit/e037eca710e7ad193be09cc6615ab0dbe54af914). It has a `bun.lock`, 1,616 tests run by `bun test`, and a TypeScript type check. For the package manager comparison we also installed [vitesse-lite](https://github.com/antfu-collective/vitesse-lite) at `0b35297`, the same pnpm project and commit as our [Node.js starter](/guides/node-dev-environment-in-a-cloud-sandbox).

## Results

| Step | Time on `small` |
| - | - |
| Install Bun 1.4.4 with the official script | 1.4 s |
| Elysia: `bun install --frozen-lockfile`, empty cache (3 runs) | 0.47, 0.42, **0.37 s** |
| Same, warm cache, no `node_modules` (3 runs) | 0.055, 0.053, 0.058 s |
| Same, nothing to do (3 runs) | 0.007 to 0.011 s |
| `bun test`, 1,616 tests in 134 files (2 runs) | 5.2 s each |
| `tsc` type tests (`bun run test:types`) | 9.7 s |
| vitesse-lite: first `bun install`, migrating `pnpm-lock.yaml` | 2.1 s |
| vitesse-lite: `bun install --frozen-lockfile`, empty cache (3 runs) | 1.10, 1.09, **0.97 s** |
| vitesse-lite: same, warm cache (3 runs) | 0.30, 0.32, 0.35 s |
| `bun build --compile`, native Linux binary | 0.19 s |
| `bun build --compile` for another OS, first time / again | 1.1 to 1.3 s / 0.4 s |
| From the named snapshot: `bun install`, as saved / cache only | 0.005 s / 0.055 s |

For comparison, the [Node.js starter](/guides/node-dev-environment-in-a-cloud-sandbox) measured a cold `pnpm install --frozen-lockfile` of vitesse-lite at 11.7 seconds on the same size, the same day. Both runs deleted the package store and metadata cache first. With Bun, the install is rarely what you wait for; tests and type checks are.

## 1. See what the sandbox already has

```bash
sbx new --name bun-1 --type small
sbx exec sb_zuwookunbs bash -lc "bun --version; bun --revision; which bun bunx; node --version"
```

```text
1.4.3
1.4.3+c6da4a4d3
/usr/local/bin/bun
/usr/local/bin/bunx
v22.23.3
```

Bun is a root-owned binary in `/usr/local/bin`, and Node 22 sits beside it for packages that call `node`. These are the versions on 2026-10-27; the image changes over time. The full inventory is in [What's pre-installed in a sandbox](/guides/whats-preinstalled-in-a-sandbox).

## 2. Upgrade Bun: `bun upgrade` fails, the install script works

Because the preinstalled binary belongs to root, `bun upgrade` downloads the new version and then can't replace it:

```bash
sbx exec sb_zuwookunbs --timeout 120 bash -lc "cd /tmp && bun upgrade"
```

```text
error: Failed to move new version of Bun to /usr/local/bin/bun to EACCES

Please reinstall Bun manually with the following command:
   curl -fsSL https://bun.com/install | bash
```

Do what it says. The [official install script](https://bun.com/docs/installation) installs into `~/.bun/bin`, and the sandbox's login `PATH` already puts `~/.bun/bin` ahead of `/usr/local/bin`. The current release on 2026-10-27 was 1.4.4 (published 2026-10-22). Pin it so every sandbox gets the same one:

```bash
sbx exec sb_zuwookunbs --timeout 180 bash -lc "curl -fsSL https://bun.com/install | bash -s bun-v1.4.4"
sbx exec sb_zuwookunbs bash -lc "which -a bun; bun --version"
```

```text
bun was installed successfully to ~/.bun/bin/bun
…
/home/user/.bun/bin/bun
/usr/local/bin/bun
1.4.4
```

It took 1.4 seconds, and no `~/.bashrc` edit or new shell was needed. Use `bash -lc` with `sbx exec` so the login `PATH` applies. From now on `bun upgrade` works too, because Bun lives in your home. Everything under `/home/user` survives `sbx stop` and goes into a snapshot.

## 3. Install, test and type-check a real project

```bash
sbx exec sb_zuwookunbs --timeout 120 bash -lc "git clone -q https://github.com/elysiajs/elysia ~/elysia && cd ~/elysia && git checkout -q e037eca710e7"
```

We timed three cases, three runs each, with a script in the sandbox. "Cold" deletes `node_modules` and Bun's global cache, `~/.bun/install/cache`. "Warm" deletes only `node_modules`.

```bash
cd ~/elysia
TIMEFORMAT=%R
for i in 1 2 3; do rm -rf node_modules ~/.bun/install/cache; echo -n "cold $i: "; { time bun install --frozen-lockfile >/dev/null 2>&1 ; } 2>&1; done
for i in 1 2 3; do rm -rf node_modules; echo -n "warm $i: "; { time bun install --frozen-lockfile >/dev/null 2>&1 ; } 2>&1; done
for i in 1 2 3; do echo -n "noop $i: "; { time bun install --frozen-lockfile >/dev/null 2>&1 ; } 2>&1; done
```

```text
cold 1: 0.466
cold 2: 0.422
cold 3: 0.372
warm 1: 0.055
warm 2: 0.053
warm 3: 0.058
noop 1: 0.007
noop 2: 0.011
noop 3: 0.011
```

The cold install downloaded 219 packages, about 135 MB, in under half a second. `--frozen-lockfile` installs exactly what `bun.lock` pins and fails if `package.json` disagrees with it ([Bun docs](https://bun.com/docs/pm/cli/install#production-mode)).

On Linux, Bun [hardlinks files](https://bun.com/docs/pm/cli/install#platform-specific-backends) from its cache into `node_modules`. In our sandbox `node_modules` was 134 MB, but `node_modules` and the cache together were only 141 MB, and a file like `node_modules/typescript/lib/typescript.js` had a link count of 2. Keeping both in a snapshot costs about the same as one.

Then the tests and the type check:

```bash
sbx exec sb_zuwookunbs --timeout 300 bash -lc "cd ~/elysia && bun test 2>&1 | tail -6 && bun run test:types"
```

```text
 1614 pass
 2 skip
 0 fail
 3008 expect() calls
Ran 1616 tests across 134 files. [5.15s]
$ tsc --project tsconfig.test.json
```

`bun test` ran the suite in 5.2 seconds both times; it has no result cache, so a rerun costs the same. `bun run test:types` runs `tsc` and took 9.7 seconds. Bun runs TypeScript without checking types, so keep a `tsc` step in CI.

## 4. Bring a pnpm or npm project over

`bun install` reads other lockfiles. On a project with `pnpm-lock.yaml` and no `bun.lock`, it [converts the lockfile](https://bun.com/docs/pm/cli/install#pnpm-migration) on the first install:

```bash
sbx exec sb_zuwookunbs --timeout 120 bash -lc "git clone -q https://github.com/antfu-collective/vitesse-lite ~/vl && cd ~/vl && git checkout -q 0b352977755e && bun install && git status --short"
```

```text
bun install v1.4.4 (38afee6c8)
[1157.76ms] migrated lockfile from pnpm-lock.yaml
copied pnpm-workspace.yaml to workspaces in package.json

622 packages installed [2.07s]
 M package.json
?? bun.lock
```

Two files changed: a new `bun.lock`, and `package.json`, which gained a `workspaces` entry copied from `pnpm-workspace.yaml`. `pnpm-lock.yaml` was left alone. Commit both changes, or discard them if you are only trying Bun.

With the migrated `bun.lock`, a cold `bun install --frozen-lockfile` of the 622 packages took 0.97 to 1.10 seconds, and a warm one about 0.3 seconds. The project's Vitest tests ran through `bun run test --run` (3 passed in 3.4 s).

## 5. A dev server with `--hot` on a private URL

We added a small Elysia app to the clone, `app.ts`. Set `hostname` yourself:

```typescript
import { Elysia, t } from "./src";

const app = new Elysia()
  .get("/", () => ({ msg: "hello v1", bun: Bun.version }))
  .get("/add", ({ query }) => ({ sum: query.a + query.b }), {
    query: t.Object({ a: t.Number(), b: t.Number() }),
  })
  .listen({ hostname: "127.0.0.1", port: 3000 });

console.log(`listening on ${app.server?.url}`);
```

Why 127.0.0.1: the [Bun.serve docs](https://bun.com/docs/runtime/http/server#changing-the-port-and-hostname) give `0.0.0.0` as the default, and in our sandbox a server without `hostname` listened on `*:3001` in `ss -ltn`, every address including IPv6. A sandbox also has a public IPv6 address. Its firewall (`ufw`) is closed except SSH by default, but binding to `127.0.0.1` keeps the port private whatever the firewall does, and leaves the private URL as the only way in.

Run it with [`--hot`](https://bun.com/docs/runtime/watch-mode) and host the port with `--localhost`:

```bash
sbx exec sb_zuwookunbs --cwd elysia --detach "bun --hot app.ts > ~/app.log 2>&1"
sbx host sb_zuwookunbs 3000 --localhost --title "Elysia dev"
```

```text
https://steady-sandy-marble-3000.box.sandboxer.com?_token=<token>
```

From our machine, without the token the URL answered `401` three times out of three. The token link answered `302` and set a cookie; with that cookie:

```text
GET /                 200  {"msg":"hello v1","bun":"1.4.4"}   0.20 s
GET /add?a=2&b=40     200  {"sum":42}
GET /add?a=2&b=x      422  {"type":"validation","on":"query","property":"/b",…}
```

Then we changed `hello v1` to `hello v2` in `app.ts` inside the sandbox. One second later the same URL returned `{"msg":"hello v2","bun":"1.4.4"}`, and `ss` showed the same server PID before and after. `--hot` reloads the code inside the running process instead of restarting it, so the hosted route and the relay stay up. `bun --watch` restarts the process instead.

A `--detach` process does not come back if the sandbox ever boots from disk. To keep a server running across that, use a systemd unit as shown in [Run a Next.js dev server with a preview URL](/guides/run-a-next-js-dev-server-with-a-preview-url).

## 6. Compile a single binary, for Linux or another OS

[`bun build --compile`](https://bun.com/docs/bundler/executables) bundles your code and the Bun runtime into one executable. `--target` builds for another OS and CPU:

```bash
cd ~/elysia
bun build app.ts --compile --minify --outfile dist/app
for t in bun-darwin-arm64 bun-windows-x64 bun-linux-arm64; do
  bun build app.ts --compile --minify --target=$t --outfile dist/app-$t
done
file dist/*
```

```text
dist/app:                     ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, …
dist/app-bun-darwin-arm64:    Mach-O 64-bit arm64 executable, flags:<NOUNDEFS|DYLDLINK|TWOLEVEL|BINDS_TO_WEAK|…
dist/app-bun-linux-arm64:     ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), dynamically linked, …
dist/app-bun-windows-x64.exe: PE32+ executable (console) x86-64, for MS Windows, 13 sections
```

| Target | Time | Size |
| - | - | - |
| Native linux-x64 | 0.19 s | 109 MB |
| darwin-arm64, first time / again | 1.14 s / 0.40 s | 90 MB |
| windows-x64 | 1.33 s | 116 MB |
| linux-arm64 | 1.20 s | 110 MB |

Each first cross build downloaded that target's Bun runtime into `~/.bun/install/cache/bun-<os>-<arch>-v1.4.4` (85 to 110 MB each); later builds reuse it. Put those runtimes in your snapshot and cross builds start at 0.4 seconds. The native binary ran in the sandbox and answered `{"sum":3}` on `/add?a=1&b=2`; it needs only glibc. We checked the other binaries' file types, not runs on a Mac or Windows. Copy one out with `sbx scp sb_zuwookunbs:/home/user/elysia/dist/app-bun-darwin-arm64 .`.

## 7. Save it as a named snapshot and start from it

Stop the dev server first; a snapshot keeps files, not processes. Stop it with `pkill` inside the sandbox; today `sbx exec --kill <pid>` can leave behind child processes that start their own session. The brackets keep `pkill` from matching the shell that runs it:

```bash
sbx exec sb_zuwookunbs "pkill -f '[b]un --hot app.ts'"
sbx snapshot sb_zuwookunbs bun-cache
sbx new --name bun-2 --type small --from bun-cache
```

The save took 2.1 seconds and the new sandbox was ready in 3.2. The snapshot holds Bun 1.4.4 in `~/.bun/bin`, the clone, `node_modules`, the package cache and the three cross-compile runtimes, about 1 GB in total. In the new sandbox:

```text
toolchain: /home/user/.bun/bin/bun 1.4.4
as saved 1: 0.070
as saved 2: 0.005
as saved 3: 0.005
cache only 1: 0.451
cache only 2: 0.057
cache only 3: 0.055
bun test: Ran 1616 tests across 134 files. [4.97s]
compile darwin-arm64: 0.334
```

The first command to touch the cache after a restore was slower each time (0.07 and 0.45 s), then installs matched the original sandbox. The macOS build reused the saved runtime and downloaded nothing. To name the snapshot after a hash of `bun.lock` and rebuild it when the lockfile changes, follow [Cache dependencies in a named snapshot](/guides/cache-dependencies-in-a-named-snapshot).

## 8. Run the Sandboxer SDK with Bun

The TypeScript SDK runs under Bun as is, and Bun runs the `.ts` file without a build step. Inside the sandbox, `SANDBOXER_TOKEN` and `SANDBOXER_API_URL` are already set:

```typescript
// whoami.ts, run in the sandbox with Bun
import { Sandboxer, SandboxerError } from "sandboxer-sdk";

const sandboxer = new Sandboxer(); // reads SANDBOXER_TOKEN and SANDBOXER_API_URL
const me = await sandboxer.get(process.env.SANDBOXER_ID!);
console.log(me.id, me.info.name, me.state);

const run = await me.exec("bun --version", { cwd: "elysia" });
console.log("exec:", run.stdout.trim());

try {
  await sandboxer.create({ type: "small" });
} catch (e) {
  if (e instanceof SandboxerError) console.log(e.status, e.code);
  else throw e;
}
```

```bash
mkdir ~/sdk-check && cd ~/sdk-check && bun add sandboxer-sdk && bun whoami.ts
```

```text
1 package installed [264.00ms]
sb_bsirjo4muy bun-2 ready
exec: 1.4.4
403 api_key_action_forbidden
```

The built-in token can act on its own sandbox but not create others. From your laptop or CI the same code works with `SANDBOXER_API_KEY`; the Node starter shows [the outside and inside cases side by side](/guides/node-dev-environment-in-a-cloud-sandbox#8-use-the-typescript-sdk-from-outside-and-inside).

## FAQ

### Which Bun version does a Sandboxer sandbox have?

On 2026-10-27, Bun 1.4.3 at `/usr/local/bin/bun`, with Node 22.23.3 also installed. The image is updated over time, so check `bun --version` in your own sandbox.

### Why does bun upgrade fail with EACCES?

The preinstalled Bun is owned by root, so `bun upgrade` can't replace it. Run `curl -fsSL https://bun.com/install | bash` instead. It installs into `~/.bun/bin`, which comes first on the sandbox's login `PATH`.

### Where does Bun cache packages in a sandbox?

In `~/.bun/install/cache`, on the sandbox disk. Bun hardlinks from it into `node_modules`, so both survive `sbx stop` and fit in a snapshot for little more than the size of one.

### Can I use Bun on a project that has a pnpm lockfile?

Yes. The first `bun install` wrote a `bun.lock` from `pnpm-lock.yaml` and added a `workspaces` field to `package.json`. Cold installs of that 622-package project then took about one second.

### Why should a Bun server bind to 127.0.0.1 in a sandbox?

Without `hostname`, our Bun server listened on every address, including the sandbox's public IPv6 address. The firewall is closed except SSH by default, but binding to `127.0.0.1` keeps the port private whatever the firewall does. Bind to `127.0.0.1` and publish the port with `sbx host --localhost`, which keeps it behind the private URL's token.

## Related

- [Set up a Node.js dev environment with pnpm](/guides/node-dev-environment-in-a-cloud-sandbox)
- [Cache dependencies in a named snapshot](/guides/cache-dependencies-in-a-named-snapshot)
- [Run a Next.js dev server with a preview URL](/guides/run-a-next-js-dev-server-with-a-preview-url)
- [Hosting](/docs/hosting)
