Sunday, March 4, 2018

Dockerizing a Node Express API app with MongoDB in a Linux container

Node, Express and MongoDB work very well together. Put Docker into the mix and you have a very interesting cocktail.

In this post we shall develop a Node Express app from scratch and then containerize it using Docker. Before we start, you will need to ready your environment with the following pre-requisites for your specific operating system:

- Install Node.js from https://nodejs.org/en/download/
- Install MongoDB from https://www.mongodb.com/download-center#community
- Install Docker from https://docs.docker.com/install/
- Optionally, you can install Visual Studio Code from https://code.visualstudio.com/Download

In a terminal window, go to a suitable location on your computer's hard drive and create a directory named nodemongo:

mkdir nodemongo

Change into the nodemongo directory:

cd nodemongo

Let us initialize a node application. This is done by running the following command:

npm init

Respond as below:

package name: [Hit ENTER]
version: [Hit Enter]
description: Dockerizing node/express/mongo app
entry point: server.js
test command: [Hit Enter]
git repository: [Hit Enter]
keywords: [Hit Enter]
author: [Enter your name]
license: [Hit Enter]

The package.json file is displayed and you are asked to confirm. Enter y to confirm.

This is what my project.json file looks like:

{
  "name": "nodemongo",
  "version": "1.0.0",
  "description": "Dockerizing node/express/mongo app.",
  "main": "server.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Medhat Elmasry",
  "license": "ISC"
}

We will need two node packages: express and mongojs. Install these packages with the following command:

npm install express mongojs

I am using Visual Studio Code to edit my application. Use whatever editor you are familiar with like sublime, atom, brackets, etc....

Create a file named server.js in the nodemongo folder and add to it the following code:

var express = require("express");
var users = require("./routes/");
var config = require('./config');

var app = express();

app.use("/", users);

app.listen(config.port, function() {
    console.log("Server started on port " + config.port)
});

Lines 2 and 3 above suggest that we are missing folders /routes and /config. Therefore, create folders routes and config inside the nodemongo directory.

Inside the /config directory, create a JavaScript file named index.js and add to it the following code:

module.exports = {  
    // Setting port for server
    'port': process.env.PORT || 3000,
    
    // Setting mongo host name
    'mongo_host': process.env.MONGO_HOST || 'localhost',

    // Setting mongo port number
    'mongo_port': process.env.MONGO_PORT || 27017,

    // Setting mongo database name
    'mongo_database': process.env.MONGO_DATABASE_NAME || 'demodb',
};

The above code configures environment variables for: (1) the server listening port number, (2) the MongoDB host name, (3) the MongoDB listening port number and (4) the MongoDB database name. 

Inside the /routes directory, create another JavaScript  file named index.js and add to it the following code:

var express = require("express");
var mongojs = require('mongojs');
var config = require('../config');

var connStr = 'mongodb://' + config.mongo_host;
connStr += ':' + config.mongo_port;
connStr += '/' + config.mongo_database;

var db = mongojs(connStr, ['students']);
var usersCollection = db.collection('users');

var router = express.Router();

router.get('/', (req, res, next) => {
    usersCollection.count({},(err, docCount) => {
        if (docCount == 0) {
            const users = [
                {"name":"user_1","email":"user_1@bogus.com"},
                {"name":"user_2","email":"user_2@bogus.com"},
                {"name":"user_3","email":"user_3@bogus.com"}            
            ];
        
            // use the Event model to insert/save
            usersCollection.save(users, (err, data) => {
                if (err) {
                    res.send(err);
                }
            })
    
        }
    })

    usersCollection.find( (err, data) => {
        if (err)
            res.send(err);
        
        res.json(data);
    })

});

router.get('/add', (req, res, next) => {
    var name = 'user_'+ Math.floor(Math.random() * 1000);
    var email = name + '@bogus.com';

    var doc = {'name': name, 'email': email};
    usersCollection.save(doc, (err, data) => {
        if (err) {
            res.send(err);
        }
        res.json(data);
    })
});

// get single user
router.get("/users/:id", (req, res, next) => {
    usersCollection.findOne({_id: mongojs.ObjectId(req.params.id)},function(err, data){
        if (err) {
            res.send(err);
        }
        res.json(data);
    });
});

module.exports = router;

The above code sets up the following API endpoints:

http://localhost:3000/ Displays all documents in the users collection. If the users collection is empty then it will be seeded with three documents.
http://localhost:3000/add Adds a user document with a randomly generated name and email
http://localhost:3000/users/:id Displays a singe document by MongoDB object id

If the MongoDB database server has not started already, start it. At this point, we are able to test our application and make sure it is working as expected.

After starting the database, launch the application by executing the following terminal command:

npm start

You should see the following message in the terminal window:

> nodemongo@1.0.0 start D:\demo\_docker\nodemongo
> node server.js

Server started on port 3000

