Skip to main content

Command Palette

Search for a command to run...

How to Dockerize an Express App

A Step-by-Step Guide to Running Express.js in a Docker Container

Updated
10 min read
How to Dockerize an Express App

Introduction

Differences across development environments tend to cause compatibility issues for developers during deployment. Enter Docker, a platform that helps solve this problem by providing a consistent environment for developing and deploying applications, otherwise known as a container. A container is a running instance of a software package containing all the code, libraries, and dependencies required to run an application anywhere, whether it’s on a local machine, in the cloud, or on a production server.

The software package we use to run a container is called a Docker image. Think of this as a read-only snapshot of your code alongside everything else it needs to run, all packaged into one neat, (not so) little executable piece of software. You can then run this anywhere, at any time, and thanks to the isolation technology of containers, you can be certain it will work the same way, 100% of the time. A core Docker component called the Docker Engine handles the creation, management, and execution of Docker containers.

This tutorial provides a guide to containerizing an Express.js application in a development environment. In it, you will learn how to:

  • Set up a basic Express app.

  • Write a Dockerfile that contains the instructions for creating a Docker image.

  • Build an image of your application.

  • Run a container using that image.

And, as a bonus, you will also learn how to upload your image to Docker Hub where you can share it with other developers, just like you would with your code on GitHub.

Prerequisites

To complete this tutorial, you will need the following:

  • A local development environment for Node.js. You can download the latest stable version from the Node.js official website or install it using a version manager like nvm.

  • Basic familiarity with the Express framework. If you are new to Express, you can check out the official documentation to get started.

  • Docker installed on your machine. You can find specific OS instructions in the docs (no pun intended).

  • A Docker Hub account to upload and manage your Docker images.

  • Knowledge of basic command line operations. This guide provides a good starting point.

  • A code editor of your choice. Visual Studio Code is a very popular option.

Setting up the Application

The first step is to build a basic Express app to serve as the foundation for your project. Begin by creating a new directory to house all of the files and configurations, and then navigate into it:

mkdir express-docker-app
cd express-docker-app

Next, initialize a new project using npm (Node Package Manager for installing and managing software packages). This will generate a package.json file to keep track of your project’s metadata and dependencies. The -y flag in the command automatically answers “yes” to all setup questions and creates the package.json file with default settings. If, however, you would rather customize these settings, run the command without the flag:

npm init -y

Now you can install the Express framework with the command below. This will add it to the previously mentioned package.json file and also create a node_modules folder which stores all the libraries and frameworks your application requires:

npm install express

Create a new file named index.js to contain your application code:

touch index.js

Now open this file in your code editor and input the following code:

const express = require("express");
const app = express();

const PORT = 3000;

app.get("/", (req, res) => {
    res.send("Hello, Express App!");
});

app.listen(PORT, () => {
    console.log(`Server running on port ${PORT}`);
});

The code above creates a very basic Express app. We start by importing the Express framework we installed earlier with require(“express”) and then we initialize an instance with const app = express(). We use this instance to define a route app.get(“/”, …) that responds with a simple message when we access the server. And with app.listen(PORT, …), we are starting the server on the predefined port const PORT = 3000 and printing a message to the command line to let us know the server is running successfully.

Test your application by running the command below:

node index.js

If your app has been correctly set up, you should see the terminal output: Server running on port 3000.

Navigate to http://localhost:3000 on a web browser and the message “Hello, Express App!” will be displayed on the page:

Partial screenshot of a browser window showing "localhost:3000" in the address bar and "Hello, Express App!" on the page.

Close the server for now with Ctrl + C and move on to the next step.

Writing the Dockerfile

The next step is to create and set up a Dockerfile. This is a document outlining a set of instructions that tell Docker how to build a container image for your application. These instructions consist of a series of commands, with each one creating a new layer in the image. The layers are stacked on top of one another, and Docker caches them to allow for faster builds.

Create a Dockerfile in your project directory:

touch Dockerfile

Open this file in your code editor and input the following lines of code:

FROM node:22-alpine

FROM

This is the starting point for the image, and it tells Docker which base image to use in building the container. A base image is necessary because it establishes the environment by providing an operating system which is vital for running applications in a container. This is where the consistent environment mentioned earlier comes into play. By specifying a base layer, we ensure our app runs on the same OS and Node.js runtime, no matter where it is deployed.

In this instance, we are using the Alpine Linux version of Node.js 22. It is extremely small and lightweight (about 5MB) which means faster download, build, and start times.

WORKDIR /usr/src/app

WORKDIR

We use this command to set the working directory for subsequent instructions in the file. The path specified is where the following instructions (COPY, RUN, CMD, etc) will be executed from, and this can either be absolute or relative. If the directory you specify does not exist, Docker will create it for you. Here, we’re setting the path to /usr/src/app based on the Linux Filesystem Hierarchy according to the official Docker recommendation.

COPY package*.json ./

COPY

