DEV Community

Cover image for Node.js and Docker: Write, Build and Publish
Ahmed A. Elkhalifa
Ahmed A. Elkhalifa

Posted on

Node.js and Docker: Write, Build and Publish

Overview

In this tutorial we aim to create a simple Node.js app with Express.js and containerize it with Docker then publish it to Docker Hub.

NOTE: This will be a practical tutorial so I will not be explaining any concepts such as Node.js, containerization or Docker. And I will also assume you already have node and docker installed on your machine. But if you want me to write an article explaining those concepts, let me know in the comments.


Sections

  1. Write the Node.js App
  2. Containerize the App with Docker
  3. Publish the Image to Docker Hub

1. Write the Node.js App

We will first create a simple Node js app that we will then work with. Follow these steps to create the app:

1. Create a new directory for the project
Create a new directory for the project with whatever name you like

$ mkdir nodejs_docker_tutorial
Enter fullscreen mode Exit fullscreen mode

and cd into it

$ cd nodejs_docker_tutorial
Enter fullscreen mode Exit fullscreen mode

2. Initialize the project
I'm gonna be using npm but you can use whatever package manager that suits you. To initialize the project with npm run:

$ npm init
Enter fullscreen mode Exit fullscreen mode

Fill out the information and set entry point to be src/app.js
the final package.json should be something like

{
  "name": "nodejs_docker_tutorial",
  "version": "1.0.0",
  "description": "",
  "main": "src/app.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}
Enter fullscreen mode Exit fullscreen mode

3. Install the packages
We are going to need these packages:
express, cors and (as an optional dev dependency) nodemon
run:

$ npm i express cors
$ npm i -D nodemon # Optional
Enter fullscreen mode Exit fullscreen mode

4. Create the files and directories
Create the following files and directories so that the project tree should look like:

.
├── package.json
├── package-lock.json
└── src
    ├── app.js
    └── routes
        └── home.js
Enter fullscreen mode Exit fullscreen mode

5. Use nodemon to watch for changes (Optional)
Installing and using nodemon is optional and I've included it in the tutorial just as an example to simulate a real-life scenario.
In the scripts section in package.json add the dev script as follows:

...
"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "nodemon src/app.js"
  },
...
Enter fullscreen mode Exit fullscreen mode

and now in a terminal session you can run:

$ npm run dev
Enter fullscreen mode Exit fullscreen mode

and it will watch for changes in your source code and rerun the app every time a change is made

6. Write the code
Now we are going to write our actual application.
The goal is to make a simple web server that listens on port 8080 and have 2 endpoints:

GET /
Responses:
200 Hello from Docker!

GET /greetings/:name
Responses:
200 Hello, {name}!
Enter fullscreen mode Exit fullscreen mode

So now in your favorite text editor edit the source code as follows:

The source code for src/app.js will be:

const express = require('express')
const cors = require('cors')
const app = express()

// Use CORS with default options to allow all origins
app.use(cors())

// Import the home router
const homeRouter = require('./routes/home')

// Use the home router
app.use('/', homeRouter)

// Define the port to listen on
const port = 8080

// Start the server and log a message after it starts
app.listen(port, 
    () => console.log(`Server listening on port: ${port}`)
)
Enter fullscreen mode Exit fullscreen mode

and for src/routes/home.js it will be:

const express = require('express')

// Create the router
const router = express.Router()

// Configure the endpoint for the router
router
    .get('/', (req, res) => res.send('Hello from Docker!'))
    .get('/greetings/:name',
            (req, res) => res.send(`Hello, ${req.params.name}`))

// Export the router
module.exports = router
Enter fullscreen mode Exit fullscreen mode

7. Test the app
Now we are going to test if our code works or not.

  • If you are using nodemon just head to http://localhost:8080
  • If you are not using nodemon just run:
$ node src/app.js
Enter fullscreen mode Exit fullscreen mode

and then head to http://localhost:8080

You should see a web page as follows:

First Output

And if you go to http://localhost:8080/greetings/Ahmed you will see something like:

Second output

Congrats! Now the app is done and we can move to the docker stuff!

2. Containerize the App with Docker

