Hosting Chatroom Using Nginx and Upstart

In the old days, setting up web hosting for a project like Chatroom (+ Realtime) was a difficult enterprise: unless you only want your server to host a single, Node-only website, there needs to be some piece of software that sits in front of Node to correctly direct requests to any number of different server technologies. The problem was, though, that the more straightforward tools for doing this didn’t support WebSockets.

However, just a few weeks ago, Nginx announced the release of version 1.4.0 — the first stable version to support WebSocket forwarding. Now, setting things up is a piece of cake.

An Overview of the System

Let’s say you just bought the domain example.com, and you want to host your chatroom there. One option is to run your Node chatroom server on port 80, which will mean that any requests to example.com will be routed directly to Node. If this is the only website on your server, then this is fine; however, typically, you want to be able to set your server up to handle multiple different websites, potentially with multiple different server-side technologies (e.g., if you wanted to build a website with PHP instead of Node).

One solution to the problem would be to simply run your different sites on different ports, but that isn’t ideal: people would then have to go to example.com:8080 and example.com:3000, for example, instead of example.com/siteA and example.com/siteB, or siteA.example.com, etc. You could also buy multiple servers, and point different domains to them — e.g., siteA.example.com could go to server 1, and siteB.example.com could go to server 2. That gets really expensive, really quickly, though.

The ideal scenario would be to only have to purchase a single physical server, and be able to host as many sites as you want, with any URL naming scheme you want — siteA.example.com and siteB.example.com, example.com/siteA and example.com/siteB, even example.com and othersite.com, all on one physical machine. The way we achieve this is by putting our Node processes on different ports (like 8080 and 3000), and then designate a special type of server to listen on port 80 and forward incoming requests to the appropriate Node proccesses. This is called a reverse proxy, and few web servers do the job better than Nginx.

Nginx (pronounced “engine X”) is a highly capable web server in its own right, but it also doubles as a fast and powerful reverse proxy. We’ll be setting up a server with Nginx running on port 80, and Chatroom running on port 8080. We’ll also configure Upstart, a modern replacement for init.d written by the Ubuntu developers, to make sure that our Chatroom automatically restarts if it crashes, and starts up when our server starts up.

Signing Up for Web Hosting

There are a lot of web hosting options out there, but hosting a Node website puts us in an interesting position: we don’t need many “features,” so to speak, just a plain ol’ machine running Linux. If you’re willing to forego a certain level of hand-holding, this means you can find plenty of options that are relatively cheap for the power they provide.

My personal choice is the Media Temple (ve), starting at $30 p/month, for their superior support and straightforward management tools. Digital Ocean, a relative newcomer to the game, offers comparably spec’d servers for only $10 p/month, with plans as cheap as $5 p/month! However, for the purposes of this tutorial, we’ll be using a Linode VPS, a somewhat more popular choice, and on raw tech specs, a good value at just $20 p/month. Most of the setup instructions (other than the Linode-specific stuff) should translate easily to any other server running Ubuntu (which all three hosting companies offer).

It’s worth mentioning that if you choose Media Temple, you can also buy your domain from them and they will set it up for you; with Linode, we’ll have to go through a third-party.

To start out, we’ll head to the Linode home page, and in the box labeled “Create Free Account”, pick a username and password. After clicking the link in the email you receive, you’ll be offered a 4-hour free trial. Ordinarily, you’d want to choose the option labeled “No thanks, I’d like to complete my signup,” but I’ll be using the free trial for the purposes of this tutorial.

Next you’ll be asked to select a datacenter location for your Linode. The geographic location isn’t really important (unless you have a business reason to choose a particular country), but it’s usually best to pick the datacenter that’s closest to where you expect most of your visitors will be. I selected the Newark, NJ datacenter for the tutorial.

You’ll then be asked what configuration you’d like for your VPS. Ubuntu is a great, easy-to-configure server operating system, so I would recommend that you pick the newest 64-bit version labeled “LTS” (Long-Term Support). At the time of this writing, that’s Ubuntu 12.04 LTS. You can usually leave the defaults for disk and swap size, and choose a secure root password for the machine. Then click “Rebuild”!

The progress of setting up your VPS will be shown on the next screen under the Host Job Queue. Once each of the steps shows a green “Success” label, your machine will be fully configured. Click the “Boot” button under your Ubuntu configuration on the Dashboard, and once you see “System Boot” complete on the Host Job Queue, we’ll be ready to rock ’n roll.

Logging In to Your Server

Switch over to the “Remote Access” tab in the Linode Manager, and find the command listed under “SSH Access” — this is what you’ll use to sign into your machine. So, for example, enter the following in a new terminal window:

ssh root@50.116.57.83

You’ll probably be asked to confirm the machine’s RSA fingerprint (type “yes”), and then you’ll be asked for the root password you specified during configuration. You’re now logged in!

Before we go any further, we should create a restricted user account for ourselves (it’s incredibly dangerous to work as root, and it should be avoided when possible — which is almost always). On Ubuntu (and other Debian-based systems), the recommended way to do this is with the adduser command. So, if I wanted to create a user named mp:

