Setting up Node.js
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
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
andNODE_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 thecontext
for thedocker-compose
by setting it to./web
directory. This directory contains our code inserver.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 thesample
database which you created in mongodb:networks
assigns the samebackend
network to the containernode_server
, so that it could talk tomongodb
.ports
map the external port80
to the internal one where the application is listening. You will find that theserver.js
is listening on port3000
.- 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 Soni < rahul soni@xxxx.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 Soni < rahul soni@xxxx.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!!!
$ 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:
If all goes well, you be like:
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 Soni < rahul soni@xxxx.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
inwatch_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.
What next?
Stay tuned for upcoming articles. You may contact us for your software and consultancy requirements.