SSD Nodes Learn 🎉 VPS from $5.50/mo
Guides Matt ConnorBy Matt Connor

Self-host Dormice for agent sandboxes

Dormice runs E2B-compatible agent sandboxes on one VPS you own. Install it, execute code in a sandbox, check the isolation, and size the host.

What Dormice is, and what it is not

Dormice is a self-hosted agent sandbox: one daemon on a Linux VPS you own, which your agent code calls over HTTP to run untrusted code inside an isolated container. Your program asks for a sandbox by name, gets the same sandbox back whatever state it was in, runs a command inside it, and reads the output. The sandbox is a programmatic resource, not a machine you log into.

That is a different shape from giving an agent a whole computer. A throwaway VM for a coding agent is a box you SSH into, let the agent wreck, then delete. Dormice sits one level down: it is the execution API your program calls when it already has code and needs somewhere safe to run it. Use the throwaway VM when a whole machine is the unit of work. Use Dormice when a single exec call is the unit of work, and you want a hundred of them a day without a hundred VMs.

The project calls itself E2B compatible. E2B is a hosted sandbox service whose client library many agent frameworks already import. Dormice serves the same protocol under its own URL prefixes, so an application written against the official e2b package keeps running when you point it at your own box. The application code does not change. Two URLs and one API key prefix change.

What "the SQLite of agent sandboxes" means in practice

SQLite is a database you embed instead of a service you operate, and Dormice borrows that comparison directly. One daemon, one SQLite file for the ledger, one TCP port. No Kubernetes, no separate database, no scheduler. The daemon takes a lock beside its ledger and refuses to start when its ledger and the machine it finds cannot belong together, so a split brain cannot happen quietly. One machine is the design. If you need a fleet across many hosts, the README tells you plainly to pick something else, and you should listen.

The second half of the idea is about cost. A hosted sandbox bills for every second it exists, so hosted sandboxes are disposable by design. Dormice runs on hardware you already pay for, so its sandboxes are permanent and get cheaper the longer they sit still. A sandbox cools down one rung at a time: active, then frozen, then stopped, then archived. Any acquire pulls it back up from whichever rung it reached.

Freezing is the part worth understanding, because it is what makes keeping every agent's sandbox forever affordable. These are the project's own published figures, measured on its hardware and not on yours.

ChartOne idle sandbox before and after freezing, figures published by the project
The data behind this chart
[
  {
    "label": "Active, holding 1 GiB",
    "resident_memory_mib": 1024,
    "wake_ms": 0
  },
  {
    "label": "Frozen",
    "resident_memory_mib": 5,
    "wake_ms": 50
  }
]

An idle sandbox holding 1024 MiB of memory drops to 5 MiB resident once frozen, and comes back in about 50 ms. Processes suspend and resume in place, so a long-lived agent keeps its shell state and its half-finished work across the freeze. Reproduce it on your own host before you plan capacity around it.

What the host needs before you install

The host is Ubuntu or Debian on x86_64, and the installer needs root. The daemon keeps root at runtime because it does loop mounts and writes cgroups.

Sandboxes run under Docker with gVisor (a container runtime that puts a userspace kernel between the container and the host kernel), which supplies the runsc runtime each sandbox uses. Node 22 or newer runs the daemon, and the installer brings its own copy, so your system Node is left alone.

Swap must exist, and vm.swappiness must be 100. This is not tuning advice, it is a functional requirement. Freezing works by pushing an idle sandbox's memory out to swap, gVisor holds sandbox memory as shared memory, and the kernel will not swap shared memory at the default swappiness. The project measured 0 bytes reclaimed at the default value and 99.5 percent reclaimed at 100. Check the value the kernel is really using, because some cloud images ship vm.swappiness = 0 in a file you will never think to read.

sysctl vm.swappiness
swapon --show

sysctl vm.swappiness should print vm.swappiness = 100, and swapon --show should list a swapfile. If swappiness prints 0, every freeze is a no-op that leaves you paying full memory for every idle sandbox.

Install Dormice on Ubuntu

The documented install is one pipe into bash:

curl -fsSL https://raw.githubusercontent.com/BitMiracle-AI/Dormice/main/deploy/install.sh | bash