adduser mp

The utility will walk you through setting a new password, and filling out a small bio (really only our full name is necessary). Once this is done, you’ll want to enable your new account to run privileged commands using sudo, which on Ubuntu, is as easy as adding them to the sudo group:

adduser mp sudo

Finally, exit ssh, and reconnect to the server using your new username and password:

ssh mp@50.116.57.83

At this point, you may want to disable login on your root account — anything you need privileged access for you can do using sudo. The way to do this varies by distribution, but on Ubuntu, you can do this:

sudo passwd -l root

If you’d like to take more steps to secure your server, Sam recommends this article by Bryan Kennedy; he leaves the root account enabled, but disables root login via SSH (in addition to setting up a firewall and some other security tools). To each his own!

Setting up Nginx

Before we do anything, we’ll need to install a set of development tools.

sudo apt-get update
sudo apt-get install gcc g++ make

You’ll also need to install the PCRE library, which provides Regular Expression-based location matching:

sudo apt-get install libpcre3 libpcre3-dev

Finally, if you want to someday support SSL, you need to install OpenSSL:

sudo apt-get install openssl libssl-dev

Next, head to the Nginx download page, and find the URL for the newest version on the stable branch. At the time of this writing, that’s version 1.4.1. Use something like wget or curl to download the source code to your home directory, and unzip it:

wget http://nginx.org/download/nginx-1.4.1.tar.gz
tar -zxvf nginx-1.4.1.tar.gz
cd nginx-1.4.1/

Run the configure script for Nginx, optionally passing any compile-time options you’d like to use. Generally, you shouldn’t need to pass any, but in case you might want to support SSL somewhere down the road, I’d enable the HTTP SSL module.

./configure --with-http_ssl_module

Once it’s configured, make it, and then install it:

make
sudo make install

Finally, we need to configure Nginx with Upstart. Using your text editor of choice (with sudo), create a new file at /etc/init/nginx.conf with the contents of the configuration file on the Nginx wiki for Upstart. Note that you need to change /usr/sbin/nginx on line 9 to /usr/local/nginx/sbin/nginx if you configured using the default options. Finally, start Nginx.

sudo vi /etc/init/nginx.conf  # and copy in the configuration file
sudo start nginx

If you now type your server’s IP address into a web browser, you should see the Nginx welcome page.

http://50.116.57.83

Configuring a Domain Name

The next step is to configure your domain name to point to your new server. Namecheap is a good budget registrar, but there are plenty of them out there. Pick one that gives you a good price (they’re otherwise all pretty much the same), and buy your domain. My only advice is to stay away from GoDaddy, they sometimes do nasty things (like buying domains that you search for, and then charging you extra to buy it back from them).

Once you’ve purchased your domain, you need to point it to Linode’s name servers. Every registrar will have a slightly different control panel for doing that, but you should find somewhere where you’re allowed to enter a list of name servers. Linode provides five — enter as many as your registrar allows (two is usually sufficient).

Once that’s done, log back into the Linode Manager, and switch to the “DNS Manager” tab. Click “Add a domain zone,” enter your new domain name and email address, leave the default options selected, and click “Add a Master Zone.”

Note that this part can take 24 to 48 hours to propagate correctly, but it may be faster if your domain is new, and you’ve never tried to visit it before. For me, it took about 10 minutes for DNS queries to resolve correctly.

Finally, setup reverse DNS by going back to the “Remote Access” tab (on the “Linodes” tab) and clicking “Reverse DNS” under Public IPs. Enter your new domain name, click “Look up”, and when asked, confirm that you’d like to use it as your reverse DNS domain. If you plan to host multiple domains on this server, use the domain that you consider to be the server’s “main” domain (it’s a somewhat arbitrary distinction sometimes).

Installing Node

Log back into your server via SSH (if you ever logged out), change into your home directory, head over to the Node.js download page, and grab the URL for the “Source Code” install of the current version (if there’s a distinction between stable and development, choose stable). Then, repeat the steps you used for Nginx to install Node:

wget http://nodejs.org/dist/v0.10.5/node-v0.10.5.tar.gz
tar -zxvf node-v0.10.5.tar.gz
cd node-v0.10.5/
./configure
make
sudo make install

Note that make in particular may take awhile, because it will build a private copy of V8, Google’s JavaScript engine.

Type node at the command prompt, and you should be greeted by your old friend the REPL. Exit that (control-D), and now get ready to setup your Chatroom!

Note well: for whatever reason, I had some serious issues with v0.10.5 on my VPS. I ended up rolling back to v0.8.23, which can be found on Node’s download archives page. You might want to try that instead if the newest version gives you problems.

Installing and Running Chatroom

You’ll need to move your Chatroom project files over to your new server. You can use scp for that, or you can use any FTP client of your choosing that supports SFTP (make sure you choose that protocol). From your personal machine (the one that contains your Chatroom project), an scp command to do this might look like this:

scp -r chatroom/ mp@50.116.57.83:chatroom

