Setting up nginx, ssl, and virtual hosts

01 February 2008

Setting up nginx, ssl, and virtual hosts

installing nginx

To install nginx you have a few options depending on the flavour of operating system you are running. You can either install it via a package manager, or download the source and compile it yourself. Using the package manager will mean you will get the latest known stable version for your OS, and have to worry less about working out dependencies or conflicts. However, you may be a version or two behind the official release.

installing nginx via yum/apt-get/rpm

If you've got a preferred package manager on your system then it should be as easy as one of the following (choose whichever is appropriate): sudo yum install nginx sudo apt-get install nginx

Alternatively you can download the rpm and install using that:

wget ftp://ftp.univie.ac.at/systems/linux/fedora/updates/9/i386.newkey/nginx-0.6.32-1.fc9.i386.rpm
rpm -Uvh nginx-0.6.32-1.fc9.i386.rpm

installing nginx via source

If none of the above is suitable, then you're off to compile it on your own. The following should do the trick and include the libraries/packages we need:

wget http://sysoev.ru/nginx/nginx-0.7.2.tar.gz
tar zxvf nginx-0.7.2.tar.gz
cd nginx-0.7.2
./configure --prefix=/usr/local/nginx --sbin-path=/usr/local/sbin --with-debug --with-http_ssl_module
make
sudo make install
sudo nginx

configuring an nginx virtual host

Now that it's up and running, time to configure nginx to know about a virtual host (or two) on your system. For now, lets just serve some static content so we get a feel for the config files:

sudo nano /usr/local/nginx/conf/nginx.conf

You'll see the default nginx config file, in recent versions it's fairly well commented to help you understand what is going on. Scroll down and find the definition that looks like this:

server {
      listen       80;
      server_name  _;

      #charset koi8-r;

      #access_log  logs/host.access.log  main;

      location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
      }

      error_page  404              /404.html;
      location = /404.html {
        root   /usr/share/nginx/html;
      }
    }

I wont go into too much detail here, the important thing to note is where the root location is. Here's is specified as /usr/share/nginx/html, which means a request to http://yoursite.com/directory/file.html is going to try and serve the file on your server from /usr/share/nginx/html/directory/file.html. To create a new virtual host lets change this definition to look more like this:

