Assuming I have a NodeJs application being bootstrapped to life using this app.js script.
'use strict';
// Set default node environment to development
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
var express = require('express');
var mongoose = require('mongoose');
var config = require('./config/environment');
var http = require('http');
// Connect to MongoDB
mongoose.connect(config.mongo.uri, config.mongo.options);
mongoose.connection.on('error', function (err) {
console.error('MongoDB connection to <' + config.mongo.uri + '> failed: ' + err);
process.exit(-1);
});
// Setup server
var app = express();
//create a single NodeJs server
var httpServer = http.createServer(app);
var socketio = require('socket.io')(httpServer, {
serveClient: config.env !== 'production',
path: '/socket.io-client'
});
require('./config/socketio')(socketio);
require('./config/express')(app);
require('./routes')(app);
// Start server
function spinASingleServer() {
httpServer.listen(config.port, config.ip, function () {
if ('test' !== app.get('env')) {
console.log('HTTP express server listening on %s:%d, in %s mode', config.ip, config.port, app.get('env'));
}
});
}
setImmediate(spinASingleServer);
// Expose app
exports = module.exports = app;
Now on my Nginx configuration file ( /etc/nginx/sites-enabled/default ), this is how it looks like:
server {
listen 80;
server_name mycoolapp.com;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
If you noticed, app.js scripts creates a single NodeJs server. Also Nginx configuration’s proxy_pass value is directly connected to that sole NodeJs server.
Translating into picture, this is how the single NodeJs instance looks like.
Single NodeJs setup is okay but not flexible when my application becomes popular and is going to be able to sustain high traffic hits.
One cheapest way to increase my application’s reliability is leveraging NodeJs’ cool feature - having the ability to easily spin-up multiple server instances on the fly and load-balanced them using Nginx.
This is the end-result of what I want to achieve.
I’ll have multiple (4) NodeJs server instances. These instances will be listening on different ports ( or even different host IPs ).
Let me start by modifying the original app.js script to spin-up multiple NodeJs servers on different ports.
'use strict';
// Set default node environment to development
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
var express = require('express');
var mongoose = require('mongoose');
var config = require('./config/environment');
var http = require('http');
// Connect to MongoDB
mongoose.connect(config.mongo.uri, config.mongo.options);
mongoose.connection.on('error', function (err) {
console.error('MongoDB connection to <' + config.mongo.uri + '> failed: ' + err);
process.exit(-1);
});
// Setup server
var app = express();
var isDev = (app.get('env') === 'development');
//spin-up multiple servers
var httpServer1 = http.createServer(app);
var socketio = require('socket.io')(httpServer1, {
serveClient: config.env !== 'production',
path: '/socket.io-client'
});
require('./config/socketio')(socketio);
var httpServer2 = http.createServer(app);
var socketio = require('socket.io')(httpServer2, {
serveClient: config.env !== 'production',
path: '/socket.io-client'
});
require('./config/socketio')(socketio);
var httpServer3 = http.createServer(app);
var socketio = require('socket.io')(httpServer3, {
serveClient: config.env !== 'production',
path: '/socket.io-client'
});
require('./config/socketio')(socketio);
var httpServer4 = http.createServer(app);
var socketio = require('socket.io')(httpServer4, {
serveClient: config.env !== 'production',
path: '/socket.io-client'
});
require('./config/socketio')(socketio);
require('./config/express')(app);
require('./routes')(app);
// Start servers
function spinMultipleServers() {
var port1 = parseInt(config.port),
port2 = parseInt(config.port) + 1,
port3 = parseInt(config.port) + 2;
port4 = parseInt(config.port) + 3;
httpServer1.listen(port1, config.ip, function () {
if ('test' !== app.get('env')) {
console.log('HTTP express server listening on %s:%d, in %s mode', config.ip, port1, app.get('env'));
}
});
httpServer2.listen(port2, config.ip, function () {
if ('test' !== app.get('env')) {
console.log('HTTP express server listening on %s:%d, in %s mode', config.ip, port2, app.get('env'));
}
});
httpServer3.listen(port3, config.ip, function () {
if ('test' !== app.get('env')) {
console.log('HTTP express server listening on %s:%d, in %s mode', config.ip, port3, app.get('env'));
}
});
httpServer4.listen(port4, config.ip, function () {
if ('test' !== app.get('env')) {
console.log('HTTP express server listening on %s:%d, in %s mode', config.ip, port4, app.get('env'));
}
});
}
setImmediate(spinMultipleServers);
// Expose app
exports = module.exports = app;
Notice that I am spinning-up 4 NodeJs servers on different ports. I’m incrementing 1 from the original port and assign them to each NodeJs server created. So say, I configured the first port to be 8000. This is how the server port assignment will look like:
Next thing to modify is Nginx’s config file.
upstream my_servers {
ip_hash; # ensures persistence of session id across servers
server 127.0.0.1:8000; # httpServer1 listens to port 8000
server 127.0.0.1:8001; # httpServer2 listens to port 8001
server 127.0.0.1:8002; # httpServer3 listens to port 8002
server 127.0.0.1:8003; # httpServer4 listens to port 8003
#this could also be entirely a different host server
#Ex. server 113.333.123.190:8000;
}
server {
listen 80;
server_name mycoolapp.com;
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
#tell Nginx to distribute the load
proxy_pass http://my_servers/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
Take note of the declared upstream block. This tells Nginx that we want to distribute the load across the declared servers. Also to persist session across servers, I need to use the ip_hash directive since I’m using socket.io for each port. Very important else I’ll get these “Invalid Request” errors from my app.
After restarting Nginx, I’ll have a load-balanced baby. Cool right? But significantly this baby doesn’t cost me a penny to scale-up!
Now this is just a basic load-balancing configuration for Nginx. You can do some more advance configuration like caching, static file serving etc. Read-up more on Nginx’s official website.
Thanks for reading.:)