Skip to content
Guides · Starters

How Do I Set Up a Node.js Dev Environment in a Cloud Sandbox With pnpm?

Updated October 27, 2026 .md

Short answer: A new sandbox has Node 22.23.3 and pnpm 12.14, and pnpm switches itself to the version in your packageManager field. For a newer Node, install fnm and run fnm install 24. Clone your repo, run pnpm install --frozen-lockfile, then save a named snapshot so the next sandbox skips the 11.7-second cold install.

We ran every command on this page on prod on 2026-10-27, on small sandboxes (2 vCPU, 4 GB) in eu-central. The project is antfu-collective/vitesse-lite at commit 0b35297: a Vite + Vue starter with a pnpm-lock.yaml, about 620 packages, Vitest tests and ESLint. This is the Node twin of the Python starter; editor setup over SSH is covered there.

Results#

Step Time on small
fnm install 24 (Node 24.22.1) 2.6 s
pnpm install --frozen-lockfile, empty store and cache (3 runs) 11.75, 11.72, 11.50 s
Same, warm store, no node_modules (3 runs) 0.73, 0.70, 0.70 s
Same, nothing to do (3 runs) 0.06 s
pnpm test --run (Vitest) 3 passed, 3.3 s wall
sbx snapshot to a named snapshot 2.1 s
SDK create({ from }) to ready 2.7 s
In the new sandbox: install as saved / store only (3 runs each) 0.14 s / 1.03 s

The install timings above ran on the system Node 22.23.3. The runs in the new sandbox used Node 24 from fnm. Unlike uv on the Python page, the cold pnpm install here is the slow step. The snapshot removes it.

1. See what the sandbox already has#

sbx new --name nodedev-1 --type small
sbx exec sb_clx3ut4cn8 bash -lc "node --version; which node npm corepack pnpm; npm --version; corepack --version; cd /tmp && pnpm --version"
v22.23.3
/usr/bin/node
/usr/bin/npm
/usr/bin/corepack
/usr/bin/pnpm
10.9.9
0.36.0
12.14.0

Node is the NodeSource package 22.23.3-1nodesource1. /usr/bin/pnpm is a global npm install of pnpm, not a Corepack shim. 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. When the image's Node is too old#

Some packages require a newer patch release than the image has. Astro 7.4.2 declares "node": ">=22.23.4" in its engines field and refuses to start:

sbx exec sb_clx3ut4cn8 bash -lc "cd /tmp && pnpm dlx astro@7.4 --version"
+ astro 7.4.2

Node.js v22.23.3 is not supported by Astro!
Please upgrade Node.js to a supported version: ">=22.23.4"

You have three ways to get another Node. We tested fnm.

Method What it does Needs sudo Tested here
fnm Installs any Node version under ~/.local/share/fnm and switches per shell No Yes
NodeSource Replaces the system nodejs apt package with another major line Yes (sudo needs no password) No
Corepack Installs package managers (pnpm, Yarn), not Node No Yes, on Node 24

Install fnm and Node 24, then make it the default for login shells:

sbx exec sb_clx3ut4cn8 --timeout 300 bash -lc '
  curl -fsSL https://fnm.vercel.app/install | bash -s -- --skip-shell
  export PATH=~/.local/share/fnm:$PATH
  fnm install 24
  cat >> ~/.profile <<"P"
export PATH="$HOME/.local/share/fnm:$PATH"
eval "$(fnm env --shell bash)"
P'
sbx exec sb_clx3ut4cn8 bash -lc "fnm default 24 && fnm ls && node --version && pnpm dlx astro@7.4 --version"
Installing Node v24.22.1 (x64)
…
* v24.22.1 default
* system
v24.22.1
   astro  v7.4.2

fnm took 2.3 seconds to install and Node 24 another 2.6 seconds. Put the fnm env lines in ~/.profile, not ~/.bashrc: Ubuntu's .bashrc returns early for non-interactive shells. With them in ~/.profile, sbx exec … bash -lc, detached commands and the SDK's exec all ran Node 24 in our test. Everything lives under /home/user, so it survives sbx stop and goes into a snapshot. fnm ls-remote also listed v22.23.4 if you want to stay on the 22 line.

3. pnpm and Corepack#

vitesse-lite's package.json pins "packageManager": "pnpm@12.3.4". You don't need Corepack to honor it. The image's pnpm 12.14 downloaded 12.3.4 and ran it inside the project:

sbx exec sb_clx3ut4cn8 bash -lc "git clone -q https://github.com/antfu-collective/vitesse-lite ~/app && cd ~/app && git checkout -q 0b352977755e && pnpm --version"
12.3.4

That is pnpm's managePackageManagerVersions setting. If your team standardizes on Corepack, or the project uses Yarn, enable it. fnm's Node directory is in your home, so no sudo is needed:

