/ node

Deploying Node.js with PM2 and Nginx

Production can be a scary place. Most hackers finish up a totally rad web app and don't really know what to do with it other than run it on port 80 as root. You generally don't want to do this when there are better solutions easily available.

A couple of options are:

  • Use iptables to redirect port 80 to the port your app is running on
  • Use something like process.setuid() to drop root
  • Use a reverse proxy to hand off requests to your app

My goto for this is the last option. I usually throw it behind Nginx and continue having a great day.

If you aren't familiar with Nginx, the wiki describes it well.

Nginx (pronounced engine-x) is a free, open-source, high-performance HTTP server and reverse proxy, as well as an IMAP/POP3 proxy server.

What trips most people up is the use of "reverse proxy". What often comes to mind when the word proxy is mentioned, is what we call a "forward proxy". This would be a proxy that a user chooses to send their requests through, possibly to bypass a firewall or provide anonymity. In this case, the proxy is intended to work on behalf of the user.

A "reverse proxy" however, is a proxy that would be set up by the recipient of the requests. This allows for a centralized entry point and transparent redirection. This will make more sense later.


The app

Note: I am doing the following from an Ubuntu 12.04 box

The first thing we are going to do is code up a quick example app to deploy. Let's use Express for this.

Inside your working directory, install Express through npm: npm install express.

Next create a file named app.js with the following.

var express = require('express')
  , app = express()
  , server = require('http').createServer(app);

server.listen(3000);

app.get('/', function(req, res) {
  res.send("Hello prod!");
});

This is a simple Express app that listens on port 3000 and responds to requests with "Hello prod!".

Installing Nginx

Now we want our app to be accessible to the everyday user on HTTP port 80 through Nginx. The version of Nginx that is available via apt is a bit dated, so let's get a fresh one.

Add the Nginx PPA, sudo add-apt-repository ppa:nginx/stable. If the add-apt-repository command isn't available, sudo apt-get install python-software-properties for Ubuntu <=12.04.xx or sudo apt-get install software-properties-common for >12.04 and re-run the command. Now run sudo apt-get update to pull it in. Finally, we can install Nginx, sudo apt-get install nginx.

Configuring Nginx

We are now going to add our app as a site to Nginx. If we take a look inside the Nginx folder, /etc/nginx, we see a few files and more folders. The two we are going to worry about now are sites-available and sites-enabled. The convetion, is that you create your sites config file in the sites-available folder and then symlink it into the sites-enabled folder. You can see the default site already in place by looking inside them. Let's disable the default site, sudo rm /etc/nginx/sites-enabled/default.

Now we'll add our app. Create a new file name example inside the sites-available folder.

server {
  server_name your.domain.com;
  listen 80;

  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;
    proxy_set_header X-NginX-Proxy true;
    proxy_pass http://127.0.0.1:3000;
    proxy_redirect off;
  }
}

This is a very simple config, but for the uninitiated it can seem very foreign. For reference, the commands in the config are known as directives. Let's go through it line by line.

  • server { - Declares a server block
  • server_name your.domain.com; - Nginx routes work from most specific down to least specific. This directive is used to route the incoming request based on it's hostname header. You can fill in your domain name or not worry about.
  • listen 80; - Specifies what port to accept on. It defaults to 80, so feel free to leave it out.
  • location / { - Declares a location block. This can be used for full routing control. In our case, we are routing everything from the root / on.
  • proxy_set_header X-Real-IP $remote_addr; - The first part of this line is the directive used to add or modify a header of the incoming request that we are proxying off to our app. The second, is the name of the header we are altering / creating. In this case, it is the 'X-Real-IP' header, which is used to identify the IP of the original request. The last part, is an Nginx variable that will hold the IP. It's worth noting that all of these headers are only transmitted internally to your app and not back to the client.
  • proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - Similar to the line above, but the difference is the use case of the 'X-Forwarded-For' header. It is a comma separated list of all the proxies the request has been through, starting with $remote_addr as the first one.
  • proxy_set_header Host $http_host; - Yet another header. This contains the requested hostname of the server, e.g. example.com.
  • proxy_set_header X-NginX-Proxy true; - Only used to internally know this request was proxied through Nginx.
  • proxy_pass http://127.0.0.1:3000; - The line that makes the magic happen. This is where our request is going to be handed off to.
  • proxy_redirect off; - This directive is used to rewrite the url of the request. We want to keep ours intact so we turn it off.

Now we can activate this site by symlinking it into the sites-enabled folder, sudo ln -s /etc/nginx/sites-available/example /etc/nginx/sites-enabled and tell Nginx to reload the config by running sudo service nginx reload.

Daemonizing our app

The last thing we need to do is start our app as a daemon so it is not attached to our terminal. The best way I have found to do this is to use PM2.

PM2 is a node process manager that is very easy to use and feature packed. We can install it through npm, npm install pm2 -g. Note the -g; this is used to install it globally.

Now we just need to start our app with PM2! We can do this by running pm2 start app.js -n example. The -n flag lets us add a nickname to the process that we can use later on if we want to stop or restart it. You can confirm that your app started by running pm2 list.

Finally, let's configure pm2 to start our app on system startup. Run pm2 startup ubuntu to generate the proper files. After ensuring that your app is currently running, run pm2 save to save the list of apps to start. For more information, check out the docs.