Run each Bun app in its own Linux microVM, on your machine

built from a Git commit with builds kept off the host behind a health check with data on the host disk with backups and restore with a CLI and a desktop app

defuss-container builds an HTTPS Git repository into a Bun distroless image inside a disposable builder VM and runs each application in a persistent smolvm microVM. A user-level daemon keeps applications, releases and jobs in SQLite; the CLI and the desktop app are clients of its loopback API.

Built by Aron Homberg (kyr0) (opens in a new tab) · Open Source (MIT (opens in a new tab))

Repository code runs only in VMs

On the host, Git only fetches and archives. Dependency installs and scripts.build run in a builder VM that cannot reach private networks, and the host checks every path, link, type and size of its output before packaging it.

One persistent VM per app

Each application owns a named machine with the CPUs, memory and disk its spec sets, on KVM (Linux), Hypervisor.framework (macOS) or Windows Hypervisor Platform. A deploy switches the code release; the VM and its data stay.

Deploys you can undo

A deploy waits for the app's health endpoint. A release that fails readiness is replaced by the previous one, which restarts if it was running, and rollback switches back on request. Rollback changes code, not database schemas.

Data that outlives releases

./var lives on the VM's own ext4 disk with full flush durability. Backups archive it while the workload is stopped; restore verifies the checksum and takes a safety backup first.

One daemon, two clients

The CLI and the desktop app call the same HTTP API. Every action returns a durable job at once; jobs outlive both clients, and a daemon restart marks unfinished jobs interrupted and brings each app back to its desired state.

Local by design

The API listens on 127.0.0.1:63705 only, rejects browser origins and rotates its 256-bit token on every start; the token never reaches JavaScript or a guest. defuss-container itself contacts only your Git remote, Docker Hub and, from the builder VM, the npm registry.

Fetch.

Build.

Run.

Desktop app

Every app, release and job at a glance

The desktop app shows each application with its desired state, release and storage, runs every action from build to delete, and keeps the recent jobs in view. Closing it leaves the daemon and the machines running.

The defuss container desktop app: the Applications dashboard with one running app named hello, its release, storage and the actions Build release, Stop, Rollback and Back up data
The dashboard with the example app deployed, captured by the end-to-end suite in Chromium against a live daemon.
Why

Your own services, each behind a hypervisor

defuss-container is a single-user tool for the Bun services you run on your own computer. One daemon owns all state and is the only process that runs Git or the VM engine; the CLI and the desktop app only talk to it.

Architecture One daemon, two clients, one VM per app Clients reach the daemon over its loopback API with a bearer token; the daemon passes the engine arguments only, never a shell command.
Host · your user account
Hypervisor
Client CLI defuss-container
Client Desktop app defuss UI · Rust bridge
127.0.0.1:63705 defuss-containerd jobs · releases · backups
SQLite apps · releases · jobs
Pinned engine smolvm 1.32.4 KVM · HVF · WHP
Git remote · Docker Hub HTTPS fetch · verified digests
Disposable Builder VM install · scripts.build
One per app App VM supervisor · bun exec start
  1. HTTP + token
  2. HTTP + token
  3. WAL
  4. argv
  5. HTTPS
  6. boot
  7. boot
  • Daemon and app VMs
  • Upstream services

A microVM per app

Each app runs under its own Linux kernel, so the boundary between apps and host is the hypervisor, and the host needs no Docker or Podman. It is no sandbox for hostile code: apps have outbound network access and reach host services through host.

One daemon, thin clients

Builds, deploys and backups run as jobs that outlive the window that started them. One API serves the CLI, the desktop app and the end-to-end suite alike; a library in each client would need its own locking and lifetime rules.

The engine CLI, not the SDK

The smolmachines SDK documents Linux and macOS only; the smolvm 1.32.4 CLI also covers Windows. Every flag the adapter passes was checked against the pinned macOS binary's help output.

Releases switch, machines stay

A deploy selects a release through a host-private launch file and a read-only share, then restarts the machine. ./var stays on its disk, so deploys and rollbacks keep the data; there is no zero-downtime deploy.

How it works

From a Git commit to a running app