The result will be a new folder in your (server) home directory named “chatroom” which contains your project. Note that, at this stage (if your DNS has propagated), you can use your domain name instead of your IP address. If your package.json has been kept up to date (i.e., if any time you installed a new module, you made sure it was listed under “dependencies”), this might go significantly faster if you first delete your node_modules folder, and then just run npm install on the server to reinstall your modules.

Note that you may need to delete node_modules/sqlite3 and run npm install on the server in case the module was compiled for a different platform.

SSH into your Linode, change into the “chatroom” directory, start your server (node server.js), and visit port 8080 on your new domain (e.g., http://example.com:8080). If everything worked as expected, you should see your Chatroom project!

Assuming everything works, you can now stop your Node server (control-C), and configure it with Upstart like we did with Nginx. Open a new file at /etc/init/chatroom.conf (using sudo), and use something that looks like this:

# upstart script for the Chatroom server
description "Chatroom"
author "Matt Patenaude"

start on runlevel [2345]   # auto-start Chatroom in "normal" run modes
stop on runlevel [!2345]   # auto-stop it as well

chdir /home/mp/chatroom    # make sure we set the correct directory
respawn                    # if it crashes, restart it automatically

script
    export HOME="/home/mp"
    exec sudo -u mp /usr/local/bin/node /home/mp/chatroom/server.js >> /dev/null 2>&1
end script

There are a few important things to note here: first of all, you must change the path to your home directory, both in the “export HOME” line, and in the path to your Chatroom server.js file, and make sure you set the correct path for the chdir instruction. The other thing to note is that we use sudo -u mp to run the Chatroom server as my limited user account rather than root. This is generally a good idea, because it limits the scope of destruction if your Node server were to be compromised.

Save the file, and then start your Chatroom server:

sudo start chatroom

Head back to port 8080 (http://example.com:8080) and your Chatroom should be running away happily.

Configuring Nginx for Chatroom

The last step is to tell Nginx to direct requests to our Chatroom server running on port 8080. To do this, start by opening the Nginx configuration file (with sudo) in your choice of text editor:

sudo vi /usr/local/nginx/conf/nginx.conf

The configuration file looks pretty long because of all the comments, but really the only significant content looks like this:

worker_processes  1;

events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    sendfile        on;

    keepalive_timeout  65;

    server {
        listen       80;
        server_name  localhost;

        location / {
            root   html;
            index  index.html index.htm;
        }

        # redirect server error pages to the static page /50x.html
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }

    # our Chatroom configuration is going to go here!
}

Pretty straightforward: run a single process with some default settings, with one virtual server configured to return the default welcome page.

We can setup our Chatroom server by adding a new “server” stanza (inside “http”) configured to proxy all requests on “/” to port 8080.

server {
    listen 80;
    server_name example.com;

    location / {
        proxy_pass http://localhost:8080;
        proxy_set_header Host $http_host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

This tells Nginx that anytime a request comes in for “example.com” (change that!), pass it off to the server running locally on port 8080 (and pass along a few useful HTTP headers). If you wanted to use a subdomain instead of “example.com”, you could use (e.g.) server_name chatroom.example.com. Similarly, if you wanted to use a subdirectory, instead of location / you would use, e.g., location /chatroom.

There’s only one more thing we have to do: the above configuration works fine for most sites, but it doesn’t proxy WebSocket connections. We just have to add a few more small things to enable that: we need to pass through the Upgrade and Connection headers, and we need to define a variable that only passes the Connection header if it’s set. Our new final configuration (to be added inside the “http” block) looks like this:

# include a variable for the upgrade header
map $http_upgrade $connection_upgrade {
    default   upgrade;
    ''        close;
}

server {
    listen 80;
    server_name example.com;

    location / {
        proxy_pass http://localhost:8080;
        proxy_set_header Host $http_host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
    }
}

Make sure you used the correct domain (not “example.com”), save the file, and then restart Nginx:

sudo restart nginx

Now when you visit your domain (this time, without :8080), you should see your Chatroom working perfectly. If you ever want to add a different Node website to the same server, just add a new “server” stanza like above (the “map” stanza only needs to appear once in your configuration file), making sure to run your new Node process on something other than port 8080.

Bonus: Using Nginx for Static Files

We can now make one small optimization to our Chatroom with this special configuration. You may recall that, in order to serve your scripts and stylesheets, you had to tell Express to expose something like a “public” directory. The code to do this was a line like this:

app.use('/public', express.static(__dirname + '/public'));

Nginx, though, is a purpose-built file serving machine. No matter how good Express’s “static” middleware gets at serving static files, Nginx will always do a better job. We’d like to change our server, then, so that any requests to /public get served by Nginx directly, rather than being passed through to Node.

Open up the Nginx configuration file (as above), and inside our server, add a new location stanza:

location /public {
    alias /home/mp/chatroom/public;
}

Save the configuration file, remove the app.use(...) line from your Chatroom server, and restart both servers:

sudo restart nginx
sudo restart chatroom

With any luck, everything should still work, but your static files are now being served directly by Nginx!

If you have any questions, feel free to shoot me an email and I’ll do my best to elaborate. Good luck!