Language Runtime Version Management

2 minute read

Do you need different versions of nodejs, ruby, go or other language runtimes / compilers? Why not use Docker?

The Problem

For almost every programming language, there is one or more version management utilities, e.g. rvm or rbenv for Ruby, nvm, n or nave for Node, pyenv for Python and many others. Every tool has it’s own quirks and specifics, including those like asdf that are trying to rule them all.

The reason for this mess is probably all the moving parts and dependencies needed to build a specific version of the language runtime or compiler. This is why OCI containers are perfect-fit for the job, as they can bundle a specific version with all the necessary dependencies.

Moreover, Docker library already has a collection of base images for the most popular language runtimes and compilers.

How?

Add this to your .bash_profile:

# Linux
here() {
  docker run -it --rm -v "$PWD:/work" -w /work -u "$(id -u):$(id -g)" "[email protected]"
}

# macOS / Windows
here() {
  docker run -it --rm -v "$PWD:/work" -w /work -u "1000:1000" "[email protected]"
}

And use it:

[email protected]$ here -p 3000:3000 node:16 bash
[email protected]:/work$ yarn start
Starting development server...

Alternatively, you could create a shell script named here in your $HOME/bin directory and run chmod +x "$HOME/bin/here" afterwards:

#!/bin/sh

exec docker run -it --rm -v "$PWD:/work" -w /work -u "$(id -u):$(id -g)" "[email protected]"

The Details

The idea is to use a base image from the Docker library and run it in the current working directory. It allows for easy version management and at the same time it provides environment isolation and bundles all the dependencies needed for the specific version.

Security

If you are interested in securing your development containers, go check my follow-up post about securing development with Docker and gVisor.

Caveats

The general thing to consider with Docker is that instances are ephemeral and changes outside the working dir will not be persisted after exiting the container.

Docker Base Images

We are running the Docker image as the current user, so the OS package management in the container lacks persmissions to install new packages. It requires some Docker knowledge and additional work to build a new base image for development. One way to circumvent that is to jump into running container using docker exec -it -u "0:0" running_docker_instance bash and install the packages you need with root permissions. However, this needs to be done for every container instance.

In some cases this approach might result in multiple fetches of package dependencies and increased data usage, due to missing caches. It might not be very suitable for slow Internet or otherwise limited connections.

Non-Linux Host OS

Windows and macOS run Docker a VM, thus the mounts (in this case, the work directory) are connected through cifs or nfs and it’s harder to watching for changes in files. You need to use polling, instead of the general approach to listen to filesystem events.

Note: Maybe Docker on WSL2 solves that issue?