Run NodeJS anywhere using Docker and Nexe

A project I’m working on currently requires me to build a server side agent daemon. Due to the nature of the agent, a live connection requires to be open to the managing node server, using websockets. PHP is not very well suited to this kind of workload, so the obvious choice was to develop it using NodeJS. I will build a Docker image containing the application, as well as provide a standalone binary for people who don’t have Docker, but also don’t want the hassle of installing NodeJS.

For the purposes of this tutorial, or walk-trough if you will, we will create a very, very simple NodeJS application, which prints “Hello world from NPM”. And I will do it only with available Docker images - apart from them, there is no need to install node, npm or any other software on the server you’ll be working. All you need is Docker.

Notice: Hey there. I noticed that this post has been getting a bit of attention. If you want to update your DevOps skills with Docker and 12 Factor apps, consider purchasing 12 Factor Apps with Docker and Go. The techniques presented in the book also apply to NodeJS!

For your application, create a folder structure like this:

app/
app/package.json
app/index.js

The index.js file should contain only one line for this test, but basically, this is where you would write your application.

console.log("Hello world from NPM");

and the other file, package.json, should contain something like this:

{
  "name": "jstest",
  "version": "0.0.1",
  "scripts": {
    "start": "node index.js"
  }
}

The settings under scripts enable us to run the application with a common command: npm start. This allows us to use any filename in the app, so we don’t need to know about index.js when building a Docker image.

The first thing we should do is to run the application from a Docker image. There is an official build of node on Docker Hub, and we will use it to run the application.

# docker run -it --rm --name titpetric-jstest -v `pwd`/app:/app -w /app node:0.10 npm start

> jstest@0.0.1 start /app
> node index.js

Hello world from NPM

So, just in case you didn’t know before, this is how you run a Docker image containing NodeJS, which runs your application. Just to make things slightly easier for running on various machines, we should still build a Docker image. In the root folder, the one which contains the folder app, create a file named Dockerfile. I based mine of a gist I found here.

FROM ubuntu:14.04
MAINTAINER Tit Petric "tit.petric@monotek.net"

ENV DEBIAN_FRONTEND noninteractive

RUN apt-get -qq update
RUN apt-get install -y nodejs npm
RUN apt-get clean

# fucking debian installs `node` as `nodejs`
RUN update-alternatives --install /usr/bin/node node /usr/bin/nodejs 10

VOLUME ["/app"]

ADD ./app /app
RUN cd /app && npm install

EXPOSE 8888

WORKDIR /app

CMD ["npm", "start"]

This Dockerfile is basically a recipe, which will create a Docker image with all the required dependencies, and will run your application on startup. Now that you have created this file, we can build a Docker image using the following command:

# docker build -t titpetric/jstest:latest --rm .
Sending build context to Docker daemon 6.656 kB
Sending build context to Docker daemon 
Step 0 : FROM ubuntu:14.04
 ---> 91e54dfb1179
Step 1 : MAINTAINER Tit Petric "tit.petric@monotek.net"
 ---> Running in 527fe008fe3f
 ---> 94e1c43c8127
Removing intermediate container 527fe008fe3f
Step 2 : ENV DEBIAN_FRONTEND noninteractive
 ---> Running in cb97d620c92a
 ---> 89f31baa553b
Removing intermediate container cb97d620c92a
Step 3 : RUN apt-get -qq update
 ---> Running in cb96c9e58976
 ---> 2194977f7249
Removing intermediate container cb96c9e58976
Step 4 : RUN apt-get install -y nodejs npm
 ---> Running in 3d853372c7fe
[snip]

The docker build might take a few minutes, depending on how fast it will download all the packages (node has about 100+ packages as dependencies). After the docker build is finished, your docker image is created - you can list images using docker images.

# docker images
REPOSITORY          TAG       IMAGE ID         CREATED           VIRTUAL SIZE
titpetric/jstest    latest    9c9bd8d9c9f1     40 seconds ago    361.8 MB

So, the image was created. Lets run it!

# docker run -it --rm titpetric/jstest

> jstest@0.0.1 start /app
> node index.js

Hello world from NPM

Success!

But, providing a 360mb docker image for a simple “Hello world” application tells us that this might not be the best use case for docker. You can package your application in this way, but it doesn’t really shine as the best example of using Docker.

Thankfully, we have an alternative: nexe

Nexe is a command-line utility that compiles your Node.js application into a single executable file.

Lets use the Docker nexe image to try and build our app:

# docker run -it --rm --name jstest-nexe-build -v `pwd`/app:/app -w /app asbjornenge/nexe-docker -i index.js -o app.bin
----> bundle /app/index.js
----> bundle -> /tmp/nexe/latest/node-v0.10.26/lib/nexe.js
----> make
[snip long text]
  LINK(target) /tmp/nexe/latest/node-v0.10.26/out/Release/node: Finished
make[1]: Leaving directory `/tmp/nexe/latest/node-v0.10.26/out'
ln -fs out/Release/node node
----> cp /tmp/nexe/latest/node-v0.10.26/out/Release/node /app/app.bin
# ./app/app.bin 
Hello world from NPM

Success! The application was successfully packaged, weighing in at about 11MB, which is a great deal less than the Docker image. And we have 3 different ways to run our agent application on any number of servers:

  1. We can install NodeJS on servers, dependencies and our application with it,
  2. We can install our application as a single binary built by nexe,
  3. We can install our application as a Docker image and run a container

Final notes - using Docker instead of a package manager is not the best thing. The Docker image for node is about 600MB. If you’re sensitive about space and run a double digit number of distinct Docker images, it’s worth keeping an eye on disk usage.

The plus side of using Docker instead of a package manager is obvious - it is incredibly clean. If you don’t use something, just wipe the container and the image, free up space instantly - and never worry if you need specific libraries for one piece of software, and other versions of the same libraries for another.

I still think it’s a bit of an elaborate way to achieve something, but when you want that something, whatever it is, to be repeatable, then it seems to be worth the effort very quickly.

While I have you here...

It would be great if you buy one of my books:

I promise you'll learn a lot more if you buy one. Buying a copy supports me writing more about similar topics. Say thank you and buy my books.

Feel free to send me an email if you want to book my time for consultancy/freelance services. I'm great at APIs, Go, Docker, VueJS and scaling services, among many other things.