The daemon fetches one commit, verifies the base image by digest and builds in a disposable VM. It checks and packages the result as an immutable release. A deploy starts that release in the app's own machine and waits for its health check.

Host · daemon
Guest · microVMs
01 · Fetch
02 · Build
03 · Package
04 · Run
HTTPS Git Pin one commit archive the tree · no hooks
Bun 1.4.4 distroless Verify base image manifest · config · layers
Disposable · private nets blocked Builder VM frozen install · scripts.build
Host checks Check and package paths · links · types · sizes
Release OCI image · checksum · SQLite
App VM supervisor · health check
  1. source
  2. image
  3. tar layer
  4. OCI
  5. deploy

Inside the app VM

The image holds the Bun base, the first release and a runtime layer with the supervisor. On every start the supervisor runs three steps:

  1. Verify

    Checks the release layer's checksum, extracts it into an empty staging tree and moves it into place.

  2. Link

    Links ./var, ./tmp, ./input, ./output and ./etc into the release, empties ./tmp and maps host to the gateway 100.96.0.1.

  3. Start

    Runs scripts.start with bun exec in its own process group. Stop runs scripts.stop, then SIGTERM, then SIGKILL after half the stop timeout.

A workload that exits powers the machine off, so a crashing release fails readiness at once instead of after the 60 s timeout.

Where an app's files live

PathBackingLifecycle
./varVM's ext4 diskKept across stop, start, deploy, rollback; backed up
./tmpInside the VMEmptied before every start
./inputHost share, read-onlyWritten by you on the host
./outputHost share, writableVisible on the host
./etcHost share, read-onlyConfiguration you provide
1.9 s"hello" from scratch: VM created, booted, healthy
0.4–0.6 s"hello" restart from stopped until healthy
25 MiBdaemon peak memory
11 MiBinstalled binaries
Measured

Measured, not promised

Measured by make e2e on macOS 26.7.1 with an Apple M5 Max and Hypervisor.framework, with the example app at 2 vCPUs, 1024 MiB and 20 GiB. Job durations come from the daemon's log of one run, memory and disk from the run in VERIFICATION.md. Linux and Windows are not measured yet, and neither is guest I/O (make bench).

9.5 sfirst build in a builder VM, empty daemon cache
0.7 sdeploy a new release until healthy
0.7 sroll back to the previous release
1.2 srestore a backup, safety backup included

Job durations

Each "hello" job of one end-to-end run, from start to end as the daemon logged it. Build v1 downloads the base image and the example's dependencies into an empty daemon cache; deploy v1 creates the VM from the release image.

Source: daemon job log of one make e2e run

Peak memory per process

Resident set size: from the kernel for the daemon and the CLI, from ps samples every second for VM processes and every 0.5 s for the desktop app, alone and with its 3 WebKit helpers. The app VM's guest used 86 of its 986 MiB; the image import helper is the engine's short-lived VM for a new release image.

Source: make e2e resource sampler; desktop app sampled by hand

Disk use

Allocated bytes; VM disks and engine templates are sparse files.

ItemAllocated
Installed binaries11 MiB
Desktop app bundle6.1 MiB
Engine bundle (30 GiB apparent)133 MiB
Verified base-image blobs58 MiB
Each release: app layer 4.5 MiB, OCI image 62 MiB66 MiB
App VM disk after the run169 MiB
Engine image seed per new machine imageabout 150 MiB
Backup of the example's ./varunder 0.1 MiB

Planning a host

Estimates from these runs, not measured limits.

  • Memory: budget each app's configured memory plus about 0.25 GiB, and about 0.7 GiB more for a moment while a new release image is imported.
  • Disk: one app with three releases came to about 0.7 GiB. A machine that never built before also needs the Bun base image seed, 136 MiB here, so plan about 0.85 GiB.
  • Retention: releases, backups and engine image seeds have no retention policy yet.
Portrait of Aron Homberg

Want help with your setup?

Aron Homberg, freelance and independent AI researcher, offers consulting services worldwide.

Use it

Run your first app

Start the daemon from a checkout and deploy an app with the CLI or the desktop app, or let your coding agent prepare an app for it.

From a checkout

