Using Node with Docker

As part of developing for Cloud66, I have a whale of a time writing many a Dockerfile.

Today, I thought I'd share some of the more weird or painful issues that I have experienced while trying to get various Node projects to work with Docker. I will be on cloud 9 if by virtue of reading this article you avoid any one of these in the future!

Without further ado (and terrible puns!) follows a short list.

EXDEV: cross-device link not permitted

If you try update npm 3.x in an image that already has npm installed (docker run mhart/alpine-node:5 npm install/update -g npm); install certain packages, such as gulp, twice; or run npm update inside the Dockerfile, you may experience this error. More specifically, this happens whenever the installation process for a package uses fs.rename.

A clever solution, suggested by nodrluf [2], is to add the following to your Dockerfile (it is important that they are part of the same RUN command, as you will see shortly)

RUN mv ./node_modules ./node_modules.tmp \  
  && mv ./node_modules.tmp ./node_modules \
  && npm install (or whichever command caused your failure)

The reason this works is a bit long-winded. For one, it only happens when you are using the AUFS filesystem with Docker. AUFS is a layered filesystem. For a Docker image, at any one point there are many read-only AUFS layers corresponding to the Docker image layers, with a writable layer on top where the operations take place (either as part of creating the full Docker image, or running a Docker container). The union of these layers gives the complete picture of the filesystem.

When you try to run the rename system call on AUFS, it cannot do so in the read-only layers - they're read-only! It must copy all the data it wants to use to the current layer from the read-only layer, and then perform the operation. Since the read-only layer could require data from another read-only layer below it, and so on, this is a potentially very expensive operation.

If AUFS deems the operation too expensive, it throws an EXDEV error [1]. Since your application does not know how to handle this error, we end up with our current predicament.

However, mv does know how to handle the EXDEV error correctly, and uses system calls other than rename. The end result is that the node_modules folder is in now in the writable layer, and you no longer get the EXDEV error in future operations. Yay!

Bower installation does not run with root!

The solution seems simple enough - bower has an --allow-root flag.

But what if bower install happens as part of, say, grunt init? Not so easy to pass flags anymore.

A possible solution is to add the following to your Dockerfile, which affects the global config of bower.

RUN echo '{ "allow_root": true  }' > /root/.bowerrc  

Alternatively, you could perform the build with a non-root user - create one with adduser and use the USER directive in your Dockerfile to change user. You'd likely have to fumble a bit with permissions, but it seems to be the technically superior solution.

Containers are ephemeral!

More than once I have seen projects that create config files (or even plain text password files!) at runtime. This is usually to accommodate a pretty installation page on the illustrious port 80.

This is all great and stuff, and works fine for installation on a server rather than a container. But what if your container crashes? Or you want to move it to another server? You runtime generated files will be lost. As such, you should either commit any required files for your containers operation to your application's repository, or generate them during the build. If you have any sensitive information, use environment variables.

References

[1] http://aufs.sourceforge.net/aufs.html

[2] https://github.com/npm/npm/issues/9863