Node.js and Docker: Write, Build and Publish
Tags: node, docker, webdev, devops
Published: 2022-01-22 15:01:12

## 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](https://hub.docker.com/).

**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
```
and `cd` into it
```
$ cd nodejs_docker_tutorial
```

**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
```
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"
}
```

**3. Install the packages**
We are going to need these packages: 
[express](https://www.npmjs.com/package/express), [cors](https://www.npmjs.com/package/cors) and (as an optional dev dependency) [nodemon](https://www.npmjs.com/package/nodemon)
run:
```
$ npm i express cors
$ npm i -D nodemon # Optional
```

**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
```

**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"
  },
...
```

and now in a terminal session you can run:
```
$ npm run dev
```
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}!
```

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}`)
)
```

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
```

**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
```
and then head to `http://localhost:8080`

You should see a web page as follows:

![First Output](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mf85rvpbhvqw0xeqr7v7.png)

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


![Second output](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l48yxc3crw56fl47x711.png)

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
```

**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" ]
```
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
```
We select the base image to use for the custom image we want to create, here we are using the [Official Node Image](https://hub.docker.com/_/node) 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
```
To use node version 14.18.

_Step 2: Select the working directory_
```
WORKDIR /App
```
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
```
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
```
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/
```
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" ]
```
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 .
```
**NOTE:** _You might need to run the docker commands with `sudo` if you haven't done the Docker [Post-installation Steps](https://docs.docker.com/engine/install/linux-postinstall/)_

After the command is done you should see something like:
```
...
Successfully built 33482f9f2921
```

**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
```
The output will be something like:
```
REPOSITORY   TAG       IMAGE ID       CREATED          SIZE
<none>       <none>    33482f9f2921   1 minute ago   177MB
...
```
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>
```
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](https://docs.docker.com/engine/reference/run/).

The output should be a hash, something like:
```
70f36364143abafd4ce2a4f338b20d97015fda400a0bcfd86fd819e86ee39752
```
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>
```
Replace `<CONTAINER_ID>` with the output of the previous step or run:
```
$ docker ps
```
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](https://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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gvs5lw0qe5a6x5t5wcyz.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ovvhwqnhexwt5yn00j3v.png)

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 .
```

The output should be something like:
```
...
Successfully built 33482f9f2921
Successfully tagged ahmedwadod/nodejs-docker-tutorial:latest
```

**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
```

The output will be:
```
Login Succeeded
```

**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
```

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
```
Now if you go to the repository in Docker Hub and then to the `tags` tab you will find the tag `latest` available.

![Pushed](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hzuj2p6k2wa3v1ddgksw.png)

**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
```
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](https://github.com/ahmedwadod/nodejs_docker_tutorial)

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.