Needs Rust through rustup, a C compiler, Git, Python 3.11 and a hypervisor your user may use. make start builds the binaries, installs the checksummed engine and runs the daemon in the background; make gui also needs Bun 1.4 and the Tauri prerequisites.

git clone https://github.com/kyr0/defuss-containercd defuss-containermake start                    # daemon on 127.0.0.1:63705make start-cli ARGS="doctor"  # engine and hypervisor ready?make gui && make start-gui    # the desktop app
{  "name": "hello",  "repository": "https://github.com/your-org/your-bun-app.git",  "git_ref": "main",  "mounts": "/absolute/path/to/hello-shares",  "ports": [{"host": 8080, "guest": 3000}],  "env": {"PORT": "3000"},  "health": {"port": 3000, "path": "/health"}}
defuss-container create app.jsondefuss-container build hello              # prints a job with its iddefuss-container wait JOB_ID              # result holds the release iddefuss-container deploy hello RELEASE_IDdefuss-container wait DEPLOY_JOB_IDcurl http://127.0.0.1:8080/health

The README lists every command and every app.json field.


Vibe coding

Paste this prompt into your coding agent inside your Bun app. It links the repository, so the agent works from the current application contract.

Make this Bun app deployable with defuss-container: https://github.com/kyr0/defuss-container
Read the Application contract in its README first. package.json needs nonempty scripts.start and scripts.stop and a committed bun.lock; the start script stays in the foreground and binds 0.0.0.0, and the process closes its databases on SIGTERM.
Keep persistent data under ./var and scratch files under ./tmp, read from ./input and ./etc, write results to ./output. Scripts run with bun exec in a distroless image, so no bash, curl or other system tools.
Add a /health endpoint, then write an app.json with ports, env and health for it.
send
HTTP API and daemon control

Script it

The CLI and the desktop app use nothing but this JSON API on 127.0.0.1. Every request carries the bearer token from connection.json, which rotates on each daemon start; requests with a browser Origin are rejected.

Call it, run it

Requests with curl, the development daemon's verbs and the installed login service. Actions are build, deploy with a release, start, stop, rollback, backup, restore with a backup, and delete with confirm set to the app's name. The login service has not been run on any platform yet.

conn=./var/home/connection.json    # the make start daemontoken=$(jq -r .token $conn)api=$(jq -r .address $conn)          # http://127.0.0.1:63705curl -s -H "Authorization: Bearer $token" $api/v1/appscurl -s -X POST -H "Authorization: Bearer $token" -H 'Content-Type: application/json' -d '{"action":"start"}' $api/v1/apps/hello/actionsdefuss-container api GET /v1/jobs    # the same through the CLI's client
make start                           # development daemon on ./var/homemake status                          # running, dead or stoppedmake log                             # last daemon log linesmake stop                            # SIGTERM; running jobs finish firstdefuss-containerd --home DIR --engine PATH --port 63705
python3 scripts/install.py --service  # registers and starts itsystemctl --user status defuss-container                    # Linuxlaunchctl print gui/$(id -u)/org.defuss.container           # macOSStart-ScheduledTask -TaskName 'Defuss Container'             # Windows

Routes

MethodRouteResult
GET/v1/healthSoftware and API version
GET/v1/doctorEngine version, provider, hypervisor checks
GET/v1/appsRegistered apps
POST/v1/appsRegister an app spec; 201
GET/v1/apps/{name}App record and observed engine state
GET/v1/apps/{name}/releasesSuccessful builds
GET/v1/apps/{name}/backupsBackups with checksums
GET/v1/apps/{name}/logsLast 200 workload log lines
POST/v1/apps/{name}/actionsQueue an action; 202 and a job
GET/v1/jobsMost recent 200 jobs
GET/v1/jobs/{id}One job record
GET/v1/jobs/{id}/logThe job's command log

Errors are {"error":{"message":"..."}} with 401, 403, 404, 409 or 400. docs/API.md has the full contract.

Your Bun services, each in its own microVM

Built from a Git commit in a disposable VM, deployed behind a health check, backed up and restored on your own machine.

MIT licensed · release 0.1.0, the first source release