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/base/Containerfile # base image definition (optional)
~/.config/dvm/vms/<vm>/config.sh # per-VM config
~/.config/dvm/vms/<vm>/setup.sh # per-VM setup script
~/.config/dvm/vms/<vm>/projects/<proj>/project.sh # project container spec (optional)
~/.config/dvm/vms/<vm>/projects/<proj>/setup.sh # project setup, runs in the container

Shared tooling is baked once into the base image Containerfile. The per-VM setup.sh runs for VM-specific, stateful steps (SSH keys, dotfiles, tunnels).

~/.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
DVM_VM_TYPEvz on Apple Silicon, else unsetLima VM type; vz is lighter than QEMU and reclaims idle VM memory better
DVM_DEV_BASEdev-base image, else bare Fedoradefault image for project containers that set no IMAGE

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.

DVM_VM_TYPE defaults to vz on Apple Silicon macOS, using Apple’s Virtualization framework: lighter overhead than QEMU, and it lets the host reclaim idle VM memory far more readily, which matters when running many VMs at once. Elsewhere it is empty and Lima picks its own backend. Set it explicitly to override, including DVM_VM_TYPE=qemu to force software emulation. See Why Lima, not Tart or Apple container.

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

These configure the base image build:

VariableDefaultMeaning
DVM_BASE_DIR~/.config/dvm/baseContainerfile and build context
DVM_BASE_IMAGE~/.cache/dvm/base/disk.qcow2built image VMs boot from
DVM_BUILDER_LIMAdvm-builderbuilder Lima instance name
DVM_BUILDER_MEMORY4builder memory (GiB)
DVM_BUILDER_DISK50builder disk (GiB)
DVM_BOOTC_BASEfedora-bootc, digest-pinned in bin/dvmimage dvm-base is built from
DVM_BISC_IMAGEbootc-image-builder, digest-pinned in bin/dvmdisk-image builder

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.logper-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.

The per-VM setup script, ~/.config/dvm/vms/<vm>/setup.sh, runs during dvm sync after the VM is created, started, and the user/project directory exist. It is convention-based and runs when present.

Put VM-specific, stateful steps here (SSH keys, dotfiles, tunnels). Shared tooling that every VM installs belongs in the base image Containerfile, baked once, rather than re-run on every sync.

DVM prepends set -Eeuo pipefail and exports these variables to the 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

The setup script is trusted provisioning code. DVM checks it is owned by you and not group/world writable (see troubleshooting.md to repair). For snippets, see examples.

A project under a VM has its own project.sh (a container spec, sourced like config.sh) and setup.sh (which runs inside the container on sync). Scaffold them with dvm add <vm>/<proj>.

VariableDefaultMeaning
IMAGE$DVM_DEV_BASEcontainer image the project runs
NESTED01 exposes /dev/fuse and relaxes SELinux so the project can run its own podman/compose
PROJ_WORKDIR/workworking dir and mount point of the persistent workspace volume
PROJ_PORTS()host:guest ports to publish (on the VM loopback, forwarded to localhost)

The same owner/mode checks that guard config.sh apply to project.sh and the project setup.sh. See Trust tiers & project containers for the full model.

Built and maintained by eshlox.