Skip to content
Guides · Starters

How Do I Run Bun in a Cloud Sandbox?

Updated October 27, 2026 .md

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, a Bun-first web framework, at commit e037eca. 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 at 0b35297, the same pnpm project and commit as our Node.js starter.

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

sbx new --name bun-1 --type small
sbx exec sb_zuwookunbs bash -lc "bun --version; bun --revision; which bun bunx; node --version"
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.

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:

sbx exec sb_zuwookunbs --timeout 120 bash -lc "cd /tmp && bun upgrade"
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 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:

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"
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#

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.

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

On Linux, Bun hardlinks files 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:

sbx exec sb_zuwookunbs --timeout 300 bash -lc "cd ~/elysia && bun test 2>&1 | tail -6 && bun run test:types"
 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 on the first install:

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

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 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 and host the port with --localhost:

sbx exec sb_zuwookunbs --cwd elysia --detach "bun --hot app.ts > ~/app.log 2>&1"
sbx host sb_zuwookunbs 3000 --localhost --title "Elysia dev"
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:

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.

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

bun build --compile bundles your code and the Bun runtime into one executable. --target builds for another OS and CPU:

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

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:

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.

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:

// 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;
}
mkdir ~/sdk-check && cd ~/sdk-check && bun add sandboxer-sdk && bun whoami.ts
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.

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.

More guides

Top AI sandbox platforms for code execution in 2026, ranked

For running AI-written code we rank Sandboxer, Freestyle and E2B first: each gives code its own VM with an API or SDK and can pause with memory. Pick Daytona for Windows or GPUs, Modal or Northflank for GPUs at scale, E2B or Northflank to run in your own cloud, and Vercel Sandbox if your app lives on Vercel.

Read the guide
Best sandboxes for running coding agents (Claude Code, Codex, Gemini CLI) in 2026, ranked

For coding agents we rank Sandboxer first: a full Ubuntu VM with Claude Code and Codex preinstalled, Docker, and stop-and-resume that keeps the agent's session in memory. Freestyle and Amp Orbs also keep memory across pauses. Pick E2B for prebuilt agent templates on open source, Docker Sandboxes for free local runs, and Codespaces if you live in GitHub.

Read the guide
Best Daytona sandbox alternatives in 2026, ranked

The best Daytona alternatives depend on why you're switching. For a full Linux microVM on every sandbox with memory-saving stop and Docker, we rank Sandboxer first. E2B suits open source and self-hosting, Modal massive short-job fleets, Northflank bring-your-own-cloud with GPUs, and Vercel Sandbox apps on Vercel.

Read the guide
Best E2B alternatives in 2026, ranked

If you're leaving E2B for longer sessions or a fuller machine, we rank Sandboxer first, then Freestyle: both give each sandbox a full Linux VM that keeps memory across pauses, with Docker and no short session cap. Pick Daytona for Windows or GPUs, Northflank for bring-your-own-cloud, and stay on E2B if you need open source.

Read the guide