sbx exec sb_clx3ut4cn8 bash -lc "corepack --version && corepack enable pnpm && which pnpm && cd ~/app && pnpm --version"
0.36.0
/home/user/.local/state/fnm_multishells/2687_1793098779889/bin/pnpm
12.3.4
! Corepack is about to download https://registry.npmjs.org/pnpm/-/pnpm-12.3.4.tgz
…

The Corepack notice goes to stderr, so it can print after the version.

Corepack caches package managers in ~/.cache/node/corepack (109 MB in our sandbox, after a couple of versions).

4. Install: cold store, warm store, nothing to do#

We timed three cases, three runs each, with this script in the sandbox. "Cold" deletes node_modules, the store (~/.local/share/pnpm/store/v11) and the metadata cache (~/.cache/pnpm/v11). "Warm" deletes only node_modules and adds --offline to prove the network isn't used.

cd ~/app
TIMEFORMAT=%R
for i in 1 2 3; do rm -rf node_modules ~/.local/share/pnpm/store/v11 ~/.cache/pnpm/v11 ~/.cache/pnpm/lockfile-verified.jsonl; echo -n "cold $i: "; { time pnpm 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 pnpm install --frozen-lockfile --offline >/dev/null 2>&1 ; } 2>&1; done
for i in 1 2 3; do echo -n "noop $i: "; { time pnpm install --frozen-lockfile >/dev/null 2>&1 ; } 2>&1; done
cold 1: 11.745
cold 2: 11.499
cold 3: 11.716
warm 1: 0.729
warm 2: 0.696
warm 3: 0.697
noop 1: 0.062
noop 2: 0.067
noop 3: 0.064

--frozen-lockfile installs exactly what pnpm-lock.yaml pins and fails rather than updating it. Without --offline the warm runs took 0.72 to 0.82 seconds.

On disk, node_modules was 293 MB and the store 281 MB, but du counted only 302 MB for both together. pnpm hardlinks files from the store into node_modules, so keeping both in a snapshot costs about the same as one. The metadata cache was larger than either, 427 MB, almost all of it in metadata-full.

5. Run the tests and lint#

sbx exec sb_clx3ut4cn8 --timeout 300 bash -lc "cd ~/app && export NO_COLOR=1 && pnpm test --run && pnpm lint"
 Test Files  2 passed (2)
      Tests  3 passed (3)
…
✖ 2 problems (0 errors, 2 warnings)

pnpm test runs vitest, which watches by default; --run makes it exit. The tests took 3.3 seconds wall time and lint 5.9 seconds.

6. A dev server on a private URL#

Start Vite on 127.0.0.1 in the background, then host the port with --localhost:

sbx exec sb_clx3ut4cn8 --cwd app --detach "pnpm exec vite --host 127.0.0.1 --port 5173 --strictPort > ~/vite.log 2>&1"
sbx host sb_clx3ut4cn8 5173 --localhost --title "Vite dev"
https://first-upbeat-honey-5173.box.sandboxer.com?_token=<token>

Without the token the URL answered 401. With it, Sandboxer accepted the request, and Vite itself answered 403:

Blocked request. This host ("first-upbeat-honey-5173.box.sandboxer.com") is not allowed.
To allow this host, add "first-upbeat-honey-5173.box.sandboxer.com" to `server.allowedHosts` in vite.config.js.

Vite's dev server only answers hostnames it knows (server.allowedHosts). A leading dot allows a domain and all its subdomains, so one line covers every sandbox and port:

export default defineConfig({
  // …
  server: { allowedHosts: ['.box.sandboxer.com'] },
})

Vite restarted on its own when the config changed. Then the token link answered 302 and set a cookie, and the page loaded with 200 in about 0.2 seconds. We also opened Vite's HMR WebSocket through the hosted URL with that cookie. It answered 101 Switching Protocols and pushed a file-changed event when we edited src/components/TheCounter.vue in the sandbox. We tested the socket with curl, not a browser.

A --detach process does not come back if the sandbox ever boots from disk. To keep a dev server running across that, use a systemd unit as shown in Run a Next.js dev server with a preview URL.

7. Save it as a named snapshot#

Stop the dev server first. A snapshot keeps files, not processes:

sbx exec sb_clx3ut4cn8 --kill 2929
sbx snapshot sb_clx3ut4cn8 nodedev-pnpm
{"ok":true,"type":"snapshot.named.info","snapshot":{…,"name":"nodedev-pnpm",…,"status":"ready","type":"small",…}}

The save took 2.1 seconds. It holds Node 24, Corepack's pnpm, the clone, node_modules, the store and the metadata cache. You can delete the source sandbox afterwards. To name the snapshot after a hash of pnpm-lock.yaml and rebuild it when the lockfile changes, follow Cache dependencies in a named snapshot.

8. Use the TypeScript SDK from outside and inside#

Outside, on your laptop or CI runner, new Sandboxer() reads SANDBOXER_API_KEY. This script starts a sandbox from the snapshot, checks the toolchain, times the install and the tests, and deletes the sandbox:

