Docker for Developers - Setting up Node.js

Now that you have learnt how to setup MongoDB in Docker... this article will teach how you can setup a web server with Node.js. In this series, the files are built incrementally, so you can learn the nuances along the way. You can follow along if you like... but it is not necessary since I will be documenting everything here. This article assumes that you already know about the MEAN stack, Git and have been following the Docker series.

Series

Part 1: Docker for Developers - Setting up your developer machine
Part 2: Docker for Developers - Setting up MongoDB
Part 3: You are HERE
Part 4: Docker for Developers - Load Balance using Nginx

Let's Dive in

Step 1: Download the project

Open a terminal and type git clone https://github.com/attosol/nginx-and-nodejs-on-docker.git.

Step 2: Change your directory and checkout the commit

$ cd nginx-and-nodejs-on-docker
$ git checkout 551c768

Step 3: Review the docker-compose.yml file carefully

Review

version: '2'

services:  
  database:
    image: ${DATABASE_VERSION}
    networks:
      - backend
    container_name: ${DATABASE_CONTAINER_NAME}
    volumes:
      - mongo-data:/data/db
      - ./docker/scripts:/scripts
      - ./docker/data:/data

  node_server:
    container_name: ${NODE_CONTAINER_NAME}
    image: ${NODE_VERSION}
    build:
      context: ./web
      dockerfile: node.dockerfile
    networks:
      - backend
    ports:
      - "80:3000"

networks:  
  backend:
    driver: bridge

volumes:  
  mongo-data:

Notice the following:

  • node_server is added as a new service. NODE_CONTAINER_NAME and NODE_VERSION variables are loaded from .env file which contains:
# Project Information
COMPOSE_PROJECT_NAME=node-nginx-seed  
TAG=1.0

# Database Information
DATABASE_VERSION=mongo:3.4.8  
DATABASE_CONTAINER_NAME=mongodb

# NodeJS Information
NODE_VERSION=node:6.11.3  
NODE_CONTAINER_NAME=node_server  
  • build sets the context for the docker-compose by setting it to ./web directory. This directory contains our code in server.js file which is a super simplistic Node server (see below). There are just 2 routes... / and /users. When you hit /users route, it will find users from the sample database which you created in mongodb:
  • networks assigns the same backend network to the container node_server, so that it could talk to mongodb.
  • ports map the external port 80 to the internal one where the application is listening. You will find that the server.js is listening on port 3000.
  • Node.js code for server.js is pretty straightforward and doesn't need explanation :-)

server.js

"use strict";

// Create an Express app
let express = require('express');  
let app = express();

const PORT = 3000;  
const HOST = '0.0.0.0';

app.get('/', (req, res) => {  
  res.send('Hello world!\n');
});

let mongoose = require('mongoose');  
mongoose.connection.on('error', function (err) {  
  console.log('Could not connect to mongo server!');
  console.log(err);
});

mongoose.connect("mongodb://mongodb:27017/sample", function (err) {  
  if (err) console.log("Connection error - %s", err.message);
  else console.log("Successfully connected to the database");
});

let UserSchema = new mongoose.Schema({  
  name: String,
  gender: String,
  age: String
});

mongoose.model('Users', UserSchema);  
let User = mongoose.model('Users');

app.get('/users', function (req, res) {  
  User.find({}, function (err, users) {
    if (!err) {
      res.write("<table>");
      res.write("<tr>");
      res.write("<th>User Name</th>");
      res.write("<th>Gender</th>");
      res.write("<th>Age</th>");
      res.write("</tr>");
      users.forEach(function (k, v) {
        res.write("<tr>");
        res.write("<td>" + k.name + "</td>");
        res.write("<td>" + k.gender + "</td>");
        res.write("<td>" + k.age + "</td>");
        res.write("</tr>");
      });
      res.end();
    }
  });
});

app.listen(PORT, HOST);  
console.log(`Running on http://${HOST}:${PORT}`);  
  • dockerfile points to the file in ./web directory and contains:
FROM node:6.11.3  
MAINTAINER Rahul Soni <rahul@attosol.com>

# Create a directory to host Node App
WORKDIR /web

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

# Install dependencies
RUN npm install

# Deploy Code from current directory to WORKDIR
COPY . .

# Expose website on port
EXPOSE 3000

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

You can read about all the commands in the dockerfile here. I would like to callout EXPOSE though, since it is the one that is often misinterpreted.

Quote from the documentation

The EXPOSE instruction informs Docker that the container listens on the specified network ports at runtime. You can specify whether the port listens on TCP or UDP, and the default is TCP if the protocol is not specified. The EXPOSE instruction does not actually publish the port. It functions as a type of documentation between the person who builds the image and the person who runs the container, about which ports are intended to be published. To actually publish the port when running the container, use the -p flag on docker run to publish and map one or more ports, or the -P flag to publish all exposed ports and map them to to high-order ports.

With docker-compose you can simply map the ports as you have already seen in the yml file.

Step 4: Build the containers

Before you start the containers, you must build them by executing docker-compose build from the root folder.

$ docker-compose build
Building node_server  
Step 1/8 : FROM node:6.11.3  
6.11.3: Pulling from library/node  
Digest: sha256:b6a60e4007b4391a65b1968bd878698e990b307be1a3541cfe9803f9e6327aa2  
Status: Downloaded newer image for node:6.11.3  
 ---> 8095ae5163c9
Step 2/8 : MAINTAINER Rahul Soni <rahul@attosol.com>  
 ---> Running in 34e08f3398d6
 ---> e836cfe1eaa7
Removing intermediate container 34e08f3398d6  
Step 3/8 : WORKDIR /web  
 ---> b468db36c154
