How To Build a Node.js Application with Docker

Introduction

The Docker platform allows developers to package and run applications as containers. A container is an isolated process that runs on a shared operating system, offering a lighter weight alternative to virtual machines. Though containers are not new, they offer benefits — including process isolation and environment standardization — that are growing in importance as more developers use distributed application architectures.

When building and scaling an application with Docker, the starting point is typically creating an image for your application, which you can then run in a container. The image includes your application code, libraries, configuration files, environment variables, and runtime. Using an image ensures that the environment in your container is standardized and contains only what is necessary to build and run your application.

In this tutorial, you will create an application image for a static website that uses the Express framework and Bootstrap. You will then build a container using that image and push it to Docker Hub for future use. Finally, you will pull the stored image from your Docker Hub repository and build another container, demonstrating how you can recreate and scale your application.

This file includes the project name, author, and license under which it is being shared. Npm recommends making your project name short and descriptive, and avoiding duplicates in the npm registry. We've listed the MIT license in the license field, permitting the free use and distribution of the application code.

Additionally, the file specifies:

"main": The entrypoint for the application, app.js. You will create this file next.

"dependencies": The project dependencies — in this case, Express 4.16.4 or above.

This will install the packages you've listed in your package.json file in your project directory.

We can now move on to building the application files.

Step 2 — Creating the Application Files

We will create a website that offers users information about sharks. Our application will have a main entrypoint, app.js, and a views directory that will include the project's static assets. The landing page, index.html, will offer users some preliminary information and a link to a page with more detailed shark information, sharks.html. In the views directory, we will create both the landing page and sharks.html.

First, open app.js in the main project directory to define the project's routes:

nano app.js

The first part of the file will create the Express application and Router objects, and define the base directory and port as constants:

The require function loads the express module, which we then use to create the app and router objects. The router object will perform the routing function of the application, and as we define HTTP method routes we will add them to this object to define how our application will handle requests.

This section of the file also sets a couple of constants, path and port:

path: Defines the base directory, which will be the views subdirectory within the current project directory.

The router.use function loads a middleware function that will log the router's requests and pass them on to the application's routes. These are defined in the subsequent functions, which specify that a GET request to the base project URL should return the index.html page, while a GET request to the /sharks route should return sharks.html.

Finally, mount the router middleware and the application's static assets and tell the app to listen on port 8080:

The top-level navbar here allows users to toggle between the Home and Sharks pages. In the navbar-nav subcomponent, we are using Bootstrap's active class to indicate the current page to the user. We've also specified the routes to our static pages, which match the routes we defined in app.js:

In addition to setting font and color, this file also limits the size of the images by specifying a max-width of 80%. This will prevent them from taking up more room than we would like on the page.

Save and close the file when you are finished.

With the application files in place and the project dependencies installed, you are ready to start the application.

If you followed the initial server setup tutorial in the prerequisites, you will have an active firewall permitting only SSH traffic. To permit traffic to port 8080 run:

sudo ufw allow 8080

To start the application, make sure that you are in your project's root directory:

cd ~/node_project

Start the application with node app.js:

node app.js

Navigate your browser to http://your_server_ip:8080. You will see the following landing page:

Click on the Get Shark Info button. You will see the following information page:

You now have an application up and running. When you are ready, quit the server by typing CTRL+C. We can now move on to creating the Dockerfile that will allow us to recreate and scale this application as desired.

Step 3 — Writing the Dockerfile

Your Dockerfile specifies what will be included in your application container when it is executed. Using a Dockerfile allows you to define your container environment and avoid discrepancies with dependencies or runtime versions.

Following these guidelines on building optimized containers, we will make our image as efficient as possible by minimizing the number of image layers and restricting the image's function to a single purpose — recreating our application files and static content.

In your project's root directory, create the Dockerfile:

nano Dockerfile

Docker images are created using a succession of layered images that build on one another. Our first step will be to add the base image for our application that will form the starting point of the application build.

Add the following FROM instruction to set the application's base image:

~/node_project/Dockerfile

FROM node:10-alpine

This image includes Node.js and npm. Each Dockerfile must begin with a FROM instruction.

By default, the Docker Node image includes a non-root node user that you can use to avoid running your application container as root. It is a recommended security practice to avoid running containers as root and to restrict capabilities within the container to only those required to run its processes. We will therefore use the node user's home directory as the working directory for our application and set them as our user inside the container. For more information about best practices when working with the Docker Node image, see this best practices guide.

To fine-tune the permissions on our application code in the container, let's create the node_modules subdirectory in /home/node along with the app directory. Creating these directories will ensure that they have the permissions we want, which will be important when we create local node modules in the container with npm install. In addition to creating these directories, we will set ownership on them to our node user:

If a WORKDIR isn't set, Docker will create one by default, so it's a good idea to set it explicitly.

Next, copy the package.json and package-lock.json (for npm 5+) files:

~/node_project/Dockerfile

...
COPY package*.json ./

Adding this COPY instruction before running npm install or copying the application code allows us to take advantage of Docker's caching mechanism. At each stage in the build, Docker will check to see if it has a layer cached for that particular instruction. If we change package.json, this layer will be rebuilt, but if we don't, this instruction will allow Docker to use the existing image layer and skip reinstalling our node modules.

To ensure that all of the application files are owned by the non-root node user, including the contents of the node_modules directory, switch the user to node before running npm install:

