Keep your friends close, your supply chain in a VM.
DVM is a tiny Bash wrapper around Lima. It creates one Fedora VM per project, runs the small setup baseline in every VM, lets each project opt into plain shell recipes, and keeps dev tools, AI CLIs, service credentials, and project code inside the VM.
The core rule:
DVM renders one Lima VM, starts it, sources one VM config on the host, and runs shell recipes inside the guest.
Requirements:
- macOS with Lima 2.0.0 or newer installed:
brew install lima - Bash 3.2+; the macOS system Bash works
The bundled Lima template uses vmType: vz, so Linux hosts are not supported by the
default template. Linux may work with a custom QEMU Lima template, but it is not tested.
Install the wrapper:
./install.sh --initThis installs a small launcher into ~/.local/bin and copies defaults into
~/.config/dvm without overwriting existing files. The launcher runs each invocation
from a temporary snapshot of bin/dvm and its shell libraries, so editing or pulling
this repo cannot corrupt a long-running dvm sync. Bundled recipes, the Lima
template, and example VM configs stay in the repo under share/dvm.
For zsh completion, add the in-repo completion directory before compinit in
~/.zshrc:
fpath=(/path/to/dvm/share/dvm/completions $fpath)
autoload -Uz compinit
compinitIf your ~/.zshrc already runs compinit, add only the fpath=... line above it.
dvm init app
dvm sync app
dvm sync --all
dvm sh app
dvm ssh app -- pwd
dvm cp ./plan.md app:.
dvm log cloudflared
dvm ssh-key app
dvm gpg-key app
dvm ls
dvm stop app
dvm stop --all
dvm stop --inactive
dvm rm app --yesdvm sync <name> creates the Lima VM if missing, starts it, runs
recipes/baseline.sh, runs recipes selected by ~/.config/dvm/vms/<name>.sh, then
runs ~/code/<name>/.dvm/sync.sh inside the guest if that file exists.
Use dvm sync --all after recipe changes or when you want to update recipe-managed
tools such as AI CLIs across every active VM.
dvm rm requires --yes and checks nested Git repos for dirty work before deleting.
Use --force only when you intentionally want to skip that check.
dvm stop --all stops every DVM-managed Lima instance listed with the internal
dvm- prefix. This releases VM memory without deleting disks or config.
Use dvm stop --inactive to stop only VMs without a detected active shell,
tmux/zellij, or known DVM service unit.
dvm ssh-key <name> creates separate VM-local SSH keys for Git hosting access and Git
commit signing. Use the access key as a deploy/authentication key and add the signing
key to your Git hosting account's SSH signing keys, if supported.
Global defaults:
~/.config/dvm/config.shPer-VM config:
~/.config/dvm/vms/app.shCreate a VM config from the bundled app example:
dvm init appdvm init writes a fully commented template; uncomment what you need. Defaults are
DVM_CPUS=2, DVM_MEMORY=2GiB, DVM_DISK=10GiB, DVM_CODE_DIR=~/code/$DVM_NAME,
empty DVM_PORTS. The template shows the host's CPU and memory ceilings as inline
comments and calls use_tools, a helper defined in your global config that holds the
recipes shared across every app VM. Edit ~/.config/dvm/config.sh once to choose
your toolset; new VMs pick it up automatically. Per-VM configs can add extra
recipes, define more helpers, or comment the use_tools line for a minimal VM.
~ in DVM variables means the guest user's home. Host project directories are not
mounted into the VM. VM names use lowercase letters, numbers, and hyphens, starting
with a letter. DVM commands use the public project name, for example eshlox-net; the
internal Lima instance is named dvm-eshlox-net.
New app VM:
dvm init myapp
dvm sync myapp
dvm sh myappDedicated llama VM:
dvm init llama llama
dvm sync llama
dvm log llama -fThe bundled llama VM opens port 8080 for host access at http://127.0.0.1:8080 and
VM-to-VM access at http://lima-dvm-llama.internal:8080. It skips the dev-tool
baseline and installs only the llama recipe.
Cloudflared tunnel VM:
dvm init cloudflared cloudflared
CLOUDFLARED_TOKEN="..." dvm sync cloudflared
dvm log cloudflared -fThe cloudflared token is staged through a mode 0600 guest temp file during sync
instead of being passed as a limactl shell env argument.
Tailscale supports two patterns:
Private dev access — reach app VMs by hostname from your Mac without
juggling host ports. Add use tailscale to each app VM and sync:
TAILSCALE_AUTH_KEY="tskey-..." dvm sync fida
# then from the host browser: http://fida:5173Public Funnel URLs — publish a local service at a public *.ts.net URL
on demand:
# One-time: create the VM and join your tailnet
dvm init tailscale tailscale
TAILSCALE_AUTH_KEY="tskey-..." dvm sync tailscale
# Turn Funnel ON, pointing at another VM's service
DVM_TAILSCALE_FUNNEL_TARGET="http://lima-dvm-app.internal:3000" \
dvm sync tailscale
# Turn Funnel OFF
dvm sync tailscaleFunnel is OFF by default; the recipe resets it on every sync, so leaving the
target unset turns it off. Auth keys get the same mode 0600 guest temp-file
handling as Cloudflare tokens. See docs/services.md
for both patterns, the Funnel ACL prerequisite, and auth-key sourcing options
(global config, env var, or macOS Keychain).
Bundled recipes live in share/dvm/recipes and can be copied or overridden in
~/.config/dvm/recipes.
Local recipes override bundled recipes. Keep only recipes you intentionally customize in
~/.config/dvm/recipes; otherwise DVM will not see bundled recipe updates.
Add your own tools with recipes. Use individual bundled tool recipes such as
use helix and use lazygit, or define your own local helper in
~/.config/dvm/config.sh.
First-pass recipes include:
baseline: required setup basics onlyzsh,git,helix,lazygit,starship,fzf,bat,git-delta,just,tmux,zellij,yazi: optional interactive toolsagent-user:dvm-agentplus mandatory Bubblewrap sandboxing for AI toolscodex,claude,opencode,mistral: hosted AI CLIs inside the VMchezmoi: public HTTPS dotfilesllama: dedicated llama service VMcloudflared: dedicated Cloudflare Tunnel VMtailscale: tailnet membership and optional Funnel public ingressnode,python: language basics
Codex and Claude default to unattended mode inside the dvm-agent Bubblewrap sandbox
so they can edit code and run project commands without prompting. Set
DVM_CODEX_YOLO=0 or DVM_CLAUDE_BYPASS=0 in a VM config when you want the tool's own
approval prompts and sandboxing.
dvm sync llama
CLOUDFLARED_TOKEN="..." dvm sync cloudflared
dvm log cloudflaredExample service configs live in share/dvm/vms. Copy one into ~/.config/dvm/vms
when you want that VM to be active.
- Commands: command reference
- Config: global and per-VM Bash variables
- Lima: template and networking decisions
- Recipes: recipe authoring and bundled recipe behavior
- AI:
dvm-agentand hosted AI tools - Services: llama and cloudflared VMs
- Dotfiles: chezmoi over HTTPS
- Security Standards: operating rules
- Docs index