Removing intermediate container 9fca738f8095  
Step 4/8 : COPY package.json .  
 ---> abffd0ba4f62
Step 5/8 : RUN npm install  
 ---> Running in 0ab5e8dcb1b5
npm info it worked if it ends with ok  
npm info using npm@3.10.10  
npm info using node@v6.11.3  
npm info attempt registry request try #1 at 8:26:55 AM  
npm http request GET https://registry.npmjs.org/express  
npm info attempt registry request try #1 at 8:26:55 AM  
npm http request GET https://registry.npmjs.org/mongoose  
npm http 200 https://registry.npmjs.org/express  
npm info retry fetch attempt 1 at 8:26:58 AM  
npm info attempt registry request try #1 at 8:26:58 AM  
npm http fetch GET https://registry.npmjs.org/express/-/express-4.15.5.tgz  
npm http fetch 200 https://registry.npmjs.org/express/-/express-4.15.5.tgz  
...
...
+-- express@4.15.5 
| +-- accepts@1.3.4 
| | +-- mime-types@2.1.17 
| | | `-- mime-db@1.30.0 
| | `-- negotiator@0.6.1 
| +-- array-flatten@1.1.1 
| +-- content-disposition@0.5.2 
...
...
  `-- sliced@1.0.1 

npm WARN node_app@1.0.0 No repository field.  
npm info ok  
 ---> 22112d405db0
Removing intermediate container 0ab5e8dcb1b5  
Step 6/8 : COPY . .  
 ---> 86501a6bc165
Step 7/8 : EXPOSE 3000  
 ---> Running in ae4aa75e5736
 ---> 1fc73dff325d
Removing intermediate container ae4aa75e5736  
Step 8/8 : CMD node server.js  
 ---> Running in 559979052b55
 ---> a737179113d1
Removing intermediate container 559979052b55  
Successfully built a737179113d1  
Successfully tagged node:6.11.3  
database uses an image, skipping  

Great... the build succeeds, and you can simply type docker-compose up -d to let the docker magic begin!!!

Magic

$ docker-compose up -d
Creating network "nodenginxseed_backend" with driver "bridge"  
Creating node_server ...  
Creating node_server  
Creating mongodb ...  
Creating mongodb ... done  

You have just built two containers that talk to each other and have exposed ports that you can use to access the web app. Watch the output of docker ps

$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED              STATUS              PORTS                  NAMES  
2cf760e53b69        mongo:3.4.8         "docker-entrypoint..."   About a minute ago   Up About a minute   27017/tcp              mongodb  
2368cd1f2d5d        node:6.11.3         "node server.js"         2 minutes ago        Up About a minute   0.0.0.0:80->3000/tcp   node_server  

Step 5: Browse the site

Get the IP of the server...

$ docker-machine ip default
192.168.99.100  

Hit http://192.168.99.100 in a browser to be greeted with Hello World!. If you hit http://192.168.99.100/users you should be able to see a list of users like so:

List of users

If all goes well, you be like:
happy

Step 6: Automatically restart application on code changes

So, the build is ready and the container is up! Every time you make a change to the source code, you will have to repeat the process of docker-build > docker-compose and as you can guess... is not effective. This approach is more suitable when you have to ship the code for dev/test/stage scenarios. From a developer perspective, we need to do a bit more such that the updates to the source code instantly reloads the application. Worry not! The solution is easy...

Get the modified files from Github
$ git checkout b91d19

You can view the changes here. There are just 4 simple changes:

Add a volume using docker-compose.yml
  node_server:
    container_name: ${NODE_CONTAINER_NAME}
    image: ${NODE_VERSION}
    build:
      context: ./web
      dockerfile: node.dockerfile
    networks:
      - backend
    ports:
      - "80:3000"
    volumes:
      - ./web:/web
Install PM2 using web/node.dockerfile and change the CMD command:
FROM node:6.11.3  
MAINTAINER Rahul Soni <rahul@attosol.com>

# Create a directory to host Node App
WORKDIR /web

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

# Install dependencies
RUN npm install && \  
    npm install -g pm2

# Deploy Code from current directory to WORKDIR
COPY . .

# Expose website on port
EXPOSE 3000

CMD ["pm2-docker", "process.json"]  
Create a process.json file in ./web
{
  "apps": [
    {
      "name": "sample",
      "script": "./server.js",
      "watch": true,
      "autorestart": true,
      "watch_options": {
        "usePolling": true,
        "interval": 500
      }
    }
  ]
}

Learn about the PM2 declaration here

NOTE

You don't necessarily have to use usePolling in watch_options. This is the worst case scenario when your OS doesn't relay file change notifications properly to the container. If you are on Linux, most likely you won't hit this issue. I am on OSX and VirtualBox was apparently gobbling up the file change notifications. Even though the file changes were visible in the container due to volumes, these changes didn't cause the application restart as expected! Hence, I had to fallback to polling. If you have a better workaround, please drop a note in the comments section.

Rebuild and Enjoy!
$ docker-compose build
$ docker-compute up -d

Your changes to the code should reflect instantly now. So, you can basically use any IDE/Editor of your choice on your local OS and start coding! You get the goodness of Docker without polluting your OS.

yay

What next?

Well, stay tuned for upcoming articles. Say hi, share this article, leave a comment or Subscribe now to stay updated through our monthly newsletter. Also, check out our services or contact us at contact@attosol.com for your software and consultancy requirements.

Happy Docking!

Ads:

Rahul Soni

⌘⌘ Entrepreneur. Author. Geek. ⌘⌘

Kolkata, India

Subscribe to Attosol Technologies

Get the latest posts delivered right to your inbox.

or subscribe via RSS with Feedly!