Vladi Gleba

I create things for the internet.

Deploying Rails Apps, Part 4: Configuring Nginx

published in Deployment, Deployment Series, Phindee Comments

I talked about how I configured Unicorn for Phindee in part 3, and now I’ll cover how I configured Nginx. While Unicorn will handle requests for pages dynamically generated by Rails, Nginx will handle requests for static assets, like stylesheets, scripts, images, and fonts. If you’re wondering why I chose Nginx over Apache, see part 2 for the explanation.

All right, since there is quite a bit to cover, we’ll jump right in. We’ll start by creating a file called nginx.conf inside our app’s /config directory on our local computer. Here’s how mine looks like:

nginx.conf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
upstream unicorn {
  server unix:/tmp/unicorn.phindee.sock fail_timeout=0;
}

server {
  server_name www.phindee.com;
  return 301 $scheme://phindee.com$request_uri;
}

server {
  listen 80 default deferred;
  server_name phindee.com;
  root /var/www/phindee/current/public;

 location ^~ /assets/ {
    gzip_static on;
    expires max;
    add_header Cache-Control public;
  }

  try_files $uri/index.html $uri @unicorn;
  location @unicorn {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_redirect off;
    proxy_pass http://unicorn;
  }

  error_page 500 502 503 504 /500.html;
  keepalive_timeout 10;
}

Nginx has a master process managing all the workers, just like Unicorn, which are responsible for processing requests from clients. Unlike Unicorn, Nginx also has a cache loader process that checks and/or populates the cache with metadata, as well a cache manager process that’s responsible for cache expiration. Together, they keep Nginx internals running quickly and efficiently.

A Bit on Workers

If you log in to your VPS and cd into /etc/nginx, you’ll find a file called nginx.conf. This is the main Nginx configuration file that Nginx will parse when it runs, and it’s the place where you can modify the number of workers available to process requests. You can do this by modifying the worker_processes directive defined at the top of the file. It’s set to four workers by default, but I changed mine to one because that’s more than enough for a low-traffic app. It’s also possible to modify the number of connections a worker can accept by modifying the worker_connections directive inside the events block; I changed mine to 1024 connections.

This means that given our current configuration, our server will be able to accept a total of 1024 simultaneous connections. If you want to increase this, it’s generally best to increase the worker_connections value before increasing the number of workers. (Remember, each worker is a single-threaded process, so whenever you increase the number of workers, you’re also increasing the total amount of memory that will be used.) But having one worker process that’s capable of handling 1024 connections is more than enough for a low-traffic app.

By the way, if you’re wondering how our own nginx.conf file we created above will get executed, /etc/nginx/nginx.conf already has an include directive inside the http block that will automatically include any files in the /etc/nginx/sites-enabled directory, and that’s the place where we will put our own nginx.conf file when it’s time to deploy.

Hooking up with Unicorn and Handling Redirects

Since Nginx is not capable of handling requests for pages that are dynamically generated by Rails, we need to tell it to somehow pass such requests off to Unicorn. We’ll take the first step to accomplishing this by defining an upstream block called unicorn, inside which we point the server to the same Unix socket that we used in our unicorn.rb file from part 3. This is just the first step, however, and more work needs to be done to get this working, as you’ll see later. By the way, in case you’re wondering, setting fail_timeout to 0 is necessary for Nginx to correctly handle Unicorn timing out due to its worker being killed when it takes longer than 30 seconds to respond, as specified in unicorn.rb.

The server block right below the upstream block is there to redirect a request for “www.phindee.com” to “phindee.com”. The server_name directive specifies the URL we’re redirecting from, while the return directive specifies where to redirect to. (Notice we’re returning a 301 status code to specify a permanent redirection.) The $scheme variable stores the HTTP scheme (i.e. http, https), while $request_uri stores the unmodified URI of a client request, which includes the arguments, but not the host name (e.g. “/foo/bar.php?arg=baz”).

Where the Meat Is

The next server block contains the main configuration. The listen directive inside it tells Nginx to listen on port 80, and the server_name directive right below specifies the domain name that Nginx will try to match, which is “phindee.com” in my case.

