Node version management in Docker containers
Tags: node, docker
Published: 2019-07-05 15:04:10

Recently I came across a solution to a Docker specific problem that I didn't know about and that I think is not used very often. Mainly because you shouldn't use it without knowing the implications. It can be a useful thing to know nonetheless.

I was building containers for a client where the codebase for historical reasons uses a few different node versions. Being more of a backend / devops developer myself I hadn't really used `nvm` before.

Usually I would opt for using a Docker image `FROM node:<version_here>` but the version in use (perhaps for good reason) isn't on Docker hub. And perhaps it's actually more maintainable if the Dockerfile uses the `.nvmrc` file for determining which node version to use.

Building this container appeared to be harder than I expected. This is because nvm uses environment variables that are declared in the profile. Which is quite logical because its a developer tool and you wouldn't want switching environments to have system-wide effects.

## Current solutions

The reason why I'm writing this post is because most of the answers out there are giving advise that feels just wrong. Like this:

```dockerfile
# replace shell with bash so we can source files
RUN rm /bin/sh && ln -s /bin/bash /bin/sh
```

Or hardcoding the node version in the Dockerfile:

```dockerfile
ENV NODE_VERSION 4.4.7
```

If you're using nvm instead of a Docker hub image it's better to use the `.nvmrc` file in your folder as a single point where you document the used node version.

## A better solution?

It was only after building a `rvm` container that I came across this [Stack Overflow Answer](https://stackoverflow.com/a/51803492) 

> Don't use `RUN bash -l -c rvm install 2.3.3.` It's very scruffy. You can set shell command by SHELL [ "/bin/bash", "-l", "-c" ] and simply call `RUN rvm install 2.3.3.`

After this your Dockerfile gets a lot cleaner. But please note that you're doing something unusual here. With `-l` you're asking for a "login shell". Which is not normally the case in a Docker container and it might have side-effects you didn't intend to. But that mostly depends on what you're doing. In most cases you're safe. And in my case I don't really mind because I was using multi stage builds in docker so the end result wasn't affected by this. And in case you were using this in a development container I think you're fine as well.

So this is the Dockerfile I came up with. Note that I don't cleanup my apt file because it's an intermediate build step

```dockerfile
FROM debian

SHELL [ "/bin/bash", "-l", "-c" ]

RUN apt-get update && apt-get install -y curl
RUN curl --silent -o- https://raw.githubusercontent.com/creationix/nvm/master/install.sh | bash
# this now works
RUN nvm install && nvm use
```

Please do note that this doesn't directly work in an Ubuntu container because the `/root/.bashrc` file in Ubuntu contains this: 
```bash
# If not running interactively, don't do anything
[ -z "$PS1" ] && return
```

And it would then skip setting up the environment variables needed for nvm because in a non login shell `$PS1` is not set.

## Summary

I do think that this solution is cleaner on more than one front. But of course I'm open for suggestions ;) 
I hope this saves others time and helps clean up those otherwise bloated Dockerfiles.