Version managers scope the runtime. They don't scope everything else
nvm, pyenv, and rbenv genuinely solve one problem well: which language runtime a given project uses. They don't touch the rest of what actually differs between projects — global npm packages installed once and forgotten, a system library one project needed that now shadows a different version another project expects, Homebrew formulae that drifted since the last time you set up a machine, a stray environment variable in ~/.zshrc that only one project's build script depends on. "It works on my machine" is really "it works on my machine today" — the same Mac, six months and a dozen projects later, is a different machine than the one the setup instructions were written against, and version managers were never scoped to catch that.
What "one workspace per project" looks like in practice
The Ubuntu Developer template exists for exactly this — its own description is "For coding, shared projects, and VS Code Remote," and it carries a "Best for most developers" badge. Its Code & Build profile provisions 4 CPU cores, 8 GB of RAM, and 100 GB of storage: sized for a real toolchain and its dependencies, not a minimal test runner. The habit itself is simple: one workspace per project, not one workspace you reuse and half-remember the history of.
The practical split that makes this work day to day: your actual project files — the ones with git history, the ones your editor has open — live in a shared folder on your Mac, mounted straight into the guest (automatically under /Volumes/My Shared Files for a macOS guest, or via a per-folder mount -t virtiofs command shown right in the app for Linux). The OS, the compiler toolchain, the system packages, and everything a fresh apt install would touch stay inside the disposable guest instead of your host. Edit with VS Code Remote-SSH connected straight into the workspace, and the experience is indistinguishable from working locally — except a broken system package or a dependency you regret installing stays contained to a VM you can throw away, rather than a Mac you use for everything else.
Matching a colleague's setup exactly, not just similarly
A README's setup instructions are a description someone else has to correctly interpret — which package manager version, which system libraries, which order to run things in. A sealed base image is the actual disk, not a description of one. Configure a project's environment once, seal it, and everyone on the project clones the identical toolchain instead of independently reconstructing something close to it. The base image versioning post covers the naming discipline worth adopting once you're doing this regularly — the app tracks no lineage between generations, so a name like ubuntu-24.04-project-x-node22-2026.09 is the only record of what's actually in a given image. Handing that exact image to a teammate is a real, supported action too: Preferences → Base Images can export the selected image to a raw disk file, which is a genuine copy of the environment rather than a shared cloud reference — move it to another Mac, or archive it before a generation gets pruned.
Trying a new framework without touching your main Mac's global state
The smaller, more frequent version of the same idea: evaluating a new library or framework doesn't need a whole new named project, just a workspace you're willing to throw away. Clone a Disposable Workspace from your usual dev base image, run whatever the new tool's own quickstart tells you to — npm create, a fresh interpreter install, a system package you're not sure you'll keep — and close it when you're done forming an opinion. None of it touches your Mac's global npm cache, your actual Homebrew install, or any lockfile you'd otherwise have to remember to revert. Cloning is copy-on-write, so starting one of these costs seconds, not the minutes a fresh install would take on bare metal.
Where a VM is actually the right call, and where it's overkill
Worth being honest about the tradeoff, in the same spirit as the Docker Desktop / OrbStack comparison on this blog: for pure per-project dependency isolation on a single OS, a container is often lighter-weight and faster to spin up than a VM, and a good language-level version manager plus Docker Compose covers a lot of this same ground. A full Linux VM earns its overhead specifically when you want the isolation to include the OS itself — a genuinely different kernel and system package set, not just an app-level sandbox sharing your Mac's kernel — or when you want a guest's installed system packages and root-level changes to be as disposable as its project files. If today's actual pain point is "node_modules is a mess," a container solves it more cheaply. If it's "I no longer trust what's installed system-wide on my own Mac," that's the problem this workflow is actually for.
What to actually do
- One workspace per project, not one long-lived workspace you reuse indefinitely and stop trusting the history of.
- Keep project files in a shared folder, so the guest holds the toolchain and system state, not the only copy of your work.
- Seal a base image once a project's setup is right, and hand teammates the actual disk via export rather than a setup doc they have to reinterpret.
- Reach for a Disposable Workspace, not your main Mac, the moment you're evaluating something you're not sure you'll keep.
Related reading: a naming convention for base images once you're sealing them regularly, the same disposable-workspace pattern applied to QA and DevOps, editing inside a workspace with VS Code Remote, and when a container is the better fit instead. Or download Velo Workspaces and try it yourself.