How to Dockerize a Node.js application

How to Dockerize a Node.js application

Node.js and Docker. If you haven't spent the last decade in an underwater cave playing solitaire on a coral bed, you must have at least heard of these two ever-trending techs in the web development industry. In this article, we'll merge both and show you how to write a Dockerfile for a Node.js application, build a Docker image of it, and push the image to a Docker registry.

Why should I Dockerize my Node.js app?

You might've heard about the whole Docker thing, but that still doesn't answer the question of why you should bother at all. Well, here's why:

  • A Docker image is a self-contained unit that bundles the app with the environment required to run it. No more installing libraries, dependencies, downloading packages, messing with config files, etc. If your machine supports Docker, you can run a Dockerized app, period.
  • The working environment of the application remains consistent across workflows and machines. This means the app runs exactly the same for developer, tester, and client, be it on development, staging, or production server.

In short, it's is the modern-day counter-measure for the age-old "strange, it works for me!" response in software development.

Tip
If you're not sure that you fully understand the idea of Docker containers, watch this short video by TechSquidTV to get up to speed:

Part 1: Create Node.js app

Assuming this will be your first time with the blue whale, our first step will be creating a simple Hello World app, a staple of every web developer's portfolio. The app will use Express, one of the most popular Node.js frameworks.

Tip
If you're familiar with the basics of Node.js, you can jump ahead to the part to the meat and potatoes of this arcile and use the ready app from our repository as the base.
Hint
You can also sign up to Buddy with your Git provider and select repository with your Node app:

Install Node.js and npm

If you've never worked with Node.js before, start by installing npm – the Node.js package manager. Click here to choose the right version for your environment.

Tip
Node.js is bundled with npm, so one installation gets you both of them.

Initialize Project and Install Express.js

Create a new directory for the app and initialize a Node project:

bash
mkdir helloworld cd helloworld npm init$$$

When asked for the details of the application, set the name to helloworld. For other options, just confirm the default values with enter.

Success
If you are lazy value your time, run npm init --yes to create a project instantly with default settings.

Npm will create a new package.json file that will hold the dependencies of the app. Let's add the Express framework as the first dependency:

bash
npm install express --save$

The new file should look like this now:

js
{ "name": "helloworld", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "express": "^4.18.1" } }

TableMain of Hello World

With everything installed, we can create an index.js file with a simple HTTP server that will serve the Hello World app:

js
//Load express module with `require` directive var express = require('express') var app = express() //Define request response in root URL (/) app.get('/', function (req, res) { res.send('Hello World!') }) //Launch listening server on port 8081 app.listen(8081, function () { console.log('app listening on port 8081!') })

Run the app

The application is ready to launch:

bash
node index.js$

Go to http://localhost:8081/ in your browser to view it.

Part 2: How do you Dockerize Node.js appplication?

Tip
Make sure to follow other Docker tutorials in our guides section

Every application requires a specific working environment: applications, dependencies, databases, libraries, everything in a specific version. Docker allows you to create such environments and pack them into a container.

Contrary to a VM, the container doesn't hold the whole operating system — just the applications, dependencies, and configuration. This makes Docker containers much lighter and faster than regular VMs.

In this part of the guide, we will build a Docker container with the Hello World app, and then launch the app in a Docker container.

Tip
If you didn't configure the app in the first part of the guide, you can fork a working Node project from our repository on GitHub.

Install Docker

First, install Docker on your machine:

A word on Docker images and what is Dockerfile for a Node.js application?

The Docker container is launched from a Docker image, a template with the application details. The Docker image is created with instructions written in the Dockerfile. Each line in the Dockerfile is a separate instruction run from top to bottom (the order is important for proper optimization).

A Dockerfile for a Node.js application is a text document that contains all the commands and instructions needed to build a Docker image specifically for running a Node.js application. It specifies the base image to start from, sets up the environment, installs dependencies, copies application code into the image, and defines how to run the application within a Docker container.

Here's an example:

Dockerfile
# Use an official Node.js image as the base image FROM node:18 # Set the working directory in the container WORKDIR /app # Copy package.json and package-lock.json to the working directory COPY package*.json ./ # Install npm dependencies RUN npm install # Copy the rest of the application code to the working directory COPY . . # Expose the port on which the Node.js application will run EXPOSE 3000 # Command to start the Node.js application CMD ["node", "app.js"]

5 best practices to containerize Node.js app with Docker

Before we get down to business, let's talk about some good practices for creating Dockerfiles, which you'll then use to build Docker images with your application.

1. Avoid the latest tag, use explicit image references

While using images tagged latest in Dockerfiles might seem like a no-brainer, it's a better idea to fight the urge to stay up-to-date and explicitly reference the image you want to use. Why? The latest tag doesn't always mean the most recent. More importantly, using explicit references guarantees that every instance of the app uses exactly the same code as its base and as such will work the same way.

