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:
Node.js 8.5 or greater (you can download from nodejs.org or use NVM)
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.