Skip to content

Config & setup scripts

Config is Bash. Built-in defaults load first, then global config, then per-VM config, each overriding the last. Set a value once in global config and every VM inherits it; per-VM config only holds what that one VM needs to differ on. The first dvm new scaffolds the global config for you.

~/.config/dvm/config.sh # global config
~/.config/dvm/setup.sh # global setup script (optional)
~/.config/dvm/vms/<vm>/config.sh # per-VM config
~/.config/dvm/vms/<vm>/setup.sh # per-VM setup script
~/.config/dvm/config.sh
DVM_USER=developer
# DVM_CPUS auto-detects from host cores; memory and disk use the values below.
# Uncomment any line to pin it for every VM.
# DVM_CPUS=2
# DVM_MEMORY=2
# DVM_DISK=30
~/.config/dvm/vms/app/config.sh
# Only what this VM needs beyond the global config.
DVM_MEMORY=4
DVM_PORTS=(3000:3000 5173:5173)
VariableDefaultMeaning
DVM_CPUShost cores minus 1-2 (min 2)vCPUs, a time-shared ceiling not pinned to host cores
DVM_MEMORY2GiB memory (the one knob that reserves host RAM)
DVM_DISK30GiB disk (thin-provisioned ceiling, costs only what is used)
DVM_USERdevelopermain guest user
DVM_SUBUID_COUNT65536subordinate uid range size for DVM_USER
DVM_SUBGID_COUNT65536subordinate gid range size for DVM_USER
DVM_PORTS()extra host:guest port forwards

The defaults aim at a minimal but workable disposable VM: enough to run editor, shell, AI CLIs, and a dev server at once. Bump DVM_MEMORY per VM for heavy Docker builds. Lowering DVM_DISK saves nothing (the qcow2 disk is thin) and only risks running out of space, so leave it unless you need a larger ceiling.

DVM_CPUS and DVM_MEMORY behave differently, which is why only one of them auto-detects:

  • CPUs are time-shared, not reserved. A vCPU is just a host thread that the host scheduler runs on a physical core only when the guest has work. An idle VM’s vCPUs consume effectively no host CPU, and a busy VM can burst up to its vCPU count. So DVM_CPUS is a per-VM ceiling, not a slice carved out of the host: five idle VMs do not stop a sixth from using every core. Because of this, the default leaves the host a small reserve (1 core on hosts up to 4 cores, 2 beyond, floored at 2) so the host and dvm/limactl stay responsive when one VM saturates its share. Oversubscribing is safe; the only cost is contention (visible as guest “steal time”) when several VMs are busy at once.
  • Memory is reserved. Depending on the Lima backend, DVM_MEMORY is committed up front, so it does not auto-scale and 5 × max would over-commit the host and fail to start or push it into swap. Set it to what each VM actually needs.

To override the auto-detected CPU count, set DVM_CPUS globally or per VM. Existing VMs keep the value they were created with; changing the default only affects VMs created afterward.

VM and DVM_USER names must start with a lowercase letter and contain only lowercase letters, numbers, and hyphens.

The default subordinate id ranges support rootless Docker and Podman. Set both counts to 0 only when the user should get no ranges. If the user already exists, DVM adds missing ranges on the next sync.

These are read from the environment at invocation, not from config.sh:

VariableDefaultMeaning
DVM_VERBOSE01 streams the full output live instead of running steps behind the spinner
DVM_LIMA_LOG_LEVELwarnLima log level passed to limactl --log-level (info when DVM_VERBOSE=1)
DVM_DRY_RUN01 prints the limactl start argv and setup plan without contacting Lima
NO_COLORunsetset (to anything) to disable colored progress output and the spinner
DVM_CONFIG_DIR~/.config/dvmconfig location
DVM_CACHE_DIR~/.cache/dvmlock-file location
DVM_STATE_DIR~/.local/state/dvmper-VM log location
DVM_LIMACTLlimactlpath to the limactl binary

dvm sync shows a one-line spinner per step ( on success, on failure) and captures the full output of each step to a log, instead of printing Lima’s and your setup script’s output to the terminal. Logs are per VM, under ${XDG_STATE_HOME:-~/.local/state}/dvm/<vm>/:

FileContents
lima.loglimactl instance create/start and guest provisioning
setup.logglobal and per-VM setup-script output

dvm sync truncates both at the start of each run, so they always reflect the latest attempt. On failure DVM prints which step failed, the log path, and the last lines of that log. Set DVM_VERBOSE=1 to stream everything live (still written to the logs) when debugging a VM that will not come up.

Run during dvm sync after the VM is created, started, and the user/project directory exist. Both are convention-based paths, run in order when present:

  1. ~/.config/dvm/setup.sh (global, runs for every VM)
  2. ~/.config/dvm/vms/<vm>/setup.sh (per-VM)

Put shared provisioning in the global script and VM-specific steps in the per-VM script. Neither is a config variable, so per-VM config cannot redirect the global one.

DVM prepends set -Eeuo pipefail and exports these variables to each script:

VariableMeaning
DVM_NAMEVM name from config
DVM_VMsame as DVM_NAME
DVM_LIMA_NAMELima instance name, such as dvm-app
DVM_USERguest dev user
DVM_CODE_DIRguest project directory
DVM_PORTScomma-separated port forwards
DVM_SUBUID_COUNTconfigured subordinate uid range size
DVM_SUBGID_COUNTconfigured subordinate gid range size

Setup scripts are trusted provisioning code. DVM checks they are owned by you and not group/world writable (see troubleshooting.md to repair). For snippets, see examples.

Built and maintained by eshlox.