Now that our app is ready we can use Docker to create an image of our app.
To create an image for your app follow these steps:

1. Stop the running node app
First thing to avoid port conflicts later on, we need to stop the app for now, use Ctrl+C on the terminal session where you started your app.

2. Create a Dockerfile
In the root directory of the project create a file named Dockerfile, for example you can run:

$ touch Dockerfile
Enter fullscreen mode Exit fullscreen mode

3. Dockerfile code
In the Dockerfile you just created put the following code:

# Base image
FROM node:alpine

# The working directory inside the container
WORKDIR /App

# Copy the package.json file
COPY package.json package.json

# Install the packages for production environment
RUN npm i --production --omit dev

# Copy the source files
COPY src/ src/

# The main entry point of the container
CMD [ "node", "src/app.js" ]
Enter fullscreen mode Exit fullscreen mode

The Dockerfile is divided into steps, each line represents a step (lines starting with # are comments)
So I'll explain each line/step:

Step 1: Import base image

FROM node:alpine
Enter fullscreen mode Exit fullscreen mode

We select the base image to use for the custom image we want to create, here we are using the Official Node Image with the alpine tag which will basically import the Alpine Linux image with Node installed in it. I'm using Alpine image just because it is lightweight, but you can use any other image you like and you can specify what version of node you want, for example you can use:

FROM node:14.18
Enter fullscreen mode Exit fullscreen mode

To use node version 14.18.

Step 2: Select the working directory

WORKDIR /App
Enter fullscreen mode Exit fullscreen mode

We specify a directory -inside the container- to put our app inside of it, you can use anything you like.

Step 3: Copy package.json to our working directory

COPY package.json package.json
Enter fullscreen mode Exit fullscreen mode

We will copy our package.json file to the working directory we specified in the above step. Note that you don't need to navigate or write the path of the working directory after you specified it with WORKDIR instruction.

Step 4: Instal node modules for production

RUN npm i --production --omit dev
Enter fullscreen mode Exit fullscreen mode

This command will basically run npm install with the --production and --omit dev flags. You can use any other flags but this is what I personally use for production apps.

Step 5: Copy the source files to the working directory

COPY src/ src/
Enter fullscreen mode Exit fullscreen mode

Now we will copy out source code files to the working directory we specified on Step 2.

Step 6: Run the node app as the entry point of the image

CMD [ "node", "src/app.js" ]
Enter fullscreen mode Exit fullscreen mode

This is the command that will be run when we spin up a container with our image and we just want to run node src/app.js.

So that's it, we are done with our Dockerfile.

2. Build the Docker image
Now we want to build the actual image that we will use to spin up containers of our app.
In the terminal run:

$ docker build .
Enter fullscreen mode Exit fullscreen mode

NOTE: You might need to run the docker commands with sudo if you haven't done the Docker Post-installation Steps

After the command is done you should see something like:

...
Successfully built 33482f9f2921
Enter fullscreen mode Exit fullscreen mode

3. Get the Image ID
We will need the Image ID so we can use it since we didn't specify any tags for it. You can copy the ID from the above docker build output on your terminal or you can list all the images you have using:

$ docker image list
Enter fullscreen mode Exit fullscreen mode

The output will be something like:

REPOSITORY   TAG       IMAGE ID       CREATED          SIZE
<none>       <none>    33482f9f2921   1 minute ago   177MB
...
Enter fullscreen mode Exit fullscreen mode

Now copy the IMAGE ID.

4. Run a container with the new image
Now we can run a container to test our image, in the terminal run:

$ docker run -d -p 8080:8080 <IMAGE_ID>
Enter fullscreen mode Exit fullscreen mode

Replace <IMAGE_ID> with the ID of your image.
The flag -d is used to run the container in Detached Mode(in the background).
The flag -p will expose a port from the container on the host machine. It uses the syntax -p hostPort:containerPort.
You can read more about these flags in the Docker Run Reference.

The output should be a hash, something like:

70f36364143abafd4ce2a4f338b20d97015fda400a0bcfd86fd819e86ee39752
Enter fullscreen mode Exit fullscreen mode

It means that you are up and running. If you go to http://localhost:8080 you should find the app running!

5. Stop the container
You can stop the currently running container using the command:

$ docker stop <CONTAINER_ID>
Enter fullscreen mode Exit fullscreen mode

Replace <CONTAINER_ID> with the output of the previous step or run:

$ docker ps
Enter fullscreen mode Exit fullscreen mode

To get a list of the running containers and then copy the CONTAINER ID from the output.

Now we know our image is ready and we can publish it to a Container Registry to use it from anywhere we want!

3. Publish the Image to Docker Hub

Now we have completed developing our app and we built a Docker image of it, now we need a way to distribute/publish our image either publicly or privately.

Docker Hub is a Container Image Library or a Container Registry where people can push(publish) their images to repositories and make those repositories either public for anyone to view and pull(download) or private where only those authorized can view it or pull it to spin up containers.
We will be pushing our image to a public repository on Docker Hub where we can pull it and use it from anywhere.

To do that, follow these steps:

1. Create a Docker Hub account
If you don't already have an account go to hub.docker.com and create an account.
Note that your username on Docker Hub will be your namespace for your repositories, for example mine is ahmedwadod so my images will be ahmedwadod/image_name:tag

2. Create a repository
In your account home page click on Create Repository

Create Repo

Now fill out the details of your repository, we will be setting the visability to Public, if you set it to Private you are going to need to login with your credentials on Docker whenever you want to pull the image.

Creating Repo

Now click Create and you will have your repository set to go.

3. Tagging the image
Now we need to rebuild our image with the appropriate tags, the tag for your image will be: YOUR_USERNAME/REPO_NAME:TAG for the :TAG we will use latest as it is the default. The :TAG can be used to when you want to upload different versions of your app for example it can be :v1.0 or :v2.0 or it can be used for different variants of the base image used, for example :v1.0-alpine or :v1.0-ubuntu.
In my case the tag will be: ahmedwadod/nodejs-docker-tutorial:latest

To build the image go to your project's root directory and in the terminal run:

$ docker build -t YOUR_USERNAME/REPO_NAME:TAG .
Enter fullscreen mode Exit fullscreen mode

The output should be something like:

...
Successfully built 33482f9f2921
Successfully tagged ahmedwadod/nodejs-docker-tutorial:latest
Enter fullscreen mode Exit fullscreen mode

4. Login to Docker Hub
Now to publish our image we need to first login into Docker Hub from the terminal, run:

$ docker login -u <YOUR_USERNAME>
Password: # Enter your password and press enter
Enter fullscreen mode Exit fullscreen mode

The output will be:

Login Succeeded
Enter fullscreen mode Exit fullscreen mode

5. Push the image to Docker Hub
Now all we have to do is push the image, run:

$ docker push YOUR_USERNAME/REPO_NAME:TAG
Enter fullscreen mode Exit fullscreen mode

The output will be something like:

The push refers to repository [docker.io/ahmedwadod/nodejs-docker-tutorial]
a62d27597b40: Pushed 
c8b55b75e003: Pushed 
d6605a78d13e: Pushed 
86145b7dbdcb: Pushed 
25c4d12b64e7: Mounted from library/node 
1d454e07796f: Mounted from library/node 
970073eeaee3: Mounted from library/node 
8d3ac3489996: Mounted from library/node 
latest: digest: sha256:49d70d1032b3389b465db6691c7e402f146d366b71df9f2b2196301af86116c2 size: 1990
Enter fullscreen mode Exit fullscreen mode

Now if you go to the repository in Docker Hub and then to the tags tab you will find the tag latest available.

Pushed

6. Run the image from anywhere!
Now you can go to any server with docker installed on it and run:

$ docker run -d -p 8080:8080 YOUR_USERNAME/REPO_NAME:TAG
Enter fullscreen mode Exit fullscreen mode

And it will spin up a container with your Node js App!

Closure

Now that you have containerized your app, you can deploy it. I will be posting in the future about deploying web apps with Dcoker, so follow me to stay tuned.

You can find this tutorial's code on my Github

If you have faced any problems with these steps, comment down below and I will try to help you fix it.

Thank you for reading.

Discussion (0)