Fetch it and read it before you run it. This script runs as root and rearranges your host: it installs Docker if it is missing, downloads gVisor and Caddy with checksum verification, creates a swapfile, writes systemd units, and adds firewall rules.

curl -fsSL https://raw.githubusercontent.com/BitMiracle-AI/Dormice/main/deploy/install.sh -o dormice-install.sh
less dormice-install.sh
sudo bash dormice-install.sh --swap-gb 8

--swap-gb sets the swapfile size and defaults to 16, which is a lot of disk to give up on a small VPS. --mirror cn switches the downloads to mirrors reachable from mainland China. Re-running the installer upgrades the code and repairs drift, and it never rotates your API token.

Code lands in /opt/dormice, configuration in /etc/dormice/env, sandbox data in /var/lib/dormice, and the dormice and dor commands in /usr/local/bin. The installer generates the API token during install and writes it to /etc/dormice/env with mode 600.

There is no tagged release to install against. As of 4 August 2026 the repository carries no git tags and no GitHub releases, so the installer clones main and you get whatever landed that morning. Pinning a version therefore means writing down the commit you actually installed.

git -C /opt/dormice rev-parse HEAD

Save that hash with your deploy notes. When an upgrade breaks something, that commit is your only route back, because there is no version number to ask for.

The installer ends by running dor doctor, a read-only host check that boots real gVisor containers to prove the runtime works instead of trusting a package list. Run it again whenever the daemon misbehaves.

sudo dor doctor
systemctl is-active dormice

systemctl is-active dormice should print active. If it prints failed, journalctl -u dormice -n 50 holds the reason, and a failed start is usually the swap or gVisor prerequisite rather than the daemon itself.

The installer also puts Caddy on the box, so look at what is listening before you decide the firewall work is done.

sudo ss -lntp

The daemon binds 127.0.0.1:3676 and has no setting to change that, by design. Reaching it from your laptop is a deliberate act, and the cheap version is an SSH tunnel.

ssh -L 3676:127.0.0.1:3676 root@your-server

With the tunnel open, http://127.0.0.1:3676/console on your laptop is the web console. Sign in with the token once and it becomes an httpOnly session cookie, so the token itself is never stored where the page can read it. The Connect page there prints copy-and-paste client snippets already pointed at your own endpoint.

Create a sandbox and execute code in it

One operation creates a sandbox: acquire. It is idempotent, so the same key always returns the same sandbox, creating it, waking it, starting it or restoring it as needed. Every other verb answers 404 for a key it has never seen. The dor CLI has no acquire verb, so your first sandbox comes from the console or from a client library.

The console route is fastest. Open /console through the tunnel and create a sandbox named my-agent. The CLI then works on it.

sudo grep DORMICE_API_TOKEN /etc/dormice/env
export DORMICE_ENDPOINT=http://127.0.0.1:3676
export DORMICE_API_TOKEN=paste-the-value-here
dor sandbox ls
dor sandbox exec my-agent 'python3 --version'

dor sandbox ls lists each sandbox with its lifecycle state, which is how you watch one drop from active to frozen. dor sandbox exec prints a Python 3.12 version, because the stock image is Ubuntu 24.04 with Python 3.12, Node 24, git and ripgrep already installed. An authentication error instead means the token line you copied included the variable name.

Files move with dor sandbox push my-agent ./script.py, which lands at /home/user/script.py, and dor sandbox pull my-agent notes.txt brings one back. The native file verbs cap at 16 MiB per file, while the E2B file surface streams and lets the sandbox disk quota be the only limit.

Destroying is the only verb that loses data, and it is also a fair example of the project's age: the main README and the bundled agent skill both document dor sandbox destroy <key>, while the CLI package README documents dor sandbox release <key>. Run dor sandbox --help on your own build and believe that instead.

Point your existing E2B code at your own box

This is the reason to care. The official e2b package from npm, unmodified, talks to Dormice. Run this from your laptop with the SSH tunnel open, so nothing new listens on the server.

npm init -y
npm i e2b tsx
import { Sandbox } from 'e2b';

const sbx = await Sandbox.create({
  apiKey: `e2b_${process.env.DORMICE_API_TOKEN}`,
  apiUrl: 'http://127.0.0.1:3676/e2b/api',
  sandboxUrl: 'http://127.0.0.1:3676/e2b/envd',
});