// from-snapshot.mjs, run on your laptop or CI runner
import { Sandboxer } from "sandboxer-sdk";

const sandboxer = new Sandboxer(); // reads SANDBOXER_API_KEY
const t = performance.now();
const sb = await sandboxer.create({ type: "small", from: "nodedev-pnpm", name: "nodedev-2" });
console.log(`${sb.id} ready in ${((performance.now() - t) / 1000).toFixed(1)} s`);

const time = (cmd) => `TIMEFORMAT=%R; { time ( ${cmd} ) ; } 2>&1 | tail -2`;
try {
  const steps = [
    ["toolchain", "echo node $(node --version) pnpm $(pnpm --version)"],
    ...Array(3).fill(["as saved", "pnpm install --frozen-lockfile >/dev/null"]),
    ...Array(3).fill(["store only", "rm -rf node_modules && pnpm install --frozen-lockfile >/dev/null"]),
    ["tests", "NO_COLOR=1 pnpm test --run 2>/dev/null | grep 'Tests '"],
  ];
  for (const [label, cmd] of steps) {
    const run = await sb.exec(["bash", "-lc", time(cmd)], { cwd: "app", timeoutSeconds: 300 });
    console.log(`${label}: exit ${run.exitCode}, ${run.stdout.trim().split("\n").join(" | ")}`);
  }
} finally {
  await sb.delete();
  console.log("deleted", sb.id);
}
npm i sandboxer-sdk
node from-snapshot.mjs
sb_lqsmaswx4k ready in 2.7 s
toolchain: exit 0, node v24.22.1 pnpm 12.3.4 | 0.168
as saved: exit 0, 0.262
as saved: exit 0, 0.139
as saved: exit 0, 0.115
store only: exit 0, 1.638
store only: exit 0, 1.034
store only: exit 0, 0.926
tests: exit 0, Tests  3 passed (3) | 3.458
deleted sb_lqsmaswx4k

Node 24 and the pinned pnpm came with the snapshot; nothing was downloaded. The first command after the restore was slower each time we ran this script (we ran it four times). The "store only" case shows why you keep node_modules in the snapshot, not just the store: a relink still took about one second.

Inside the sandbox, SANDBOXER_TOKEN and SANDBOXER_API_URL are already set, so new Sandboxer() needs no arguments. That token is limited to its own sandbox:

// whoami.mjs, run in the sandbox
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("node --version && pnpm --version", { cwd: "app" });
console.log("exec:", run.stdout.trim().split("\n").join(" "));

let n = 0;
for await (const _ of sandboxer.list()) n++;
console.log(n, "sandbox(es) visible to this token");

try {
  await sandboxer.create({ type: "small" });
} catch (e) {
  if (e instanceof SandboxerError) console.log(e.status, e.code, e.message);
  else throw e;
}
mkdir ~/sdk-check && cd ~/sdk-check && pnpm add sandboxer-sdk && node whoami.mjs
sb_clx3ut4cn8 nodedev-1 ready
exec: v24.22.1 12.3.4
1 sandbox(es) visible to this token
403 api_key_action_forbidden This API key does not allow sandbox.create.

pnpm add took 0.8 seconds; the SDK has no runtime dependencies. Keep it in its own folder so it doesn't change your project's lockfile. The name is on me.info: the Sandbox object has no name property of its own.

Outside (laptop, CI) Inside the sandbox
Key Your SANDBOXER_API_KEY SANDBOXER_TOKEN, set for you
Can create, fork, snapshot, delete Yes, as your key's scope allows No (403)
Can exec, read and write files, host ports Any sandbox your key reaches Only its own sandbox
Use it for Orchestrating sandboxes, CI Code in the sandbox that needs its own id, files or hosted URL

FAQ#

Which Node.js version does a Sandboxer sandbox have?#

On 2026-10-27, Node 22.23.3 from NodeSource, with npm 10.9.9, Corepack 0.36.0 and pnpm 12.14.0. The image is updated over time, so check node --version in your own sandbox.

Do I need Corepack to use the pnpm version my project pins?#

No. In our test the preinstalled pnpm read packageManager and ran the pinned 12.3.4 itself. Use Corepack if your team already does or the project uses Yarn.

Why does my Vite dev server say "Blocked request. This host is not allowed"?#

Vite rejects hostnames that aren't in server.allowedHosts. Add '.box.sandboxer.com' to it; Vite restarts by itself when the config changes.

Does a Node version installed with fnm survive sbx stop?#

Yes. fnm keeps Node under /home/user/.local/share/fnm, which is on the sandbox disk. Stop keeps the disk, and a named snapshot or fork copies it.

Can code inside the sandbox start more sandboxes with the SDK?#

Not with the built-in SANDBOXER_TOKEN. It returned 403 api_key_action_forbidden for create in our test. Orchestrate from outside with your own API key.

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