This copies files and directories from your local machine into the Docker image’s filesystem. It allows you to bring in code, configuration files, and any other resources your application needs to run.

Syntax: COPY <src> <dest>.

<src> specifies the source files or directories on your local machine, and <dest> is the destination path inside the container.

This is the first of two COPY instructions we will be using in our Dockerfile. Here, we are asking Docker to copy the package.json and package-lock.json files while using an asterisk (*) as a wildcard to specify both files at once.

RUN npm install

RUN

The RUN instruction executes commands during the image build process. It allows us to install software, run scripts, or perform other tasks inside the image while it is being built. Since we have a package.json file containing a list of our dependencies, we can install them by asking Docker to run the npm install command.

COPY . .

COPY

The second COPY instruction copies the rest of the project’s files and folders (except the .dockerignore file) all in one go.

“So why do we need two separate COPY instructions?” you might ask. Well, we’re doing this for optimization purposes, so Docker won’t have to reinstall the dependencies every time we make a change to our codebase and rebuild the image. You can learn more about this practice here.

EXPOSE 3000

EXPOSE

This indicates which network the container will listen on when it is running. It does not automatically make the container externally accessible; we do that with the —port or -p flag when we run the container. What EXPOSE does is serve as a declaration of the specific port (3000 in this case) that we would like to publish later on.

CMD ["node", "index.js"]

CMD

We use CMD to specify the default command to run when we start a container from the image. A Dockerfile can only have one CMD instruction. If you define more than one, only the last one will take effect. Since we are starting our Express app with node index.js, this is the command that we pass to CMD.

After entering all of these commands, this is what your Dockerfile should look like:

FROM node:22-alpine

WORKDIR /usr/src/app

COPY package*.json ./

RUN npm install

COPY . .

EXPOSE 3000

CMD ["node", "index.js"]

Note: When Docker executes the COPY . . instruction, it copies everything from the express-docker-app directory unless you use a .dockerignore file to exclude specific files or directories. Since running npm install automatically adds a node_modules folder to the image, we don’t want to overwrite it with that of our host machine.

Go ahead and create a .dockerignore file in the root directory:

touch .dockerignore

And add the node_modules folder to it:

node_modules/

Also note that the instructions used in our Dockerfile is by no means exhaustive. For the complete list of available instructions, as well as more information on those used in this tutorial, please see the official Dockerfile reference.

Building and running the Docker Image

With your Dockerfile all set, you are ready to build your image and run your app inside a container. Run the command below from your project directory:

docker build -t express-docker-app .

The docker build part of the command creates a Docker image based on the instructions in the Dockerfile, while the -t flag tags the image with the name express-docker-app for easy identification. The dot . specifies the current directory we are in as the build context, which means Docker will work with the Dockerfile along with all the other files in this directory during the build process.

After the build is complete, you should see a message like this at the top of the build output in your terminal:

Screenshot of a terminal showing the output of a "docker build" command.

This confirms your image was built successfully and tagged as express-docker-app:latest.

Now we can run a container from the image:

docker run -p 3000:3000 express-docker-app

With docker run, we are starting a new container based on the specified image, express-docker-app. And, as mentioned earlier when talking about the EXPOSE instruction in our Dockerfile, the -p flag maps port 3000 on your local machine to port 3000 in the container and publishes it.

When the container starts running, you should the same message logged to your terminal again: Server running on port 3000.

As you did before, open a web browser, head to http://localhost:3000 and again, you should see “Hello, Express App” displayed, confirming that your application is running inside a container.

And with that, you have successfully built and Dockerized an Express app!

Helpful Commands

To help you better manage your Docker containers, below are some helpful commands.

Display a list of images available on your local machine:

docker images

Display a list of active containers and their IDs:

docker ps

To start or stop an existing container, enter the corresponding command and replace container_id with the ID of the container from the previous output:

docker start|stop <container_id>

Remove a stopped container:

docker rm <container_name>

Uploading the Image to Docker Hub

Docker provides a platform called Docker Hub for uploading Docker images and you can do this in three easy steps:

Type the command below and enter your credentials when prompted:

docker login

Tag your image with your Docker Hub username and a name for the repository:

docker tag express-docker-app <your_dockerhub_username>/express-docker-app:latest

Upload the tagged image to Docker Hub:

docker push <your_dockerhub_username>/express-docker-app:latest

Docker will push your image to the registry and make it available for public use anywhere. You can visit your Docker Hub repository to confirm the successful upload of your image.

Conclusion

In this tutorial, you learned how to build and containerize an Express app using Docker. You created a Dockerfile to define your build specs, built a Docker image using that Dockerfile, and then ran your application in a Docker container. Finally, you uploaded the image to the Docker Hub registry and made it available to other developers around the world.

If you wish to improve your knowledge, you should consider looking into further aspects of the containerization process such as testing, debugging, network, and security.

I highly recommend exploring more advanced topics such as working with a multi-container setup using Docker Compose. This Digital Ocean tutorial on integrating MongoDB with your Node Application is a great place to start.