const result = await sbx.commands.run('python3 -c "print(6 * 7)"');
console.log(result.exitCode, result.stdout);

await sbx.kill();
DORMICE_API_TOKEN=paste-the-value-here npx tsx index.ts

A healthy run prints exit code 0 and 42. The API key is your Dormice token with an e2b_ prefix in front of it, which is the form the compatibility layer expects.

The compatibility is not a stub. Streaming stdout and stderr, background commands, an interactive PTY, signed upload and download URLs, directory watching and a port proxy are all exercised through the official package against a real Docker and gVisor daemon by the project's end-to-end suite. A few deltas matter before you migrate anything real:

  • Template builds are not implemented. A template is a docker image you build yourself and register with dor template add, and Sandbox.create('name') resolves it. An unregistered name returns a 404 rather than pretending.
  • Sandboxes created through the E2B surface get real deadlines, because E2B semantics require them. Deadlines are never imposed on sandboxes created through the native API.
  • A frozen sandbox keeps its processes and resumes them mid-flight, so pause and resume here is not the stop and cold start you may be used to.

What the sandbox stops, and what it does not

gVisor intercepts the container's system calls in userspace and services them itself, so sandboxed code is not talking straight to your host kernel. Inside the sandbox, everything runs as an unprivileged user, uid 1000. That combination handles the ordinary case: a generated script that runs rm -rf /, fills the disk, or forks until something dies damages its own sandbox and stops there.

Here is what it does not stop. Each of these is your job.

  • A sandbox has working outbound network. Generated code can download whatever it likes and post whatever it finds. The installer's network hardening covers two specific things: it drops container traffic to the cloud metadata service at 169.254.0.0/16, which is where a cloud hands instance credentials to anything that can reach it, and it turns off container-to-container traffic with "icc": false in Docker's daemon.json. Nothing else is blocked. Read sudo iptables -S DOCKER-USER and add your own DROP rules for the private ranges a sandbox has no business reaching.
  • Docker inserts its own rules ahead of your firewall, so a published container port can answer from the internet while ufw insists it is closed. Read how Docker publishes ports past ufw and the ufw firewall basics for a VPS before you expose anything on this host.
  • gVisor is a userspace kernel, not a hypervisor. That is a deliberate trade, because freezing requires sandboxes to be processes, and requiring KVM would stop the thing installing anywhere. If your threat model demands hardware virtualisation, use Firecracker-class isolation and accept the operational cost that comes with it.
  • The API token is the entire security boundary on the client side. Anything holding DORMICE_API_TOKEN can create, read and destroy every sandbox on the machine. Give the agent process its own least privilege user on the VPS and treat the token the way you treat an SSH key. The habits from running Claude Code safely on a VPS transfer directly.

The daemon itself runs as root on your host. gVisor protects the host from the code inside a sandbox, and nothing protects the host from the daemon or from whoever holds its token. So the machine running Dormice should be a machine that does only that. If your agent also reaches tools over MCP (model context protocol), keep those MCP servers on a separate VPS for the same reason.

How many sandboxes fit in 4 GB and 8 GB?

Two things eat memory: the host's own baseline, and the working set of every sandbox that is currently awake. Reserve about 1 GB for Ubuntu, Docker and the daemon, then divide what is left by what one of your sandboxes really uses. A sandbox running a Python script that reads a few files sits near 200 to 300 MiB. One running a compiler or a full test suite can pass a gibibyte.

ChartConcurrent sandboxes by host RAM, arithmetic after a 1 GB host reserve
The data behind this chart
[
  {
    "host": "4 GB VPS",
    "active_at_512_mib": 6,
    "active_at_1_gib": 3,
    "frozen_on_16gb_swap": 16
  },
  {
    "host": "8 GB VPS",
    "active_at_512_mib": 14,
    "active_at_1_gib": 7,
    "frozen_on_16gb_swap": 16
  }
]

A 4 GB VPS holds about 6 sandboxes awake at once if each uses 512 MiB, or 3 if each uses a full gibibyte. An 8 GB VPS takes that to 14 and 7. Those are ceilings for concurrent work, and they are arithmetic rather than a benchmark, so watch free -m while your own load runs.