~/node_project/Dockerfile

...
USER node

After copying the project dependencies and switching our user, we can run npm install:

~/node_project/Dockerfile

...
RUN npm install

Next, copy your application code with the appropriate permissions to the application directory on the container:

~/node_project/Dockerfile

...
COPY --chown=node:node . .

This will ensure that the application files are owned by the non-root node user.

Finally, expose port 8080 on the container and start the application:

~/node_project/Dockerfile

...
EXPOSE 8080
CMD [ "node", "app.js" ]

EXPOSE does not publish the port, but instead functions as a way of documenting which ports on the container will be published at runtime. CMD runs the command to start the application — in this case, node app.js. Note that there should only be one CMD instruction in each Dockerfile. If you include more than one, only the last will take effect.

Before building the application image, let's add a .dockerignore file. Working in a similar way to a .gitignore file, .dockerignore specifies which files and directories in your project directory should not be copied over to your container.

If you are working with Git then you will also want to add your .git directory and .gitignore file.

Save and close the file when you are finished.

You are now ready to build the application image using the docker build command. Using the -t flag with docker build will allow you to tag the image with a memorable name. Because we are going to push the image to Docker Hub, let's include our Docker Hub username in the tag. We will tag the image as nodejs-image-demo, but feel free to replace this with a name of your own choosing. Remember to also replace your_dockerhub_username with your own Docker Hub username:

docker build -t your_dockerhub_username/nodejs-image-demo .

The . specifies that the build context is the current directory.

It will take a minute or two to build the image. Once it is complete, check your images:

It is now possible to create a container with this image using docker run. We will include three flags with this command:

-p: This publishes the port on the container and maps it to a port on our host. We will use port 80 on the host, but you should feel free to modify this as necessary if you have another process running on that port. For more information about how this works, see this discussion in the Docker docs on port binding.

With your container running, you can now visit your application by navigating your browser to http://your_server_ip. You will see your application landing page once again:

Now that you have created an image for your application, you can push it to Docker Hub for future use.

Step 4 — Using a Repository to Work with Images

By pushing your application image to a registry like Docker Hub, you make it available for subsequent use as you build and scale your containers. We will demonstrate how this works by pushing the application image to a repository and then using the image to recreate our container.

The first step to pushing the image is to log in to the Docker Hub account you created in the prerequisites:

docker login -u your_dockerhub_username

When prompted, enter your Docker Hub account password. Logging in this way will create a ~/.docker/config.json file in your user's home directory with your Docker Hub credentials.

You can now push the application image to Docker Hub using the tag you created earlier, your_dockerhub_username/nodejs-image-demo:

docker push your_dockerhub_username/nodejs-image-demo

Let's test the utility of the image registry by destroying our current application container and image and rebuilding them with the image in our repository.

Visit http://your_server_ip once again to view your running application.

Conclusion

In this tutorial you created a static web application with Express and Bootstrap, as well as a Docker image for this application. You used this image to create a container and pushed the image to Docker Hub. From there, you were able to destroy your image and container and recreate them using your Docker Hub repository.

If you are interested in learning more about how to work with tools like Docker Compose and Docker Machine to create multi-container setups, you can look at the following guides:

Tutorial Series

In this series, you will build and containerize a Node.js application with a MongoDB database. The series is designed to introduce you to the fundamentals of migrating an application to Kubernetes, including modernizing your app using the 12FA methodology, containerizing it, and deploying it to Kubernetes. The series also includes information on deploying your app with Docker Compose using an Nginx reverse proxy and Let's Encrypt.

November 28, 2018

Interested in Node.js but not sure where to start with Docker? This tutorial will walk you through the first step: building an image for a Node.js application and creating a container from it. We will also walk you through pushing that image to Docker Hub and using the saved image to recreate your application container.

February 7, 2019

In the process of working with Node.js, you may find yourself developing a project that stores and queries data. This tutorial will show you how to integrate MongoDB with an existing Node application. This process will involve adding user and database information to your application code and using the Object Document Mapper Mongoose. At the end of the tutorial, you will have a working application that will take a user's input and display it in the browser.

February 26, 2019

If you are actively developing an application, Docker can simplify your workflow and the process of deploying your application to production. This tutorial will show you how to set up a development environment for a Node.js application using Docker. You will create two containers — one for the Node application and another for the MongoDB database — with Docker Compose. At the end of this tutorial, you will have a working shark information application running on Docker containers.

March 22, 2019

This tutorial will show you how to migrate your Docker Compose workflow to Kubernetes using kompose. You will start by creating a single-instance setup for a Node.js application with a MongoDB database on a Kubernetes cluster. You will then scale this setup to include multiple replicas of your application and database.

April 18, 2019

Kubernetes is a system for running modern, containerized applications at scale. With it, developers can deploy and manage containerized applications across clusters of machines. When creating multi-service deployments with Kubernetes, many developers opt to use the Helm package manager. In this tutorial, you will deploy a replicated Node.js application with a MongoDB database using Helm charts, which will be a good starting point for building other resilient and fault-tolerant applications.

December 18, 2018

Secure your containerized Node.js application by following this tutorial, which shows you how to deploy a Node.js application with an Nginx reverse proxy using Docker Compose. You will obtain certificates for your application domain with Let's Encrypt and ensure that your application receives a high security rating.