Jan 14, 2023
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 port3000
of the local environment to the port3000
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