Dockerizing Node 8 on Raspberry Pi

Intro

Initially I was going to document the meaning of Docker layers, one of the killing features from Docker (IMHO), however during researching I came across something that I found curious and I’m going to dedicate a whole article about it.

Recently I documented how to dockerize Alpine on your Raspberry Pi, the next baby-step for me was to document how to build a more specific image from my dockerized Alpine image.

As the concept of layers in Docker proposes, your image can extend from a preexisting image. Therefore, from the image kafebob/rpi-alpine-base I will create an image that provides a base environment for NodeJS applications.

You can use a precompiled version of NodeJS available from Alpine packages or a compiled version directly on your Raspberry. I’m going to document both.

Precompiled NodeJS

From Alpine Repositories you can dockerize Node, a Dockerfile like next one would be more than enough.

1
2
3
4
5
6
FROM kafebob/rpi-alpine-base:3.6

RUN addgroup -g 1000 node && \
adduser -u 1000 -G node -s /bin/sh -h /home/node -D node && \
apk add --update --no-cache nodejs-npm && \
rm -rf /tmp/* /var/cache/apk/*

From previous Dockerfile, you will notice in line 3 & 4 the user and group node, I’m using it for development purposes. To build an image called kafebob/rpi-alpine-node locally available on your Pi, execute next command from same directory where previous Dockerfile is located.

1
sudo docker build -t kafebob/rpi-alpine-node .

Size of previous images is 43.3MB.

Now it’s time to create a container and check Node version.

1
2
3
4
5
6
sudo docker run --rm -it --user=node --entrypoint=/bin/sh kafebob/rpi-alpine-node
/ # node --version
v6.10.3
/ # npm --version
3.10.10
/ #

As you can see, latest available version from Alpine packages is Node.js-v6.10.3-r1 in Alpine 3.6 (at the time of writing this article).

So if you want to use a different/higher version, you should compile Node in your Raspberry Pi.

Compiling NodeJS

Here you could choose two alternatives, to compile your own version using the source code or to use NVM, although I must warn you, even though Raspberry is a masterpiece of hardware engineering in terms of computing power is not the best resource you can get, so be prepared to wait more than 2 hours for a compiled NodeJS binary.

I tried to compile my own version using Michael Hart’s Dockerfile, but Raspberry is literally inaccessible and after waiting more than 10 hours I’ll always got a message similar to

1
2
3
4
5
6
7
8
g++: internal compiler error: Killed (program cc1plus)
Please submit a full bug report,
with preprocessed source if appropriate.
See http://gcc.gnu.org/bugs.html for instructions.
make[1]: *** [deps/v8/src/v8_base.target.mk:654: /node-v8.2.0/out/Release/obj.target/v8_base/deps/v8/src/api.o] Error 4
make[1]: *** Waiting for unfinished jobs...
GCC Bugs - GNU Project - Free Software Foundation (FSF)
GCC Bugs Table of Contents Reporting Bugs What we need What we DON'T want Where to post it Detailed bug reporting instructions Detailed bug reporting instructions for GNAT

With NVM I had better luck and after 2 and a half hours compiling in my PI, I managed to get a correct image. For those who do not know, NVM is a simple bash script to manage multiple active node.js versions.

So then I’ll show you the Dockerfile, you are about to install NVM 0.33.2, Node 8.2.0 and NPM 5.3.0, as well user and group node for development purposes. (You are able to change versions in line 3).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
FROM kafebob/rpi-alpine:3.6

ENV NVM_VERSION=v0.33.2 NODE_VERSION=v8.2.0 PROFILE=/home/node/.bashrc

RUN apk add --update --no-cache curl bash ca-certificates openssl \
ncurses coreutils python2 make gcc g++ libgcc linux-headers && \
addgroup -g 1000 node && \
adduser -u 1000 -G node -s /bin/bash -h /home/node -D node && \
rm -rf /tmp/* /var/cache/apk/*

USER node
RUN cd /home/node && \
curl -o- https://raw.githubusercontent.com/creationix/nvm/$NVM_VERSION/install.sh | bash && \
echo "#NVM Setup" >> $PROFILE && \
echo 'export NVM_DIR="$HOME/.nvm"' >> $PROFILE && \
echo '[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" # This loads nvm' >> $PROFILE && \
source $PROFILE && \
nvm install -s $NODE_VERSION && \
rm -rf /home/node/.nvm/.cache/src/node-$NODE_VERSION

The key here is to use -s flag for nvm install which requests nvm download Node source in order to compile it locally.

To create the image, similar to precompiled node version, in the same directory where previous Dockerfile is located:

1
sudo docker build -t kafebob/rpi-alpine-node .

Size of this image is 226MB! It is much larger since I have left compiling tools.

From this new image, we can create a new container and check installed versions

1
2
3
4
5
sudo docker run --rm -it --user=node --entrypoint=/bin/bash kafebob/rpi-alpine-node
bash-4.3$ node --version
v8.2.0
bash-4.3$ npm --version
5.3.0

If you do not want to wait for compilation, I uploaded this image to Docker Hub so you can do a pull and create a container based
on Node 8.2.0.

Instead of build, just pull the image from Docker Hub.

1
sudo docker pull kafebob/rpi-alpine-node

Conclusions

At the moment of writing this article, Alpine has Node v6.10.3 binary available in ARM architecture.

  • If you do not care about node & npm versions just use a Dockerfile similar to the one described in Precompiled NodeJS section.
  • If version is important, the only way I have found it is compiling Node on your Pi.
    – You can use NVM and a Dockerfile similar to the one described in Compiling NodeJS section.
    – You could use version from Michael Hart, but I have not yet found a way to make it work.

Do you think there is any other alternative? Please let me know in comments.

Happy coding!

UPDATE 28-June-2017

@ganlub has point it me out about --virtual property in apk add. This option is a feature to cleanup packages after a completed setup. Packages added under this virtual name can then be removed as one group.

For instance, you could add your build dependencies like this apk --update add --virtual build-dependencies make gcc linux-headers and then at the end you could remove these dependencies like this apk del build-dependencies.

@ganlub also told me there is no need to keep build dependencies in my NVM Docker version from Compiling Node Section, he’s right but the reason to keep them was to make life easier if your project is going to use modules like imagemin, this module needs to compile optipng and jpegtran in order to be compatible with your system architecture. But it’s true, a node-base Docker image should be thinner as possible.

So then, I decided to update NVM Dockerfile.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
FROM kafebob/rpi-alpine:3.6

ENV NVM_VERSION=v0.33.2 NODE_VERSION=v8.2.1 PROFILE=/home/node/.bashrc

RUN apk add --update --no-cache curl bash \
ca-certificates openssl coreutils && \
apk add --update --no-cache --virtual build-dependencies ncurses python2 \
make gcc g++ libgcc linux-headers && \
addgroup -g 1000 node && \
adduser -u 1000 -G node -s /bin/bash -h /home/node -D node

USER node
RUN cd /home/node && \
curl -o- https://raw.githubusercontent.com/creationix/nvm/$NVM_VERSION/install.sh | bash && \
echo "#NVM Setup" >> $PROFILE && \
echo 'export NVM_DIR="$HOME/.nvm"' >> $PROFILE && \
echo '[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" # This loads nvm' >> $PROFILE && \
source $PROFILE && \
nvm install -s $NODE_VERSION --fully-static && \
rm -rf /home/node/.nvm/.cache/src/node-$NODE_VERSION

USER root
RUN apk del build-dependencies && \
rm -rf /tmp/* /var/cache/apk/*

With this Dockerfile you are going to get Node 8.2.1 and Npm 5.3.0 without building dependencies inside the container BUT!!! the image still has 230MB virtual size.

OMG! Why? Because the image depends on some layers and one of the layer is the build dependencies layer (line 5 from previous Dockerfile).

So I have a question, Is there a reason to delete build dependencies in line 23? At the end this image has almost same size as the image described here, where I didn’t care about delete dependencies.

Update 01-Aug-2017

After playing a bit with latest version, I have come to the conclusion that using Node through NVM with an user different than root is going to create more troubles than provide solutions. So I changed a bit the Dockerfile for kafebob/rpi-alpine-node, in this version I’m using just user root and the size of the image has decreased a lot, now 80MB because I’m using just one RUN command instead of three.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
FROM kafebob/rpi-alpine-base:3.6
MAINTAINER Luis Toubes <luis@toub.es>

ENV NVM_VERSION=v0.33.2 NODE_VERSION=v8.2.1 PROFILE=/root/.bashrc

RUN apk add --update --no-cache curl bash \
ca-certificates openssl coreutils && \
apk add --update --no-cache --virtual build-dependencies ncurses python2 \
make gcc g++ libgcc linux-headers && \
cd /root && \
curl -o- https://raw.githubusercontent.com/creationix/nvm/$NVM_VERSION/install.sh | bash && \
echo "#NVM Setup" >> $PROFILE && \
echo 'export NVM_DIR="$HOME/.nvm"' >> $PROFILE && \
echo '[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" # This loads nvm' >> $PROFILE && \
source $PROFILE && \
nvm install -s $NODE_VERSION --fully-static && \
rm -rf /root/.nvm/.cache/src/node-$NODE_VERSION && \
apk del build-dependencies && \
rm -rf /tmp/* /var/cache/apk/*

Still missing a step to use Node with any user but I’m working on it.