Getting my feet wet with Node.js in Docker

This weekend, I decided I wanted to learn how to package up and deploy a node app using Docker. Here are my notes. Let me know if you find them useful!

This guide assumes you’re using a Mac, but most commands should work with Linux, or be easily translatable to Windows.

One last thing: These are steps to get started with Docker for development, and are not recommendations for what you’d want to do for production deployment.

First steps

  1. Download and install the Docker Toolbox for your system.
  2. Follow prompts to create a default docker machine. I used the CLI.
  3. I highly recommend you get used to some docker commands and terminology by following the Hello world in a container user guide.
  4. For your docker commands to work properly, you need to export some docker environment variables. Add the following to your shell startup file (~/.bash_profile on mac):
eval $(docker-machine env)

Handy commands

  • docker images lists the images you’ve pulled or created locally.
  • docker ps lists any running containers (instances of images).
  • docker-machine ls lists your existing docker machines (runtimes). You only need the default machine for this guide.

Preparing your application

If you don’t have a node application in mind you want to dockerize, you could first build a small hapi.js service by following this tutorial.

If you already have a node application, make sure you have defined an npm start script in your project’s package.json:

{
"scripts": {
"start": "node index.js"
},
"dependencies": {
...
}
}

Note: You may want to use a process monitoring tool like forever or PM2 rather than relying on the raw node executable to run your app.

The next step is to create a Dockerfile in the root of your project. This describes what image you want to base your custom image FROM, what files you want to add to the image, as well as build- and installation steps to prepare your application to be run. There should only be one CMD defined to start your app.

I use a specific node version image instead of the node:onbuild image. This gives me more control over the build steps when creating the image. At first, I was confused when I couldn’t get the default 4.2 onbuild dockerfile to run npm install successfully. That’s because ONBUILD is a trigger for when the image is used in a FROM statement in a different Dockerfile (and not a command to execute at build time). Here is a working Dockerfile for the node:4.2.6 image:

FROM node:4.2.6
EXPOSE 3000
RUN mkdir -p /usr/src/app
ADD . /usr/src/app
WORKDIR /usr/src/app
RUN npm install
CMD [ "npm", "start" ]

EXPOSE serves as a hint to Docker that the application is running on port 3000. See below for how we map the dockerized port to a local port.

See https://github.com/docker-library/docs/tree/master/node for all available node images to base your image on.

Also create a .dockerignore file to ensure the image doesn’t contain extraneous files like your .git history. Ignore node_modules, since your you may have platform-dependent modules installed that need to be reinstalled for the Linux-based node container.

.git
node_modules

Note: If I were specifying a production image, I wouldn’t .dockerignore node_modules. What I’d instead do is build a canonical image in an environment that matches production; include node_modules, run the tests on it, and then copy the image as an artifact to be run on the production machine, with no RUN npm install step.

Build your Docker image!

docker build will build an image based on the Dockerfile. Give your image a memorable name by replacing my-node-app below.

docker build -t my-node-app .

Run your Docker image!

To run your application in an interactive shell, remove the container (image instance) upon finishing, and map internal port 3000 to your host’s port 8080:

docker run -it --rm -p 8080:3000 my-node-app

To run your application as a daemon:

docker run -d -p 8080:3000 my-node-app

Try your docker app!

# `docker-machine ip` outputs the IP address of the docker runtime.
open http://`docker-machine ip`:8080

In closing

As purely a development setup, the advantages of Docker aren’t apparent – especially if you’re not on a large development team. However, the ability to precisely package up your whole development environment in a sharable image can be a huge boon to a team where onboarding and support can otherwise take up too much time.

Where Docker really shines is when you start considering deployment to various environments. The Docker runtime takes care of interfacing with the OS, and all you have to worry about (in theory) is building your image.

This is just the beginning! I’ve signed up for a Codeship Docker Platform trial, and will be setting up a whole continuous delivery pipeline for my app. I will be writing more posts as I keep exploring.

More reading