Basti’s Buggy Blog

Poking at software.

Using Multiple Node Versions Without nvm

Managing projects on different versions of node can be a bit annoying. Some Linux distributions ship ancient node versions that have never even heard of big arrow functions.

Table of Contents

Node Version Manager (nvm)

Some years ago I tried the “Node Version Managernvm — a shell script that downloads and installs node versions on demand. To download and use a specific version of node, a single command is sufficient:

nvm install 10.10.0

It automatically downloads the selected version and symlinks it on your system. This allows easy switching of node versions when moving from one to another project. To remember which version is needed for a specific project, .nvmrc files can be used to declare a project specific version.

As an avid user of docker, there is no node1 project that I run or develop without docker. When developing, there have been conflicts with node dependencies that were installed by the host system’s npm, which can be incompatible with the node version in the docker container. As the docker development container bind-mounts the node_modules directory in order to save precious development time and bandwidth, those different versions can interfere and lead to conflicts.

Even when using the same version of node on your host system and in the docker container, version conflicts for binary compiled dependencies can still arise. node-sass for example is compiled on installation and was unable to run in the docker container.

The prettier alternative

As I have been using docker for my development workflow anyway, I bound some project-specific aliases to use the containerized version of node instead of the version of node present on the host system. This is easy to do and allows to run projects with different versions of node concurrently. To set the aliases I use a simple bash script that I source when I start development.

#!/bin/bash

set -euo pipefail
# -e             abort on error
# -u             unset variables are errors
# -o pipefail    pipe does not adopt exit status of last command

# The docker compose project name. The containers will be named
# "my-super-important-project_<service>".
export COMPOSE_PROJECT_NAME=my-super-important-project

# The node version to use. Also export it to make it available for
# other tools like docker-compose in order to choose the right docker
# base image
export NODE_VERSION="12.4.0"

# The tell docker to start the node process with the used and group id
# from the host system. This prevents files being created in bind
# mounts that are not deleteable by the unprivileged user.
DOCKER_USER="$(id -u ${USER}):$(id -g ${USER})"

# The base 'docker run' command with a couple of parameters:
#    --init    spawn an init process responsible for handling signals
#    -i        interactive -> accept input from stdin
#    -t        allocate a pseudo tty
#    --rm      remove the container after it exits
#    -u…       set the group and user id of the docker main process
#    -v…       mount the current host working directory into the container
#    -w…       set the container working directory to the host working directory
DOCKER_COMMAND="docker run --init -it --rm -u $DOCKER_USER -v $(pwd):$(pwd) -w $(pwd)"

alias npm="$DOCKER_COMMAND node:$NODE_VERSION npm"
alias node="$DOCKER_COMMAND node:$NODE_VERSION node"
alias npx="$DOCKER_COMMAND node:$NODE_VERSION npx"
Code Snippet 1: source.sh

Each project has its own source.sh file. Build scripts and Makefiles can use the environment variables and aliases defined in this file. This allows for a centralized definition of project variables and versions that is sufficient for my use cases.

TODO Linux signal handling

You might not have used the --init flag in your docker run commands yet. This is mainly relevant if you use docker with an interactive tty. When using the flag, docker spawns an additional process (Tini) responsible for handling various signals. More about Linux process signal handling in an upcomping post.

TL;DR

  • Download the source.sh script into your project directory
  • Change the COMPOSE_PROJECT_NAME variable
  • Run . source.sh or source source.sh
  • Execute your favorite docker commands (npm i)

  1. or any other project (?) ↩︎