server {
      listen       80;
      server_name  www.yoursite.com;
      root   /var/www/yoursite.com/html;

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

Now all requests coming in to http://www.yoursite.com will be handled by this definition, but only requests for www.yoursite.com. If you want to handle another domain within this definition you can just put an additional server name in like this:

server_name  www.yoursite.com www.othersite.com;

Alternatively, if you want to have www.othersite.com serve up content from a different location you would just add another server definition:

server {
      listen       80;
      server_name  www.yoursite.com;
      root   /var/www/yoursite.com/html;

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

    server {
      listen       80;
      server_name  www.othersite.com;
      root   /var/www/othersite.com/html;

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

configuring nginx for your rails app

The hosts are setup, now we can have one of them start serving some rails apps. Lets change the virtual host definition we had above for www.yoursite.com to look like the following:

server {
      listen 80;
      server_name  www.yoursite.com;
      root   /var/www/apps/yoursite/current/public;

      # Set the max size for file uploads to 50Mb
      client_max_body_size 50M;

      # this rewrites all the requests to the maintenance.html
      # page if it exists in the doc root. This is for capistrano's
      # disable web task
      if (-f $document_root/system/maintenance.html) {
        rewrite  ^(.*)$  /system/maintenance.html last;
        break;
      }

      location / {
        # needed to forward user's IP address to rails
        proxy_set_header  X-Real-IP  $remote_addr;

        # needed for HTTPS
        proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_redirect false;
        proxy_max_temp_file_size 0;

        # If the file exists as a static file serve it directly without
        # running all the other rewrite tests on it
        if (-f $request_filename) { 
          break; 
        }

        # check for index.html for directory index
        # if its there on the filesystem then rewite 
        # the url to add /index.html to the end of it
        # and then break to send it to the next config rules.
        if (-f $request_filename/index.html) {
          rewrite (.*) $1/index.html break;
        }

        # this is the meat of the rails page caching config
        # it adds .html to the end of the url and then checks
        # the filesystem for that file. If it exists, then we
        # rewite the url to have explicit .html on the end 
        # and then send it on its way to the next config rule.
        # if there is no file on the fs then it sets all the 
        # necessary headers and proxies to our upstream mongrels
        if (-f $request_filename.html) {
          rewrite (.*) $1.html break;
        }

        if (!-f $request_filename) {
          proxy_pass http://railsapp;
          break;
        }
      }
    }

What we have here is a series of rules that match the requested file against some regular expressions and underlying filesystem files. To tell nginx to serve any content that it finds matching the full requested path, starting in the directory defined as root we do the following:

if (-f $request_filename) { 
      break; 
    }
    if (-f $request_filename/index.html) {
      rewrite (.*) $1/index.html break;
    }
    if (-f $request_filename.html) {
      rewrite (.*) $1.html break;
    }

If that doesn't work, we assume it need to be served by the rails app so we send it through using this:

if (!-f $request_filename) {
      proxy_pass http://railsapp;
      break;
    }

But what does http://railsapp mean? No such server exists! Within nginx we need to define an upstream proxy. This will allow us to wrap all our mongrels up within a single definition, and nginx can take care of sending it through to the instances in the back end. To define the railsapp upstream mongrels we add the following outside our server definition:

upstream railsapp {
      server 127.0.0.1:3000;
      server 127.0.0.1:3001;
      server 127.0.0.1:3002;
    }

setting up your nginx ssl server

What if we want to serve out site over HTTPS for security reasons? No problem! We just add a server definition that listens on the correct port (that's 443 for those of you playing along at home). So our definition would look like this:

server {
      listen 443;
      server_name  www.yoursite.com;
      root   /var/www/apps/yoursite/current/public;

      # Rest of config here...

redirecting all traffic to your ssl server

To be extra secure, we've decided that we want all traffic to be handled over SSL. Well we can use the ssl_requirement plugin in our rails app to completely prevent non-HTTPS access, just incase. We can also add this server definition to our nginx config to re-direct anybody who mistakenly links to the plain old HTTP address:

server {
      listen 80;
      server_name www.yoursite.com;
      rewrite ^/(.*) https://yoursite.com/$1 permanent;
    }

setting nginx error pages

Nginx will display some standard error messages should there be a problem, they're about as attractive as the standard ones that come with rails though. If you want to jazz them up and make the whole experience less jarring for your users then update these lines:

error_page   404 500 502 503 504  /http_error.html;
      location = /http_error.html {
      root   /var/www/errors;
    }

And point it to the file you want to come up for all your errors. If you want to differentiate between various errors then configure each one individually:

error_page   500 502 503 504  /50x.html;
      location = /50x.html {
      root   /var/www/errors;
    }
    error_page   404   /404.html;
      location = /404.html {
      root   /var/www/errors;
    }

directing nginx log output

If you're running multiple virtual hosts, it will probably pay to give each a log file of it's own rather than having a single jumbled mess to worry about. Basically any configuration you provide in the http definition you can re-define on a per-server basis within each server definition. So simply add these lines into the relevant virtual host/server definition:

server {
      listen 80;
      server_name  www.yoursite.com;
      root   /var/www/apps/yoursite/current/public;

      access_log  /var/log/nginx/yoursite.access.log  main;
      error_log  /var/log/nginx/yoursite.error.log  debug;

      # Rest of config...
    }

make nginx serve cache content from a custom location

Some people may cache their rail output into a custom directory (and why wouldn't you, it makes clearing the cache so much easier). If that's the case, the usual nginx/rails configuration won't work as expected. Your site will still work, you'll just not be making the most of letting nginx serve your static content. To resolve it, add these lines in before the section that passes the request to your upstream proxy:

if (-f /cache$request_filename) { 
      rewrite (.*) /cache$1 break;
      break; 
    }

    if (-f /cache$request_filename.html) { 
      rewrite (.*) /cache$1.html break;
      break; 
    }

Where /public/cache is where you are storing all of your cached content. Obviously, alter to fit accordingly.

I'm putting together a weekly newsletter to help developers spend more time on what they do best. You should sign up to Cloud Services Weekly  today!