Jan 14, 2023

How to Package a Node.js Application with Docker

How to Package a Node.js Application with Docker

Docker is an open-source software running on Windows, Linux and MacOS, that enables developers to package their applications and dependencies together into a lightweight, standalone, and isolated unit called an image.

Creating a Node.js Application

Setting up the project

Let's start by creating a new directory for our application named app:

$ mkdir app
$ cd app

Let's initialize it using NPM:

$ npm init

And let's install the express package:

$ npm install --save express

Creating the server

Let's create a new file named server.js:

$ touch server.js

Within it, let's write a simple HTTP server using the Express framework that implements two routes:

  • A GET route that responds with an HTTP 200 "Hello World".
  • A fallback route that responds with an HTTP 404 "Not Found".
const express = require('express');

const app = express();

app.get('/', (req, res) => res.send('Hello World'));

app.all('*', (req, res) => res.sendStatus(404));

app.listen(3000);

We can now verify that everything works by running the server:

$ node server.js

And testing its endpoints in second terminal window:

$ curl 127.0.0.1:3000
Hello World

$ curl 127.0.0.1:3000/foo
Not Found

Creating a Dockerfile

A Dockerfile is a text file that contains all the necessary instructions for building a Docker image.

These instructions can be used for installing and configuring command line tools, declaring environment variables, copying files from the local environment, and so on.

Overview of the Dockerfile's instructions

In our case, the project's Dockerfile will:

  • Pull the node:16-alpine image.
  • Create a directory named app into which we'll copy our application's files.
  • Install the dependencies listed in the package.json file using NPM.
  • Start the server using the node utility.

Creating the Dockerfile

Let's start by creating a new file named Dockerfile within our project's directory:

$ touch Dockerfile

Setting up the base image

A valid Dockerfile always starts with a FROM instruction that initializes the bases image upon which the following instructions will be built on.

For this tutorial, we'll use the lightweight node:16-alpine image that comes pre-packaged with an installation of Node.js version 16 on top of the Alpine Linux distribution.

FROM node:16-alpine

Creating the application's directory

The WORKDIR instruction sets the working directory for any instructions that follow, and automatically creates it within the the filesystem of the container if it doesn't exist.

Let's use it to create the /app directory into which we'll copy the application's files.

WORKDIR /app

Copying the application's files

The COPY instruction copies files and directories from the local environment to the filesystem of the container.

Let's use it to copy all of the application's files at once using the dot syntax, which roughly translates to "copy everything from this local directory into the container's directory defined by WORKDIR".

COPY . .

Installing the application's dependencies

The RUN instruction executes any command using the container's default shell.

Let's use it to install the application's dependencies at build time with NPM.

RUN npm install

Exposing the application's port

The EXPOSE instruction informs Docker that the container listens on the specified network ports at runtime. Although this instruction has no effect, it is often used as a form of documentation by developers to indicate which port should be published when running the container.

EXPOSE 3000

Running the application

The CMD instruction sets the default command to be executed upon container startup.

Since we want our server to start as soon as the container is up and running, let's use it to execute the server script using the node utility.

CMD node server.js

The complete Dockerfile

# Pull Alpine Linux + Node.js v16
FROM node:16-alpine

# Create directory
WORKDIR /app

# Copy files
COPY . .

# Install dependencies
RUN npm install

# Document server port
EXPOSE 3000

# Start server
CMD node server.js

Note that the lines starting with a hash character # are comments and will be ignored at build time.

Creating a Dockerignore File

A .dockerignore file is similar to a .gitignore file as it allows us to specify which files and directories should be excluded from the build context when using an instruction such as COPY.

Since we don't want to copy the node_modules directory, nor the Dockerfile or the .dockerignore file itself when building the image, let's create a .dockerignore file:

$ touch .dockerignore

And add the following lines into it:

node_modules
Dockerfile
.dockerignore

Packaging the Application

Building the image

Now that we're all set, let's package our application into a Docker image using the docker build command:

$ docker build -t node-app .

Where:

  • The -t option flag is used to tag the image; which means giving it a name.
  • The dot . represents the Dockerfile directory path the image will be built from.

We can now verify that the image was properly built by running the docker images command, which will output the list of images present in our local environment.

$ docker images
REPOSITORY   TAG       IMAGE ID       CREATED             SIZE
node-app     latest    722d86989a20   About an hour ago   122MB

Running the container

To wrap it up, we can test our application by running a new container using the docker run command:

$ docker run -d -p 3000:3000 node-app

Where:

  • The -d option flag is used to launch the container in the background.
  • The -p option flag is used to connect the port 3000 of the local environment to the port 3000 of the container.

Finally, we can verify that everything is working as expected by running the following cURL commands:

$ curl 127.0.0.1:3000
Hello World

$ curl 127.0.0.1:3000/foo
Not Found

Related posts