For deterministic builds, use specific image version tags or image SHA256 aliases:

dockerfile
FROM node:18-alpine3.19 FROM node@sha256:affdf979bd8ec516bf189d451b8ac68dd50adc49adc4c4014963556c11efeda4

2. Choose smaller base images

When it comes to builds and pulls, smaller is always better. Faster download times, less data used, quicker builds. Additionally, large images often bundle in a lot of things that your app might not even need to run. The more things bundled in the image, the more you're exposed to security vulnerabilities.

If you want things quicker and safer, choose "diet" variants of images tagged slim or alpine. They're just as good as the full-fat ones!

3. Install production dependencies only

Why install devDependencies if you're Dockerizing a finished application? Avoid npm install, yarn install or npm ci - use this command to install production dependencies only:

bash
npm ci --only=production$

4. Clear cache and keep downsizing

Back on the topic of downsizing - it's a good idea to wipe the local cache after installing dependencies in a container. As Docker images are immutable, the cache won't be used to make future installations faster because... Well, there won't be any future installations.

Use this command to shave off as much as 50% off the size of the final image size:

bash
npm cache clean --force$

In a Dockerfile, add this command to the line that installs the dependencies:

bash
npm ci --only=production && npm cache clean --force$

5. Optimize for production

Some Node frameworks and libraries enable production optimization only with the NODE_ENV variable set to production. Include this in the second line of your Dockerfile:

bash
ENV NODE_ENV production$

Write Dockerfile for Node.js application

Add a Dockerfile to the directory with your application and configure the lines as described below.

Line 1: We shall use the lightweight official Node.js image with Node v12 as the template for our image

Hint
You can share images using image registries. In this example we'll use Docker Hub, the most popular one.
dockerfile
FROM node:12-alpine3.14

Line 2: Set the working directory in the container to /app. We shall use this directory to store files, run npm, and launch our application:

dockerfile
WORKDIR /app

Lines 3-5: Copy the application to the /app directory and install dependencies. If you add the package.json first and run npm install later, Docker won't have to install the dependencies again if you change the package.json file. This results from the way the Docker image is being built (layers and cache), and this is what we should do:

dockerfile
COPY package.json /app RUN npm ci --only=production && npm cache clean --force COPY . /app

Line 6: This line describes what should be executed when the Node Docker image is launching. What we want to do is to run our application:

dockerfile
CMD node index.js

Line 7: Expose port 8081 to the outside once the container has launched:

dockerfile
EXPOSE 8081

Summing up, the whole Dockerfile should look like this:

dockerfile
FROM node:12-alpine3.14 WORKDIR /app COPY package.json /app RUN npm ci --only=production && npm cache clean --force COPY . /app CMD node index.js EXPOSE 8081
Tip
If you'd like to deepen your knowledge on writing Dockerfiles, check out this excellent guide on the Docker's website.

Build Docker image

With the instructions ready, all that remains is to run the docker build command, set the name of your image with -t parameter, and choose the directory where the Dockerfile is located:

bash
docker build -t hello-world .$

Run Docker container

The application has been baked into the image. Dinner time! Execute the following command to launch the container and publish it on the host with the same port 8081:

bash
docker run -p 8081:8081 hello-world$

Share Docker image

Docker images can be hosted and shared in special image registries. The most popular is Docker Hub, a GitHub among Docker registries, in which you can host private and public images. Let's push Docker image to registry (e.g. Docker Hub):

  1. Sign up at Docker Hub
  2. Build the image again using your Docker Hub credentials:
bash
docker build -t [USERNAME]/hello-world .$
  1. Log in to Docker Hub with your credentials:
bash
docker login$
  1. Push the image to Docker Hub:
bash
docker push [USERNAME]/hello-world$

Image loading...Docker image on Docker Hub Congratulations! You can now use the image on any server or PC with Docker installed:

bash
docker run [USERNAME]/hello-world$

Please mind the image needs to be downloaded on the first run which may take some time depending on your connection.

Deploy a Dockerized Node.js app to server

Launching a Docker image on the server is as simple as running docker pull command and docker run in the desired location. However, whenever you update your application, it requires you to repeat the following steps:

  1. Test if the application is free of errors
  2. Build the Docker image
  3. Push the image to the registry
  4. Pulling and running the image on the server

These steps are simple, but can be time-consuming and prone to errors if done manually. The whole process, however, can be streamlined to a repository push with a properly configured pipeline. With Docker layer caching included in every plan, from free to premium, Buddy gets the job done in a blink of an eye, allowing you to track the progress of your project with full history of every pipeline execution. Get started with a free account and make your build process a breeze!

Happy coding!

Jarek Dylewski

Jarek Dylewski

Customer Support

A journalist and an SEO specialist trying to find himself in the unforgiving world of coders. Gamer, a non-fiction literature fan and obsessive carnivore. Jarek uses his talents to convert the programming lingo into a cohesive and approachable narration.