6 min read

Using Puma with NGINX

I want my Ruby on Rails applications to be fast, really fast. Typically, during the prototyping stage, I tend to use Heroku for quick deployment (I still think that's a great solution during prototyping), but it can quickly become expensive when adding things like memcached, workers, multiple processes, and more.

So what's the solution? Roll your own server.

There are many alternatives out there. You can setup a VPS in a few clicks on Digital Ocean, Linode, Amazon EC2 or Rackspace for a fraction of the cost of using Heroku with many addons.

I started building web applications a long time ago and Apache was the defacto standard. Everyone was using it and it was really easy to find others who are trying to do the same thing as you.

Shortly after rails hit version 1.0, Phusion Passenger came along. It was a godsend. Rails deployment was one of the toughest things a developer had to do.

However, Apache + Passenger is not the only solution and there are alternatives that are easier and faster to go from 0-60.

I ditched Apache in favor of NGINX for performance and simplicity (NGINX config files are much easier to understand and write) and I've tried a varation of different ruby and rack servers including Passenger and Unicorn.

Puma is a great new web server for Ruby. It's modern and concurrent and it's very fast.

NGINX is a high performance HTTP server that also works as a reverse proxy.

They work really well together. I'd like to share a little bit about how I've been able to setup Puma with NGINX.

I'm using Ubuntu 13.04 in most of my configurations, so that's what this short guide is based on. It assumes you already have Ubuntu configured to the point where you can SSH in and run commands on the server.

What you need:

  1. Ubuntu 13.04 (other versions may work, but I'm referncing 13.04 in this guide)
  2. Rails 3.X+ (I'm running Rails 4.x) and on your server (I put them in /var/www/rails-app-name)
  3. SSH (root or sudo) Access
  4. NGINX is not currently installed on your system. If so, you'll want to purge this.
  5. A domain or IP address that's already pointing to the server.
  6. A tasty beverage

Let's get started!

Installing Puma

In your gemfile you'll want to add:

gem 'puma'

Then run (in your rails directory)

bundle install

If all works well, you should be able to run rails s and see "Puma starting..."

Installing NGINX

It appears that the current version of NGINX in the Ubunty repository is a bit old. I tend to update my key server (which allows security of packages) by:

sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys ABF5BD827BD9BF62

Then you'll want to add a new source to your sources list so apt-get knows where to get the right version from. Add this to the end of /etc/apt/sources.list

deb http://nginx.org/packages/ubuntu/ precise nginx

Ok now we have a basic authentication scheme to grab the latest nginx version. We need to tell apt-get to update it's source list so we can make sure we grab the latest version. We can do that by:

sudo apt-get update
sudo apt-get install nginx

If you've installed packages using apt-get, this should be a no brainer. If you haven't, it may prompt you to agree to downloading the packages. If all goes well, you can check which version of nginx you've just installed by running nginx -v (I'm on 1.4.3).

You can start the nginx service however you'd like. I prefer using Upstart: sudo service nginx start Note: it will return without any output. This is sort of a bad user experience, but it should be running. You can verify by visiting the ip address and you should see: Welcome to nginx!

Ok so now we have both NGINX and Puma installed. The next step can be confusing if you've never used NGINX before.

Configuring NGINX

Sometimes, NGINX uses the sites-available and sites-enabled method for site configuraiton files. In this case, the installer should not (didn't for me) create those directories automatically. I don't mind because it's more of a semantic thing and has no impact on performance. Feel free to do whatever you're comfrotable with. I'll cover the conf.d directory of config files (not the sites-enabled variety).

If you navigate to /etc/nginx/conf.d/ you should see two files. Typically one is example_ssl.conf and the other is default.conf. Feel free to look at these. I tend to delete the default.conf file (or comment it out entirely). The example_ssl.conf should be commented out on it's own. By default, the NGINX config will load all files in this directory so it's important to know what is going to happen when it does (if they are commented out, nothing will happen).

  1. Create a new file in this directory. You can call it whatever you like. I tend to go with the domain name I want to use such as mattg.conf (If you don't know how to create it, the editor will automatically create the file if it doesn't exist).

  2. Open up your favorite editor and edit this file. You can do something like

    sudo nano mattg.conf

  3. Copy and past the following into this file:

     upstream mattg {
     server unix:///var/run/mattg.sock;
     }
    
     server {
     listen 80;
     server_name mattg.me;
     root /var/www/mattg/public;
    
     location / {
     	proxy_pass http://mattg;
     	proxy_set_header Host $host;
         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
     }
     }
    

Now replace every instance of mattg with what you want to use. It's easier to understand (if you're new) if they all match.

Whoa, thatls a lot of stuff! What are we doing here?

Great question! The upstream block tells NGINX where to look for the server. Since we're going to be telling Puma (down below) to run on a unix socket, we need to tell NGINX where this is.

The server block is all things facing the user. When the request comes in, it hits the server block and attemps to understand where it's going. listen 80 means to listen on port 80. This is the standard HTTP port and web browsers will automatically attempt this port first when you type in mattg.me.

server_name mattg.me means that we want to use the domain mattg.me (You'll want to change this to your domain name if you're using one or the IP address that you can access the server on).

root is where your application public folder lives. Previously I uploaded my rails app to /var/www/mattg and the public folder is inside that directory.

location / tells the application to how to handle the root (/) request. It tells it to use the pass through the proxy defined in upstream. The rest of the location block sets the headers as needed. This is a semantic thing and while you should have it, don't worry about it too much in this post.

Ok great, now we understand a little bit about the configuration file (hopefully this helps troubleshooting if you have any issues).

Let's save the file and exit. In nano you can use ctrl X then press enter/return. At the bottom of the shell window, it'll ask, "Press Y to confirm". Press Y then enter to keep the name as mattg.conf or whatever you chose to name your file.

The configuration file is now saved.

Let's restart NGINX so it knows to load the new file.

sudo service nginx restart

If you see errors, it's likely there's a formatting issue in the config file. Check to make sure you close all braces and that you don't have any typos.

NGINX should now be running, but if you attempt to access it via your domain or IP address, you'll likely get some HTTP error code (usually 403). Why? Well, we haven't started our puma server on the socket we set in the configuration file yet!

Let's do that by:

cd /var/www/mattg.me
bundle exec puma -e production -b unix:///var/run/mattg.sock

Remember to change the socket name to the one you set in the upstream block in the config file we created (above)

You should now see that Puma has started up. This is good news, but it's not ready for production quite yet. If you close the shell, puma stops. So we need to daemonize puma so it knows to run in the background of our shell instance.

To do that we need to append a -d flag to the puma start script. That looks like this:

bundle exec puma -e production -d -b unix:///var/run/mattg.sock

Awesome! Puma should now be running on the unix socket and NGINX is internally working as a proxy to forward requests from mattg.me (on port 80) to the rails app through puma.

That's all for now and happy coding!