boxer

Bring your Docker setup

Use docker or podman as the backend, build your own Dockerfile, reuse your devcontainer, and run services like Postgres and Redis with data that survives the sandbox.

boxer can run each worktree's sandbox as a docker or podman container, build your Dockerfile, and read your devcontainer. It does not run Compose.

Use docker as the backend

boxer.toml
backend = "docker"          # or "podman"

[network]
mode = "on"                 # docker cannot enforce an allowlist; "off" blocks the network

You do not need smolvm. Each worktree gets its own container instead of its own microVM. Compared with the default, you give up three things:

  • A kernel per sandbox. All containers share one kernel.
  • The egress allowlist. The network is either on or off.
  • Environment packs and forks. image_setup runs again in every new worktree, instead of once.

To make docker your default for every repository, put backend = "docker" and [network] mode = "on" in ~/.config/boxer/boxer.toml. Backends compares all four.

Build your Dockerfile

boxer.toml
build = "Dockerfile"        # relative to the worktree

boxer builds it on the host each time it creates a sandbox, then boots the result. The builder's layer cache makes an unchanged build take about a second, so editing a file the Dockerfile copies is picked up at the next boxer up --recreate.

BackendBuilt withBoots
docker, podmandocker build, podman buildthe tag boxer-<hash>
containercontainer buildthe tag boxer-<hash>
smolvmyour host's docker build, then docker savethe saved archive

On smolvm you need docker on the host to build, even though the sandbox is a microVM. The build context is the Dockerfile's directory. Set build_context to change it. Build arguments are not passed.

Use an image you already build

Point image at it.

BackendYour image
docker, podmandocker build -t myapp-dev ., then image = "myapp-dev". A local tag works.
smolvmdocker save myapp-dev -o ~/images/myapp-dev.tar, then image = "~/images/myapp-dev.tar". Use a path outside the repository, because a new worktree does not contain an uncommitted file.
anya published image, such as image = "ghcr.io/acme/dev:1.4"

When you rebuild the image, run boxer up --recreate in each worktree to pick it up. boxer never runs the image's own ENTRYPOINT or CMD. Put what should run in start.

Private registries. With docker or podman, the daemon pulls the image, so docker login is enough. On smolvm, save the image to an archive as above.

Use your devcontainer

If the repository has .devcontainer/devcontainer.json, boxer reads it. image becomes image, and build.dockerfile (or dockerFile) becomes build. It maps onCreateCommand to image_setup, postCreateCommand to setup and postStartCommand to start, and reads ports, environment, mounts and remoteUser.

It ignores these, with a warning that names them:

devcontainer keyDo this instead
dockerComposeFilerun the services in the sandbox (next section)
featuresinstall them in image_setup, or in your Dockerfile
build.args, build.targetput the values in the Dockerfile

The configuration reference has the full mapping.

Run Postgres, Redis and other services

boxer runs one sandbox per worktree, not a Compose stack. Run your services inside that sandbox. Every worktree then gets its own database on the same port, with no clashes. Clients have to run there too: psql or pg_dump typed on your machine reach your machine's own port 5432, not the sandbox's, unless you add them to intercept_also or run them with boxer run.

boxer.toml
image = "mirror.gcr.io/library/node:24-alpine"
image_setup = ["apk add --no-cache postgresql16 postgresql16-client redis"]
setup = ["npm ci"]
volumes = ["pgdata:/var/lib/postgresql/data"]
start = [
  "redis-server --save '' --appendonly no",
  "PGDATA=/var/lib/postgresql/data; chown postgres:postgres $PGDATA && chmod 700 $PGDATA && { [ -f $PGDATA/PG_VERSION ] || su postgres -c \"initdb -D $PGDATA --auth=trust\"; } && exec su postgres -c \"postgres -D $PGDATA -k /tmp -h 0.0.0.0\"",
  "npx next dev --webpack -H 0.0.0.0 -p 3000",
]
ready = "pg_isready -h 127.0.0.1 -U postgres && redis-cli ping"

[network]
allow_presets = ["alpine", "npm"]
ports = ["auto:3000"]

[env]
DATABASE_URL = "postgres://postgres@127.0.0.1:5432/postgres"
REDIS_URL = "redis://127.0.0.1:6379"
  • image_setup installs the services once. On smolvm, the result is cached, so the next worktree boots in about 2 seconds instead of reinstalling.
  • volumes keeps the database's files on your machine, one directory per worktree. They survive boxer rm, --recreate and idle reclaim. They are deleted a week after the worktree is gone, or by boxer rm --volumes.
  • On a Debian image (apt-get install postgresql), initdb and postgres are not on PATH. Use their full path, /usr/lib/postgresql/<version>/bin/.
  • start runs them every time the sandbox starts. The Postgres line creates the database the first time, when the volume is empty. It changes the volume's owner first, because Postgres refuses a data directory it does not own. The owner changes only inside the sandbox: on your machine the directory stays yours. Put the socket in /tmp, because /run is empty at boot. -h 0.0.0.0 lets a forwarded port reach it, if you add auto:5432 to ports and a host all all all trust line to pg_hba.conf.
  • A service that crashes is restarted, and five quick crashes in a row stop the retries. Each restart is a line in /tmp/boxer-start.log. boxer restart relaunches them by hand. See restart.
  • apt-get or apk in image_setup needs the distribution's mirror. The alpine and debian presets name them.
  • ready waits for both before the agent's first command.
  • setup runs before start, so seed the database from a task that the agent runs once the sandbox is ready.

On docker or podman, drop allow_presets and set network.mode = "on". With your own Dockerfile, install Postgres and Redis there instead of in image_setup, and keep the start and ready lines.

Environment variables and secrets

boxer.toml
env_passthrough = ["CI"]            # copied from your shell by name at run time
secrets = ["API_KEY"]               # the same, and marks them as secrets

[env]
NODE_ENV = "development"            # fixed values; saved in the environment snapshot, so no secrets

Values from env_passthrough and secrets never appear in a command line, a snapshot or a log.

Environment covers secrets and mounts.

Monorepos

Put one boxer.toml at the repository root. setup installs the whole workspace, and start can run several dev servers, each with its own port. Examples has a monorepo configuration.

On this page