Frozen sandboxes are limited by swap instead of RAM, which is the whole point of the design. A frozen sandbox that was holding a gibibyte keeps roughly that much in swap and almost nothing resident, so the installer's default 16 GB swapfile parks about 16 of them. Past that they need to reach the stopped rung, where they cost disk only. Disk is the real long-run limit here: every sandbox keeps its filesystem, and a few dozen agents each carrying a node_modules directory will fill a small volume long before memory becomes interesting.

Freeze, stop, archive: the lifecycle knobs

The defaults are freeze after 10 minutes idle, stop after 3 days, and archive after 7 days when archiving is configured. Setting stopAfterSeconds to null gives you a resident agent: it may freeze when idle, and it never cold starts.

Archiving is optional, and the daemon is honest about it. Set the four DORMICE_S3_* variables and a stopped sandbox's disk is packed with tar and zstd, shipped to any S3-compatible bucket, and freed locally. That bucket can be a MinIO bucket you host yourself on another machine of yours. Leave the variables unset and sandboxes park at stopped forever, and a policy asking to archive is refused rather than quietly ignored. Restores are visible instead of silent: the next acquire answers immediately with a restoring status and a progress value, then flips to ready once the disk is back.

Should you depend on it yet?

Straight answer: not for anything you cannot rebuild. The first commit in the repository is dated 8 July 2026. As of 4 August 2026 it shows 446 stars, 37 forks, an Apache-2.0 licence, and no tagged release at all. The README's own status line says nothing there is ready for production.

That combination has a specific shape of risk. The code moves under you, because the installer tracks main. The interface is still settling, which is exactly why the delete verb has two different names in two files in the same repository. And a project four weeks old can simply stop, since no licence clause obliges anyone to keep going.

What makes the risk survivable is the E2B compatibility. Your application talks to a protocol that has a hosted implementation behind it, so if Dormice stalls you change two URLs and keep working. Write your agent against the E2B surface rather than the native API and you keep that exit. The native @dormice/sdk package is not on npm yet either, so using it means building it from the repository, which is a second reason to start with the compatible path.

Run it where you can afford to lose it. Rebuild the host from a script, keep the token out of every prompt and every commit, and pull anything worth keeping out of the sandboxes on your own backup schedule.

FAQ

Is Dormice ready for production?

No, and the project says so itself. The README's status line states that nothing there is ready for production yet, and as of 4 August 2026 the repository is about four weeks old with no git tags and no releases, so there is no version number to pin. The installer clones the main branch, which means each run gives you the newest commit. Record git -C /opt/dormice rev-parse HEAD after every install, and keep anything valuable outside the sandboxes.

How is Dormice different from giving my agent a disposable VM?

A disposable VM is a machine with SSH that you create for a session and delete afterwards. Dormice is an execution API: your program calls acquire, then exec, and gets stdout and an exit code back, with no shell session in the middle. The VM suits a human or an agent that wants a whole computer for a while. Dormice suits an application that runs generated code many times a day and does not want a machine's worth of setup and teardown around each run.

Does the official E2B SDK really work without code changes?

Yes, with configuration changes. Point apiUrl and sandboxUrl at /e2b/api and /e2b/envd on your daemon, and pass your Dormice token with an e2b_ prefix as the API key. Command execution, PTY sessions, file transfer, signed URLs and the port proxy are all covered by the project's end-to-end suite running through the official package. Template building is the notable gap: e2b template build is not implemented, so a template is a docker image you build and register with dor template add.

How many sandboxes fit on a 4 GB VPS?

About 6 awake at the same time if each sandbox uses 512 MiB, or 3 if each uses a full gibibyte, after reserving roughly 1 GB for the operating system, Docker and the daemon. Frozen sandboxes are limited by swap instead, so the installer's default 16 GB swapfile parks around 16 sandboxes that each held a gibibyte. Measure your own with free -m under real load, because a sandbox running a test suite uses several times what one running a small script uses.

Why does Dormice need vm.swappiness set to 100?

Freezing a sandbox means pushing its idle memory out to swap. gVisor holds sandbox memory as shared memory, and the Linux kernel will not swap shared memory at the default swappiness, so at the default a freeze reclaims nothing and the sandbox keeps costing full memory. The project measured 0 bytes reclaimed at the default and 99.5 percent reclaimed at 100. Check the effective value with sysctl vm.swappiness rather than reading config files, because some cloud images ship a value of 0.