Dockerizing Node.js Applications

Written by: Gergely Nemeth
5 min read

Containers are one of the best ways to deploy Node.js applications these days. In this post, you will learn how you can containerize your Node.js applications using Docker.

First, we will create a simple web application in Node.js, then we'll build the Docker image and run it. You'll also learn a few tips and tricks to make development easier and faster.

Prerequisites

Before starting this tutorial, make sure you have the following installed on your computer:

Creating the Node.js Application

For the sake of simplicity, we're going to build a small application, which serves JSON over HTTP. For this, let's go with Express.

First, let's create the package.json with npm init --yes. Since we're using Express, let's add it as a dependency using npm install express. Starting with NPM version 5 or higher, this is equivalent to npm install express --save.

Once we've created the package.json and our dependency, it's time to create the Node.js application:

// index.js
const express = require('express')
const app = express()
// yes, you can use async functions with express this easily!
app.get('/', async function (req, res) {
  res.json({
    status: 'ok'
  })
})
app.listen(3000)

You can test the application by running it with node index.js and hit localhost:3000 with curl or your browser.

Creating the Dockerfile

Docker uses its syntax to describe images that will be built. These instructions are located in the Dockerfile.

The first line of the Dockerfile defines what is the base image that your image will build upon. For us, it's important that it includes the Node.js binary. You can get a list of the official images from https://hub.docker.com/_/node/.

For this tutorial, we're going to pick a base image built on top of Alpine, which is a security-oriented, lightweight Linux distribution based on musl libc and BusyBox. To do so, we have to add the following line: FROM node:8.6.0-alpine.

Next, we have to set the working directory for the Docker image: WORKDIR /usr/src/app.

Once we have it, it's time to install our application's dependencies. To do so, you have to copy to the image the package.json file as well as the package-lock.json if you are using that.

# Install app dependencies
COPY package.json .
# uncomment if using npm 5 or newer
# COPY package-lock.json .
RUN npm install

Now as we have the application's dependencies installed, we can move over the source of the application itself: COPY . ..

The next thing we have to do is to expose our application to the outside world. We can do that with the following instruction: EXPOSE 3000.

The last step is to start the application itself: CMD node index.js.

Your Dockerfile should now look like this:

FROM node:8.6.0-alpine
WORKDIR /usr/src/app
COPY package.json .
COPY package-lock.json .
RUN npm install
COPY . .
EXPOSE 3000
CMD node index.js

Using the .dockerignore File

When you are developing applications, remember that log files, generated files, or dependencies get added to the working directory. These files and directories are usually ignored in version control systems. You can and should do the same with Docker as well, skipping node_modules and log files.

You just have to create a .dockerignore file and add the files you want to exclude, like:

node_modules
npm-debug.log

Building Your Image

The next thing you want to do is to finally build the image using the Dockerfile and the .dockerignore file you have just created.

docker build -t <your username>/<app name> .

Once you've built your images, you can list all the available images on your system using docker images.

!Sign up for a free Codeship Account

Run the Built Image

Showtime! To run the image, you have to redirect the port you exposed to your host machine, as well as run the image in detached mode. To do so, run the following command:

docker run -p 3000:3000 -d <your username>/<app name>

Once you execute this command, the application will be accessible from the host system. Try hitting localhost:3000 with curl or your browser.

To get a list of all running Docker images, you can run docker ps. This will list the running images, as well as their container ID. This ID is needed if you have to kill the running images, or if you'd like to go inside the container.

To go inside the running image, you can run docker exec -it <container id> /bin/sh.

Tips and Tricks

There are a few tips and tricks that can help you a lot when working with Docker images. I'd like to share the two that I found the most useful in the past years.

Caching node_modules

When we created the Dockerfile, you could have asked yourself: why not copy everything over and run the npm install after? What's the reason for doing it in two separate steps?

The reason is that Docker caches layers, and layers only get rebuilt if the files on which they were built are changed. So if you're using a wildcard like ADD . /opt/app, npm install will be run even for a small change in your application's code.

To save this time and cache node_modules, we move our application's source in different steps. You can learn more about it here.

Tag Docker images when building

When you're shipping your application to production, it's a good practice to tag images with semver. To do so, you can modify the Docker build command in the following way:

docker  build -t docker build -t <your username>/<app name>:<app version> .

Preferably, you'll run this command in your CI/CD pipeline and not on your local machine. You should also combine it with a bumping version in your package.json to keep that in sync as well.

Stay up to date

We'll never share your email address and you can opt out at any time, we promise.