This indicates that our node web server  is running and listening on port number 3000. Point your browser to http://localhost:3000/. You should see the following seeded data comprising of three JSON objects in your browser:

[{"_id":"5a9b8f225e78082ddccba361","name":"user_1","email":"user_1@bogus.com"},{"_id":"5a9b8f225e78082ddccba362","name":"user_2","email":"user_2@bogus.com"},{"_id":"5a9b8f225e78082ddccba363","name":"user_3","email":"user_3@bogus.com"}]

Try adding a user by pointing to http://localhost:3000/add. It should randomly generate a user document similar to the following:

{"name":"user_20","email":"user_20@bogus.com","_id":"5a9b8fdd5e78082ddccba364"}

If you try http://localhost:3000/ again, you should see four JSON objects similar to the following:

[{"_id":"5a9b8f225e78082ddccba361","name":"user_1","email":"user_1@bogus.com"},{"_id":"5a9b8f225e78082ddccba362","name":"user_2","email":"user_2@bogus.com"},{"_id":"5a9b8f225e78082ddccba363","name":"user_3","email":"user_3@bogus.com"},{"_id":"5a9b8fdd5e78082ddccba364","name":"user_20","email":"user_20@bogus.com"}]

Lastly, you can get a document by MongoDB object ID with endpoint http://localhost:3000/users/:id. Copy one of the IDs and and it to the address line. I used one of my IDs and pointed my browser to the following endpoint: http://localhost:3000/users/5a9b8f225e78082ddccba361 and experienced the following response:

{"_id":"5a9b8f225e78082ddccba361","name":"user_1","email":"user_1@bogus.com"}

Of course, you will not have the same ID as I do and should use one of the IDs in your users documents.

At this stage, we have determined that our application works as expected. The next and final step is to containerize both MongoDB and our Node/Express API application.

Stop both the Node web server and the MongoDB database server.

Create a text file named .dockerignore in the root of your node project and add to it the following content. This instructs Docker not to copy the contents of node_modules into the container:

node_modules

Create a text file named DockerFile in the root folder of your application and add to it the following:

FROM node:9.7.1
WORKDIR /app
COPY package.json /app/package.json
RUN npm install
COPY . /app
EXPOSE 3000

Above are instructions to create a Docker image that will contain our Node/Express API web application. I describe each line below:

FROM node:9.7.1 Base image node:9.7.1 will be used
WORKDIR /app The working directory in the container is /app
COPY package.json /app/package.json package.json is copied to /app in the container
RUN npm install  This is run inside the containers /app directory
EXPOSE 3000 Port 3000 will be exposed in the container

Create another text file named docker-compose.yml in the root of your node project, and add to it the following content:

version: "3"

services:

  db:
    image: mongo:3.6.3
    ports:
      - "27017:27017"
    restart: always

  web:
    build: 
      context: .
      dockerfile: Dockerfile
    ports:
      - "3000:3000"
    links:
      - db
    environment:
      - MONGO_HOST=db
      - MONGO_PORT=27017
      - MONGO_DATABASE_NAME=demodb
    command: node /app/server.js

Below is an explanation of what this file does.

We will be having two containers. Each container is considered to be a service. The first service is named db and will host MongoDB. The second service is named web and will host our Node/Express API app.

The most current version of docker-compose is version 3. This is the first line in our docker-compose.yml file.

The MongoDB Container

Image mongo version 3.6.3 will be used for the MongoDB container.

restart: always is so that if the container stops, it will be automatically restarted.

Port 27017 is the default MongoDB listening port. This port number, inside the container, is being mapped to the same port number outside the container.

Node/Express API app Container

The container will be built using the instructions in the Dockerfile file and the context used is the current directory.

Port 3000 in the web container is mapped to port 3000 on the host computer.

The environment variables needed by the web app are:

- MONGO_HOST pointing to the MongoDB service name
- MONGO_PORT sets the MongoDB listening port number
- MONGO_DATABASE_NAME is the name which we want to use for the database

Running the yml file

To find out if this all works, go to a terminal window and run the following command:

docker-compose -f docker-compose.yml up

Point your browser to http://localhost:3000/ and you should see three seeded users JSON objects as below:


You can view the two running containers by executing the following Docker command in a terminal window:

docker ps -a

You will see something similar to this:

CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                      NAMES
979d458c9ba3        nodemongo_web       "node /app/server.js"    21 minutes ago      Up 20 minutes       0.0.0.0:3000->3000/tcp     nodemongo_web_1
ae80a1b9283c        mongo               "docker-entrypoint.s…"   21 minutes ago      Up 21 minutes       0.0.0.0:27017->27017/tcp   nodemongo_db_1

To shutdown and remove thes two containers, run the following Docker command:

docker-compose -f docker-compose.yml down

Thanks for coming this far in my tutorial and I hope you found it useful.


No comments:

Post a Comment