Specifying default in the listen directive, by the way, tells Nginx to use this server block by default if it can’t find a matching domain name, which means I could technically leave out the server_name directive completely, and everything would still work because of default, but I like to leave it in for readability. And finally, I added the deferred option since I’m running this on Linux, which tells Nginx to use the TCP_DEFER_ACCEPT option to speed up performance by reducing the amount of preliminary work that happens between a client and the server.

Moving along, the root directive specifies the directory in which Nginx will look to handle requests for static files. This is basically the directory we specified inside unicorn.rb, except it has an additional /public directory appended to the end of it. It corresponds to your app’s /public directory on your local computer and is the place where your static files are and will reside. Currently, it only contains various error pages, a favicon, and a robots.txt file for search engines. When we later deploy with Capistrano, it’ll contain all our assets as well, including stylesheets, scripts, images, and fonts.

Handling Asset Requests

Just like the server_name directive handles requests for domain names, the location directive handles requests for specific files and folders. The caret and tilde (^~) tells Nginx to do a regular expression match on /assets/ and to stop searching as soon as it finds a match (see the Linode Guide to learn more).

By setting the gzip_static directive to on, we’re then telling Nginx to look for an already pre-compressed .gz file before proceeding to compress it. This prevents Nginx from compressing the same file each time it is requested.

The expires directive then makes the response cacheable and marks it with an expiry date set to max, which is equivalent to December 31st, 2037. This tells browsers and any caching servers to not request these assets again until the specified date. Of course, if we make changes to our stylesheets, for example, Rails will change the filename and browsers will still receive the latest version, which will then also be cached.

Using the expires directive, however, is an outdated method of specifying caching, and it’s recommended to use Cache-Control header instead. The next line in the code does just that through the add_header directive. (The reason we include expires is to make things backward-compatible.) It’s possible, by the way, to set Cache-Control to either public or private, and I’m setting it to public because we’re caching assets that are meant to be used by everybody, whereas private would mean they’re unique to individual users (see Stack Overflow to learn more).

Trying to Find a Match

The next line is the try_files directive, which is there for requests that didn’t match with any location blocks. In our case, it tries to match non-asset requests. The $uri variable inside it contains the current request URI, minus the arguments, protocol, and host name, so if we typed in “phindee.com/foobar” into the address bar, the $uri would be “/foobar”, and given our try_files directive, Nginx would try to first match a var/www/phindee/current/public/foobar/index.html file. If it found no such file, it would then try to match the /foobar directory itself, and if that didn’t work, it would then pass the request off to Unicorn through a named location, which is defined next through the location directive and called @unicorn.

Inside the named location, the proxy_pass directive does all the heavy lifting. We set it to http://unicorn so that it points to the upstream block called unicorn, which we already defined, and the request is then handled by the Unicorn socket defined there. The two proxy_set_header directives then append additional headers needed for Unicorn, while proxy_redirect set to off prevents Nginx from doing any redirect handling. (There is a sample nginx.conf file on GitHub with comments explaining why this is necessary.)

Last Few Lines

All right, we’re down to the last two lines. error_page makes sure that our app’s 500.html page is shown for any 500-related errors, while keepalive_timeout tells Nginx to retain keep-alive connections (also known as persistent connections) for up to 10 seconds and close them if they exceed that time. The main concern when choosing the amount of time is mobile devices on slow networks, but I think 10 seconds should be enough.

Keep-alive connections, by the way, send multiple HTTP requests in a single connection, as opposed to opening a new connection for each request; in HTTP 1.1, all connections are persistent by default, which means stylesheets, scripts, images, and fonts, for example, would all be sent using a single connection.

These are, of course, not all the options you can specify. If you’d like to learn about the additional ones, feel free to read through the comments in the sample nginx.conf file I mentioned earlier.

And that wraps up part 4. I will introduce Capistrano in the next post and will explain how I configured it for Phindee. If you want to be notified when it’s out, feel free to subscribe, and you’ll get it delivered to your inbox as soon as it’s released.

Comments