29 KiB
smolvm — Agent Reference
A tool to build and run portable, self-contained virtual machines locally. <200ms boot time. No daemon, no Docker.
Platform Support
| Host | Guest | Hypervisor | Requirements |
|---|---|---|---|
| macOS Apple Silicon | arm64 Linux | Hypervisor.framework | macOS 11+ |
| Linux x86_64 / aarch64 | matching Linux | KVM | /dev/kvm |
| Windows x86_64 | x86_64 Linux | Windows Hypervisor Platform (WHP) | WHP feature enabled |
Windows caveats (run, persistent machines, volumes, port-forwarding, pack create/run, and interactive TTY all work): networking is TSI-only (TCP/UDP + inbound -p, no virtio-net); no GPU acceleration; no fork/snapshot. pack create needs storage-template.ext4 / overlay-template.ext4 beside smolvm.exe (Windows has no host mkfs.ext4). Set SMOLVM_LIB_DIR (folder holding krun.dll + libkrunfw.dll) and SMOLVM_AGENT_ROOTFS when running from a non-standard layout.
Quick Reference
# Ephemeral (cleaned up after exit)
smolvm machine run --net --image alpine -- echo hello
smolvm machine run --net -it --image alpine -- /bin/sh # interactive shell
smolvm machine run --net --image python:3.12-alpine -- python3 script.py
# Persistent (survives across exec sessions and stop/start)
smolvm machine create --net --name myvm
smolvm machine start --name myvm
smolvm machine exec --name myvm -- apk add python3 # installs persist
smolvm machine exec --name myvm -- which python3 # still there
smolvm machine shell --name myvm # interactive shell (auto-starts if stopped)
smolvm machine stop --name myvm
smolvm machine delete --name myvm
# Image-based persistent (filesystem changes persist across exec sessions)
smolvm machine create --net --image ubuntu --name myvm
smolvm machine start --name myvm
smolvm machine exec --name myvm -- apt-get update
smolvm machine exec --name myvm -- apt-get install -y python3
smolvm machine exec --name myvm -- which python3 # still there after exit+re-exec
# SSH agent forwarding (git/ssh without exposing keys) — see "SSH Agent Forwarding" below
smolvm machine run --ssh-agent --net --image alpine -- sh -c "apk add -q openssh-client && ssh-add -l"
smolvm machine create --name myvm --image alpine --ssh-agent --net # persistent: then start + exec git/ssh
# Inject secrets into workload env (referenced from host env var / file)
smolvm machine run --secret-env OPENAI_API_KEY=OPENAI_API_KEY -- ./app
smolvm machine run -s Smolfile -- ./app # Smolfile [secrets] resolves at launch
# Pack into portable executable
smolvm pack create --image python:3.12-alpine -o ./my-python
./my-python run -- python3 -c "print('hello')"
# Create machine from packed artifact (fast start, no pull)
smolvm machine create --name my-vm --from ./my-python.smolmachine
smolvm machine start --name my-vm
smolvm machine exec --name my-vm -- pip install requests
# Use local container images (CI, air-gapped, fast iteration)
docker save myapp:latest -o myapp.tar
smolvm machine run --image ./myapp.tar -- ./app # from a docker/podman save archive
docker save myapp:latest | smolvm machine run --image - -- ./app # from stdin
smolvm machine run --image ./rootfs/ -- ./app # from an unpacked rootfs dir
smolvm machine create --name myvm --image ./myapp.tar # persistent, from a local archive
When to Use What
| Goal | Command |
|---|---|
| Run a one-off command in isolation | smolvm machine run --net --image IMAGE -- CMD |
| Interactive shell (ephemeral) | smolvm machine run --net -it --image IMAGE -- /bin/sh |
| Interactive shell (persistent) | smolvm machine shell --name NAME |
| Persistent dev environment | machine create → machine start → machine exec |
| Ship software as a binary | smolvm pack create --image IMAGE -o OUTPUT |
| Fast persistent machine from packed artifact | machine create --name NAME --from FILE.smolmachine |
| Use local container images (CI / air-gapped / fast iteration) | --image ./archive.tar, --image - (stdin), or --image ./rootfs/ |
| Use git/ssh with private keys safely | Add --ssh-agent to run or create |
| Inject API keys / tokens without putting them on the command line | --secret-env/--secret-file flags or Smolfile [secrets] |
| Minimal VM without image | smolvm machine run -s Smolfile (bare VM) |
| Change mounts/ports/resources on existing VM | machine update --name NAME -v ./src:/app -p 8080:8080 |
| Declarative VM config | Create a Smolfile, use --smolfile/-s flag |
Persistence Model
machine run— ephemeral. All changes are discarded when the command exits.machine exec— persistent. Filesystem changes (package installs, config edits) persist across exec sessions for the same machine, whether bare or image-based. Changes are stored in an overlay on the machine's storage disk.machine stop+start— changes persist across restarts. The persistent overlay is remounted preserving previous changes.pack run— ephemeral. Each run starts fresh from the packed image.pack start+exec— daemon mode./workspacepersists across exec sessions and stop/start. Container overlay resets per exec (package installs don't persist — use/workspacefor durable data).machine create --from .smolmachine— creates a persistent named machine from a packed artifact. Boots from pre-extracted layers (~250ms, no image pull). Fullmachine execpersistence — package installs, file writes all survive across exec and stop/start.
CLI Structure
All commands use named flags (no positional args except machine create --name NAME and machine delete --name NAME).
smolvm machine run --image IMAGE [-- COMMAND] # ephemeral
smolvm machine exec --name NAME [-- COMMAND] # run in existing VM
smolvm machine shell [--name NAME] # interactive shell (auto-starts)
smolvm machine create --name NAME [OPTIONS] # create persistent
smolvm machine create --name NAME --from FILE.smolmachine # from packed artifact
smolvm machine start [--name NAME] # start (default: "default")
smolvm machine stop [--name NAME] # stop
smolvm machine delete --name NAME [-f] # delete
smolvm machine status [--name NAME] # check state
smolvm machine ls [--json] # list all
smolvm machine update --name NAME [OPTIONS] # modify stopped machine settings
smolvm machine cp SRC DST # copy files (host↔VM)
smolvm machine exec --stream --name NAME -- CMD # streaming output
smolvm machine monitor [--name NAME] # foreground health + restart
smolvm pack create --image IMAGE -o PATH # package
smolvm pack create --from-vm NAME -o PATH # pack from VM snapshot
smolvm pack run [--sidecar PATH] [-- CMD] # run .smolmachine
smolvm serve start [--listen ADDR:PORT|PATH] # HTTP API
smolvm config registries edit # registry auth
# Secrets are references to host env vars / files, resolved at launch — no
# built-in store. Attach them on the command line or in a Smolfile [secrets].
smolvm machine run --secret-env GUEST_VAR=HOST_VAR # from host env var
smolvm machine run --secret-file GUEST_VAR=/abs/path # from host file
smolvm machine create --name NAME --secret-env GUEST_VAR=HOST_VAR # persists the ref
smolvm machine exec --name NAME --secret-env GUEST_VAR=HOST_VAR -- cmd
Artifact References
Artifact references follow OCI conventions and support both tags and digests:
python-dev # bare name (default registry + latest)
python-dev:v1.0 # name + tag
binsquare/custom:v1 # namespace + name + tag
smolmachines.com/python-dev:latest # registry + name + tag
smolmachines.com/binsquare/custom:v1 # registry + namespace + name + tag
python-dev@sha256:abcdef0123... # digest reference (immutable)
Default registry: registry.smolmachines.com. Digest references require sha256: followed by exactly 64 hex characters.
Local container images
--image also accepts a local source — useful for CI, air-gapped hosts, and fast
local iteration. smolvm stays a microVM runtime and delegates all image work
(flatten, whiteouts, config) to container tooling (crane/docker/podman); the
archive is flattened with crane export.
./image.tar ./image.tar.gz ./image.tgz # a `docker save` / `podman save` archive (gzip ok)
- # the same archive streamed on stdin
./rootfs/ # an already-unpacked root filesystem directory
A source is treated as local when it starts with /, ./, ../, is -, or ends in
.tar/.tar.gz/.tgz; everything else is a registry reference (so bare alpine
still pulls). Archives are cached content-addressed by hash and re-resolved on
machine start. --image - cannot be combined with -i/-t (both read stdin).
smolvm boots images, it does not build them: a Dockerfile passed to --image is
rejected with a hint to build first (docker build … && docker save … | … --image -).
Key Flags
| Flag | Short | Used on | Description |
|---|---|---|---|
--image |
-I |
run, create, pack create | OCI image, or a local source: a docker save archive (./img.tar, or - for stdin) or unpacked rootfs dir (./rootfs/) |
--name |
-n |
run, start, stop, status, exec, update | Machine name (default: "default") |
--net |
run, create | Enable outbound networking (off by default) | |
--gpu |
run, create | Enable GPU acceleration (Vulkan via virtio-gpu) | |
--gpu-vram |
run, create | GPU shared-memory region size in MiB (default: 4096). Ignored without --gpu. |
|
--volume |
-v |
run, create, update | Mount host dir: HOST:GUEST[:ro] |
--port |
-p |
run, create, update | Port mapping: HOST:GUEST |
--smolfile |
-s |
run, create, pack create | Load config from Smolfile |
--interactive |
-i |
run, exec | Keep stdin open |
--tty |
-t |
run, exec | Allocate pseudo-TTY |
--allow-cidr |
run, create | CIDR egress filter (implies --net) | |
--allow-host |
run, create | Hostname egress filter, resolved at VM start (implies --net) | |
--ssh-agent |
run, create | Forward host SSH agent (git/ssh without exposing keys) |
Smolfile Reference
A Smolfile is a TOML file declaring a VM workload. Use with --smolfile/-s.
# Top-level: workload definition
image = "python:3.12-alpine" # OCI image (omit for bare Alpine)
entrypoint = ["/app/run"] # overrides image ENTRYPOINT
cmd = ["serve"] # overrides image CMD
env = ["PORT=8080", "DEBUG=1"] # environment variables
workdir = "/app" # working directory
# Resources
cpus = 2 # vCPUs (default: 4)
memory = 1024 # MiB (default: 8192, elastic via balloon)
net = true # outbound networking (default: false)
gpu = true # GPU acceleration (default: false)
gpu_vram = 4096 # GPU VRAM MiB (default: 4096, ignored unless gpu=true)
storage = 40 # storage disk GiB (default: 20)
overlay = 4 # overlay disk GiB (default: 2)
# Network policy — egress filtering by hostname and/or CIDR
[network]
allow_hosts = ["api.stripe.com"] # resolved at VM start (implies net)
allow_cidrs = ["10.0.0.0/8"] # IP/CIDR ranges (implies net)
# Dev profile (used by `machine run` and `machine create`)
[dev]
volumes = ["./src:/app"] # host bind mounts
ports = ["8080:8080"] # port forwarding
init = ["pip install -r requirements.txt"] # run on every VM start
env = ["APP_MODE=dev"] # dev-only env (extends top-level)
workdir = "/app" # dev-only workdir
# Artifact profile (used by `pack create`)
[artifact]
cpus = 4 # override resources for distribution
memory = 2048
entrypoint = ["/app/run"] # override entrypoint for packed binary
oci_platform = "linux/amd64" # target OCI platform
# Health check (used by `machine monitor`)
[health]
exec = ["curl", "-f", "http://127.0.0.1:8080/health"]
interval = "10s"
timeout = "2s"
retries = 3
startup_grace = "20s"
# Credential forwarding
[auth]
ssh_agent = true # forward host SSH agent into the VM
# Secrets — references to host sources, resolved at workload launch
[secrets]
DATABASE_URL = { from_env = "PROD_DB_URL" } # host env var (at launch)
GCP_CREDS = { from_file = "/abs/creds.json" } # host file (at launch)
Merge Precedence
CLI flags override Smolfile values:
image: --image flag > Smolfile image > None (bare Alpine)
entrypoint: Smolfile entrypoint > image metadata
cmd: trailing args (after --) > Smolfile cmd > image metadata
env: top-level env + [dev].env + CLI -e (all merged)
volumes: [dev].volumes + CLI -v (all merged)
ports: [dev].ports + CLI -p (all merged)
init: [dev].init + CLI --init (all merged)
cpus/mem: CLI flag > Smolfile > defaults (4 CPU, 8192 MiB)
Networking
- Off by default — VMs have no outbound access unless
--netis specified --netenables full outbound (TCP/UDP, DNS)--allow-host api.stripe.comenables egress only to resolved IPs of that hostname (implies--net). Also enables DNS filtering — only allowed hostnames can be resolved.--allow-cidr 10.0.0.0/8enables egress only to specified IP ranges (implies--net)--allow-hostand--allow-cidrcan be combined and used multiple times--outbound-localhost-onlyrestricts to 127.0.0.0/8 and ::1 (implies--net)-p HOST:GUESTforwards a host port to the VM (TCP)- Smolfile: use
[network] allow_hostsand[network] allow_cidrs
Proxy Support
Pass proxy settings into VMs with -e when behind a corporate proxy or VPN:
smolvm machine run --net \
-e https_proxy=http://proxy.corp:3128 \
-e http_proxy=http://proxy.corp:3128 \
-e no_proxy=localhost,127.0.0.1 \
--image alpine -- wget -q -O /dev/null https://example.com
Or declare them in a Smolfile:
net = true
env = [
"https_proxy=http://proxy.corp:3128",
"http_proxy=http://proxy.corp:3128",
"no_proxy=localhost,127.0.0.1"
]
Proxy vars are NOT forwarded automatically — each VM gets exactly the env you specify. The VM uses the host's DNS server (from /etc/resolv.conf) for name resolution.
SSH Agent Forwarding
Forward the host's SSH agent into the VM so git, ssh, and scp work with your keys — without the private keys ever entering the VM.
# Quick check the agent is forwarded (alpine needs openssh-client for ssh-add):
smolvm machine run --ssh-agent --net --image alpine -- sh -c "apk add -q openssh-client && ssh-add -l"
# Persistent VM — create, start, install tooling, then use the agent:
smolvm machine create --name myvm --image alpine --ssh-agent --net
smolvm machine start --name myvm
smolvm machine exec --name myvm -- apk add -q git openssh-client
# Smolfile
# [auth]
# ssh_agent = true
Inside the VM, SSH_AUTH_SOCK is set automatically. Any tool that uses the SSH agent protocol (git, ssh, scp) works transparently:
smolvm machine exec --name myvm -- git clone git@github.com:org/private-repo.git
smolvm machine exec --name myvm -- ssh deploy@server "systemctl restart app"
The host SSH agent signs challenges but never sends private keys across the boundary. Even with root inside the VM, keys cannot be extracted — this is enforced by the SSH agent protocol and the hypervisor isolation.
Requires SSH_AUTH_SOCK to be set on the host. If missing, smolvm exits with an error and remediation instructions.
GPU Acceleration
Enable the host GPU inside a VM with --gpu. Guest Vulkan talks to the host GPU via virtio-gpu/Venus; ANGLE uses it as the WebGL/OpenGL ES backend.
Not available on Windows (WHP) — --gpu has no effect there.
Host setup:
- macOS — bundled, no extra installs needed.
- Linux — install virglrenderer from the system package manager before use:
- Alpine:
apk add virglrenderer mesa-vulkan-intel(ormesa-vulkan-atifor AMD) - Debian/Ubuntu:
apt install virglrenderer0 mesa-vulkan-drivers
- Alpine:
# One-shot GPU workload
smolvm machine run --gpu --image alpine -- sh -c '
apk add --no-cache mesa-vulkan-virtio vulkan-tools
VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/virtio_icd.x86_64.json \
vulkaninfo --summary 2>/dev/null | grep deviceName
'
# → deviceName = Virtio-GPU Venus (Intel(R) UHD Graphics ...)
# Persistent GPU machine
smolvm machine create --name browser --gpu --gpu-vram 2048
smolvm machine start --name browser
smolvm machine exec --name browser -- \
chromium --headless=new --no-sandbox --use-gl=angle --use-angle=vulkan \
--screenshot=/tmp/out.png --window-size=1280,800 https://example.com
The guest must set VK_ICD_FILENAMES so the Vulkan loader finds the virtio ICD. Put it in env in a Smolfile to avoid repeating it on every exec:
gpu = true
gpu_vram = 2048
env = ["VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/virtio_icd.x86_64.json"]
For a complete working example see examples/headless-browser/browser.smolfile.
CUDA (experimental)
--cuda remotes guest CUDA Driver-API calls to the host NVIDIA GPU over
vsock — separate from --gpu (Vulkan graphics). The host needs a working
NVIDIA driver (libcuda.so.1 + loaded kernel module); no CUDA toolkit is
required on host or guest. Enable with --cuda on machine run/create, or
cuda = true in a Smolfile.
Guest programs reach the GPU through the drop-in driver library built from
crates/smolvm-cuda-shim (a cdylib with soname
libcuda.so.1). Mount or install it in the guest and unmodified Driver-API
programs work with no code changes:
cargo build --release -p smolvm-cuda-shim # → target/release/libcuda.so
mkdir -p /tmp/shim && cp target/release/libcuda.so /tmp/shim/libcuda.so.1
smolvm machine run --net --cuda -v /tmp/shim:/opt/shim:ro --image debian:bookworm-slim -- \
sh -c 'gcc myapp.c -o myapp -L/opt/shim -l:libcuda.so.1 && LD_LIBRARY_PATH=/opt/shim ./myapp'
Covered surface: init/device queries (name, attributes, uuid, total/free mem),
contexts (incl. the primary-context flow the CUDA runtime uses), module
load/unload (PTX, cubin, fatbin), mem alloc/free/HtoD/DtoH/DtoD/memset,
kernel launch (param sizes come from the host via cuFuncGetParamInfo,
CUDA 12.4+ drivers), streams, events, cuGetProcAddress. All work executes
synchronously host-side; *Async calls complete before returning (permitted
by the CUDA contract).
This covers programs written against the Driver API (the cu* C API), not
the Runtime API. A program built with nvcc (or PyTorch, RAPIDS, etc.) links
NVIDIA's libcudart, which bootstraps by requesting a private internal
driver interface via cuGetExportTable (a versioned UUID whose function ABIs
are undocumented). A pure libcuda shim cannot provide it, so libcudart
aborts during context init. Hosting Runtime-API workloads therefore requires
remoting at the libcudart level instead — a separate, larger effort (see
docs/cuda-support-plan.md, Phase 4). Set SMOLVM_CUDA_SHIM_TRACE=1 to log
which driver entry points a program resolves through the shim.
Without an NVIDIA driver the host serves a CPU-emulation backend (test-only:
it knows the vecadd test kernel), so the transport stays testable anywhere.
Secrets
smolvm stores no secret material. A secret is a reference to a value that
already lives on the host — a host environment variable or a host file — and is
resolved into the workload's process environment at launch time. Bring your own
secrets manager (Vault, 1Password, AWS, sops, your shell): render the value into
an env var or file, then point a ref at it. Only the reference is ever
persisted; the resolved value never lands in the VM record, the database, or a
.smolmachine pack.
Attach refs on the command line:
# From a host environment variable (GUEST_VAR=HOST_VAR)
smolvm machine run --secret-env OPENAI_API_KEY=OPENAI_API_KEY -- ./app
smolvm machine create --name web --secret-env DATABASE_URL=PROD_DB_URL # persists the ref
smolvm machine exec --name web --secret-env TOKEN=CI_TOKEN -- ./deploy
# From a host file (GUEST_VAR=/absolute/path)
smolvm machine run --secret-file GCP_CREDS=/abs/creds.json -- ./app
# Bridge any external manager through the env/file seam, e.g. 1Password:
op run --env-file=secrets.env -- smolvm machine run -- ./app
Or reference them from a Smolfile. The left-hand key becomes the env var name in the guest workload:
[secrets]
DATABASE_URL = { from_env = "PROD_DB_URL" } # host env var (at launch)
GCP_CREDS = { from_file = "/abs/creds.json" } # absolute host file (at launch)
Exactly one of from_env, from_file must be set per entry; from_file paths
must be absolute. Resolved values are appended after top-level env and CLI
-e flags. Resolution is late-bound, so rotating the underlying env var or file
takes effect at the next launch with nothing to re-sync.
Threat model: this is defense-in-depth, not zero-knowledge. The target
process sees plaintext in its own environment, and root inside the guest can
read any /proc/*/environ. Use SSH agent forwarding instead when a secret must
never leave the host.
Where they're resolved: machine run, machine create + machine start,
and machine exec resolve refs against this host under a trusted-local scope.
Untrusted surfaces — HTTP API request bodies and portable .smolmachine packs —
are treated as untrusted callers and may carry no resolvable secret ref:
from_env would expose the server's env and from_file would be an arbitrary
host-file read, so both are rejected. Configure secrets locally instead.
File Copy
Copy files between the host and a running machine using machine:path syntax:
# Upload a file to the VM
smolvm machine cp ./script.py myvm:/workspace/script.py
# Download a file from the VM
smolvm machine cp myvm:/workspace/output.json ./output.json
Image-based VMs (--image): Files copied with cp are visible to
exec at the same path, and vice versa. This works for any path —
/tmp, /home, /workspace, etc. Under the hood, cp routes
through the container's overlay filesystem so both commands see the
same files.
/workspace shared directory: Every machine has a /workspace
directory — bare VMs, image-based VMs, and machines created from
.smolmachine artifacts. It persists across exec sessions and
across stop/start cycles. It's a good default location for
scripts, data, and results. Passing -v /host/dir:/workspace replaces
the default storage-disk workspace with your host directory for that
run — the host mount takes priority and the storage workspace is skipped:
# Typical agent workflow: copy code in, execute, extract results
smolvm machine create --name r-sandbox --image r-base:latest --net
smolvm machine start --name r-sandbox
smolvm machine cp analysis.R r-sandbox:/workspace/analysis.R
smolvm machine exec --name r-sandbox -- Rscript /workspace/analysis.R
smolvm machine cp r-sandbox:/workspace/results.csv ./results.csv
smolvm machine stop --name r-sandbox
Behavior and limits:
- Files up to 1 MiB transfer as a single message — no perceptible overhead beyond the agent round-trip.
- Larger files stream automatically: 1 MiB chunks for upload, 16 MiB chunks for download. The split is asymmetric because the host→guest direction has tighter socket-buffer headroom.
- Per-transfer cap is 4 GiB in either direction. Files at or
above this size are rejected up front (
total_size exceeds maximumon upload;exceeding the byte capon download). For larger blobs, mount a host directory with--volumeinstead of copying. - A throttled progress line prints to stderr while large transfers
run, including bytes-so-far, percentage (uploads), and rate.
Pipe captures (
> file) only see the upload/download summary, not the progress noise. - Atomic on the guest side: a partially-written file never appears at the target path. If the transfer fails or the connection drops mid-stream, the staging file is cleaned up and the original destination (if any) is unaffected.
Typical throughput on macOS (Apple Silicon): ~35-42 MB/s upload, ~170 MB/s download.
Streaming Exec
Stream command output in real-time instead of buffering:
# CLI — prints output as it arrives
smolvm machine exec --stream --name myvm -- python3 train.py
# API — Server-Sent Events
POST /api/v1/machines/:name/exec/stream
Content-Type: application/json
{"command": ["python3", "train.py"]}
# Response: text/event-stream
# event: stdout
# data: Epoch 1/10...
# event: exit
# data: {"exitCode":0}
Bare VM Mode
machine run works without --image when a Smolfile provides the workload config, or for direct Alpine shell access:
# Bare Alpine shell
smolvm machine run -it
# Smolfile with entrypoint/cmd (no container overhead)
smolvm machine run -s Smolfile
# Bare VM with init setup, detached
smolvm machine run -d -s Smolfile
Bare VMs run commands directly in the Alpine rootfs — no OCI image pull needed. Use this when you need a minimal Linux environment.
Packed Binaries (.smolmachine)
smolvm pack create produces two files:
my-app— stub binary with embedded VM runtime (platform-specific)my-app.smolmachine— VM payload: rootfs, OCI layers, storage (cross-platform)
The packed binary runs as a normal executable:
./my-app run -- python3 -c "print('hello')" # ephemeral, cleaned up after exit
./my-app start # persistent daemon mode
./my-app exec -- pip install x # exec into daemon
./my-app stop # stop daemon
Alternatively, create a named machine from the .smolmachine for full lifecycle management:
smolvm machine create --name my-vm --from my-app.smolmachine
smolvm machine start --name my-vm # ~250ms boot, no image pull
smolvm machine exec --name my-vm -- pip install x # fully persistent
smolvm machine stop --name my-vm
smolvm machine ls # shows my-vm
The .smolmachine manifest includes registry-oriented metadata:
host_platform— host OS+arch this machine runs on (e.g.,darwin/arm64), distinct fromplatformwhich is the guestcreated— RFC 3339 timestamp of when the machine was packedsmolvm_version— version of smolvm that built it
HTTP API
Start with smolvm serve start --listen 127.0.0.1:8080 or smolvm serve start --listen $XDG_RUNTIME_DIR/smolvm.sock. Key endpoints:
POST /api/v1/machines Create machine
GET /api/v1/machines List machines
GET /api/v1/machines/:name Get machine
POST /api/v1/machines/:name/start Start machine
POST /api/v1/machines/:name/stop Stop machine
DELETE /api/v1/machines/:name Delete machine
POST /api/v1/machines/:name/exec Execute command
POST /api/v1/machines/:name/exec/stream Streaming exec (SSE)
PUT /api/v1/machines/:name/files/*path Upload file
GET /api/v1/machines/:name/files/*path Download file
GET /api/v1/machines/:name/logs Stream logs (SSE)
POST /api/v1/machines/:name/images/pull Pull OCI image
OpenAPI spec: smolvm serve openapi
Important Defaults
- Machine name defaults to
"default"when--nameis omitted - Network is off by default (security-first)
- CPUs: 4, Memory: 8192 MiB, Storage: 20 GiB, Overlay: 2 GiB
- Packed binaries use the same defaults (CPUs: 4, Memory: 8192 MiB)
- Memory and CPU are elastic via virtio balloon — the host only commits what the guest actually uses and reclaims the rest
Important Behaviors
- Observational commands don't stop running VMs.
machine images,machine status,machine lsand similar read-only commands leave a running VM in its current state. If the VM was already running before the command, it stays running after. machine pruneworks on a running VM. Regular prune only removes unreferenced layers and is safe while containers are active.prune --allrequires the VM to be stopped first since it deletes manifests for layers that may be in use.machine execpersists filesystem changes. Package installs, config edits, and file writes insideexecsurvive across sessions. This works for both bare VMs and image-based VMs (created with--image).machine updatemodifies a stopped machine. Add/remove mounts, ports, env vars, or change CPU/memory without recreating the VM. Changes take effect on nextmachine start. Requires the machine to be stopped.machine runis always ephemeral. The VM is created, the command runs, and everything is cleaned up. No state carries over.-v host:/workspacereplaces the default workspace. Every image-based VM exposes/workspacebacked by the VM's storage disk. Mounting a host directory at/workspacetakes priority — the host share is used instead and the storage-disk workspace is not mounted. Any other target path (e.g./data,/app) does not affect/workspace.