asdf
Extendable version manager for multiple languages
asdf is a CLI tool that manages multiple runtime versions of languages and tools through a plugin system with a single per-project config file. It runs locally on developer machines.
Key features
- Manage many runtimes
- Plugin ecosystem
- Single config file
- Cross-platform
Pros & cons
Strengths
- Manages many runtimes
- Per-project version files
- Large plugin catalog
Trade-offs
- Shims can confuse tooling
- Plugin quality varies
asdf replaces
Last reviewed Aug 26, 2026 · 860 words
One file, .tool-versions, committed next to the code, pins every runtime a project needs: nodejs 22.11.0, python 3.12.7, terraform 1.9.8, whatever the plugins cover. That is the entire idea of asdf, and it is why a version manager has 25,547 stars. Where nvm handles Node and pyenv handles Python and each has its own shell hook and its own way of breaking, asdf handles all of them through one plugin interface and one config file. It is MIT, ships as a single Go binary since its rewrite, and its 64 MB footprint is really the footprint of whatever it just installed. It is also the tool self-hosters most often put in the wrong place, which is what this guide is about.
It manages toolchains, not services
asdf lives on developer machines and build hosts. It is not how a Node app should find Node inside a container, and it is not a process manager. The right mental model is a per-project lock for tools, the way a lockfile is a per-project lock for libraries. In a homelab that puts it in exactly 3 places: the workstation you develop on, the box where you build things from source (a Gitea Actions runner, a build VM), and any long-lived server where you run scripts that depend on a specific interpreter. Production containers should install their runtime in the Dockerfile and never see asdf.
The commands that cover 95 percent of use
Install the binary, add the two shell lines the docs give you, then:
asdf plugin add nodejs
asdf plugin add python
asdf list all nodejs | tail -5 # what can be installed
asdf install # everything in ./.tool-versions
asdf set nodejs 22.11.0 # writes ./.tool-versions
asdf set --home python 3.12.7 # writes ~/.tool-versions
asdf reshim # after any global npm/pip install
asdf install with no arguments reads the project file and installs anything missing, which is the whole onboarding story for a new machine: clone, asdf install, done. Resolution walks up from the current directory to the nearest .tool-versions, falling back to ~/.tool-versions, so a server-wide default and per-project overrides coexist without ceremony.
Shims are the price, and they surprise people
asdf puts a directory of shims on your PATH, one tiny script per executable. Running node runs the shim, which finds the right .tool-versions, then executes the real binary. Two consequences follow. First, every invocation pays a resolution cost of tens of milliseconds, which is invisible for a shell session and noticeable when a build tool spawns the interpreter 400 times. Second, anything that resolves the binary path once and caches it, such as an editor, a systemd unit or a cron job, may pick up the shim without the environment the shim expects. For systemd and cron, use the real path from asdf which node and skip the shim entirely. For a global tool installed with npm install -g, run asdf reshim or the new command will not exist on your PATH.
Plugin quality is the real risk
The plugin catalogue is enormous, and that is both the strength and the weakness. The core plugins for Node, Python, Ruby, Erlang and Elixir are maintained by the asdf organisation and well behaved. Beyond those, a plugin is a shell script somebody wrote, and it may or may not handle a new release, a new CPU architecture or a checksum change. Two practical notes: the Python plugin builds CPython from source through python-build, so a small VPS wants build-essential, libssl-dev, libffi-dev, zlib1g-dev and about 5 minutes per version; and the Node plugin downloads prebuilt binaries, so it is quick but only for platforms Node publishes for. Read a plugin's repository before trusting it on a server.
mise reads the same file and skips the shims
If you are choosing fresh in 2026, look at mise before committing. It reads .tool-versions unchanged, so migration is a rename of nothing, but it is written in Rust, updates PATH on directory change instead of shimming, and adds task running and per-directory environment variables. Teams that standardised on asdf years ago have no urgent reason to move; the plugin ecosystem is shared, and the file format is the standard. Individuals starting now mostly end up on mise. Both belong in the same developer tools category and solve the same problem; they differ on mechanism and scope.
What I'd do
Put .tool-versions in every repository that has a runtime dependency, run asdf install in the CI runner's setup step so the build uses exactly what the developer used, and keep asdf off production hosts. On the build box, set a ~/.tool-versions for the defaults and let projects override. If a systemd unit ever needs to call an asdf-managed interpreter, hardcode the path from asdf which. And if you are setting up a new laptop today with no existing asdf muscle memory, install mise instead, point it at the same file, and lose nothing.
Compare asdf
1 head-to-head comparisons.
Similar developer tools & git apps
Excalidraw
Developer Tools & GitVirtual hand-drawn style whiteboard
Replaces Miro
lazygit
Developer Tools & GitSimple terminal UI for Git commands
Replaces GitKraken, Sourcetree
Hoppscotch
Developer Tools & GitOpen-source API development ecosystem
Replaces Postman, Insomnia
json-server
Developer Tools & GitFull fake REST API from a JSON file in seconds
Replaces Mockoon, Postman Mock
Strapi
Developer Tools & GitLeading open-source headless CMS
Replaces Contentful
NocoDB
Developer Tools & GitOpen-source Airtable alternative
Replaces Airtable