The problem: we run work inside E2B sandboxes. When a
sandbox goes bad, we need to migrate it to a fresh one — and that means moving
the entire /home/user home directory (source code, node_modules,
Postgres data, build artifacts) from the old sandbox into a brand-new sandbox
as fast as possible.
This repo is a reproducible benchmark of that migration. It builds one realistic,
"heavy" sandbox template, then implements the home-directory transfer three
different ways so the approaches can be compared head-to-head. Each approach
prints the byte sizes of source vs. target home directories so you can confirm
the migration was complete, and solutions/utils.ts times every step.
A migration is only meaningful against a realistically large filesystem, so the
template (template.ts) bakes one in. On top of a Node 24 image it:
- installs a full dev toolchain (
build-essential,docker.io,git,gcsfuse,zip/unzip, etc.); - clones the
vercel/next.jsrepo and installs theblog-starterexample's dependencies — i.e. a large, many-small-filesnode_modules(the classic worst case for filesystem copies); - on start (
start.sh) launches a Postgres 16 container and seeds abig_home_datatable with 100k rows, with its data dir living under/home/user/postgres-data— i.e. a few large files.
So /home/user ends up containing both pathologies at once: a deep tree of
many tiny files and a handful of big ones. That's what each approach has to
move.
All three live in solutions/ and follow the same shape: spin up a source
sandbox (from the template), get its home directory out, spin up a target
sandbox, get the home directory in, then measure both sides.
The straightforward path, going through the host:
zip -qr /tmp/home.zip .the source home directory.- Read the zip bytes down to the host via the E2B SDK (
files.read). - Write those bytes up into the target sandbox (
files.write). - Clear the target home,
unzip, andchownit back touser.
Simple and dependency-free, but the whole archive round-trips through the host machine and pays compression + transfer cost on a single stream.
Use a Google Cloud Storage bucket mounted into both sandboxes via gcsfuse as the transfer medium — no bytes flow through the host:
- Mount the bucket at
/mnt/gcs-homein the source,mvthe home dir into it. - Mount the same bucket in a fresh target, copy the contents into
/home/user,chown.
Needs GOOGLE_APPLICATION_CREDENTIALS (a service-account key) and
GCS_BUCKET_NAME. Use scripts/setup-bucket.sh to provision a throwaway bucket
(it applies a 1-day lifecycle delete and grants the service account
roles/storage.objectUser).
Use a native E2B Volume as a portable disk that detaches from one sandbox and re-attaches to another:
- Create a volume, mount it at
/mnt/homein the source,mvthe home dir onto it. - Mount the same volume at
/home/userin a fresh target — the data is just there, no copy step.
The least data-shuffling of the three: the home directory rides along on the volume rather than being streamed or re-copied.
Prereqs: Bun and an E2B account.
bun installEnvironment (Bun auto-loads .env — no dotenv needed):
E2B_API_KEY— required for every approach.GOOGLE_APPLICATION_CREDENTIALS— path to a service-account JSON key; gcsfuse only.GCS_BUCKET_NAME— target bucket; gcsfuse only.
Build the sandbox template once (defaults to the dev env → sandbox-filesystem:dev):
bun build.ts # or: bun build.ts --env prodThen run any approach and read the timing logs + the final size table it prints:
bun solutions/zip.ts
bun solutions/gcsfuse.ts
bun solutions/volume.tsFor the gcsfuse approach, provision a bucket first:
# emits export lines for GCS_BUCKET_NAME etc.
PROJECT_ID=your-project SERVICE_ACCOUNT_EMAIL=sa@your-project.iam.gserviceaccount.com \
./scripts/setup-bucket.shtemplate.ts # E2B template: heavy Node + Next.js + Postgres fixture
build.ts # builds the template (bun build.ts [--env dev|prod])
start.sh # template start cmd: boots Postgres, seeds 100k rows
solutions/
zip.ts # approach 1 — zip through the host
gcsfuse.ts # approach 2 — GCS bucket via gcsfuse
volume.ts # approach 3 — E2B volume re-mount
utils.ts # log() — per-step timing wrapper
scripts/
setup-bucket.sh # provisions a throwaway GCS bucket for the gcsfuse run