Engine what?

Nginx (engine-x) is a web server and reverse proxy for web and mail protocols (HTTP, HTTPS, SMTP, POP3 and IMAP). It has been first released in 2004, and its usage keeps growing ever since (according to Netcraft, it was hosting 14.47% of active sites in August 2014).

It’s capable of hosting many kinds of applications:

  • static HTML pages
  • PHP, using PHP-FPM
  • Ruby on Rails and any kind of Rack-based Ruby application, using Phusion Passenger
  • proxying requests to another webserver (e.g. a software launching its own web server, like Kodi)

Set up the bases

The architecture described in this post is pretty simple:

  • a default virtual host (vhost) for the top-level domain name, also catching requests to unknown sub-domains
  • different applications hosted on sub-domains
  • some vhosts will be HTTPS-only, some will offer it without being mandatory
  • enabling or disabling a vhost must be easy

Installing nginx

Nginx uses static modules, enabled or disabled at compile-time. It’s important to decide what you need before installing nginx. The only non-default module used in this post is Passenger, needed to host Rack-based applications. Everything else will work without it.

Nginx works on any decent *nix. It’s probably available in your OS repositories. If it’s not, please refer to the official installation guide. On Archlinux, a package is available on AUR including the Passenger module:

yaourt -S nginx-passenger

Configuration

Once nginx is installed, we need to configure a basic configuration. I’ll refer to the configuration root directory as $CONFDIR. It’s usually /etc/nginx/.

Note that nginx needs to be restarted to reflect any configuration change.

Directory structure

To ease the configuration, we’ll split it across three folders:

  • $CONFDIR will contain all the general files (PHP configuration, main nginx configuration file…)
  • $CONFDIR/ssl will contain the SSL certificates
  • $CONFDIR/vhosts will contain our vhosts definitions

Main configuration file

Here’s the basic configuration file we’ll start with:

$CONFDIR/nginx.conf

worker_processes                  auto;

events {
  worker_connections              1024;
}

http {
  proxy_send_timeout              600s;
  proxy_read_timeout              600s;
  fastcgi_send_timeout            600s;
  fastcgi_read_timeout            600s;
  include                         mime.types;
  default_type                    application/octet-stream;
  sendfile                        on;
  keepalive_timeout               0;
  gzip                            on;
  index                           index.html index.htm;
  client_max_body_size            2048m;

  server {
    listen                        0.0.0.0;
    server_name                   enoent.fr;

    access_log                    /var/log/nginx/localhost.access_log;
    error_log                     /var/log/nginx/localhost.error_log info;

    root                          /srv/http/localhost;
  }
}

This file sets up an nginx instance with some decent settings (enable gzip, use index.html or index.htm as default index pages…), and defines our default vhost. It answers to every request targeting the hostname enoent.fr. It will serve static pages found in /srv/http/localhost.

SSL support

As mentioned earlier, we’ll have two SSL behaviours depending on the vhost:

  • SSL is offered, but not mandatory (vhost answers to both HTTP and HTTPS)
  • SSL is offered, and mandatory (vhost answers on HTTPS, and redirect to HTTPS when it receives a request on HTTP)

We will need two files to define these two behaviours. One of them will have to be included in every vhost, depending on the SSL politic we want for this specific vhost.

Shared configuration

Here we go for the first configuration file:

_$CONFDIR/sslopt.conf

ssl_certificate_key       /etc/nginx/ssl/ssl-decrypted.key;
add_header                Strict-Transport-Security           max-age=31536000;
ssl_prefer_server_ciphers on;
ssl_ciphers               ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-RC4-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:RC4-SHA:AES256-GCM-SHA384:AES256-SHA256:CAMELLIA256-SHA:ECDHE-RSA-AES128-SHA:AES128-GCM-SHA256:AES128-SHA256:AES128-SHA:CAMELLIA128-SHA;
ssl_session_cache         shared:SSL:10m;
ssl_session_timeout       10m;
keepalive_timeout         70;

You can obviously adapt this file to your specific needs. It defines:

  • the SSL key used (/etc/nginx/ssl/ssl-decrypted.key)
  • a default max-age header
  • a list of accepted SSL ciphers
  • session, cache and keepalive durations

The other file will define the exact same settings, adding just one directive: the SSL is mandatory. Instead of copy and paste all of this, here’s what we can do:

$CONFDIR/ssl.conf

include ssl_opt.conf;
ssl     on;

Enabling SSL for a vhost

To enable SSL on a vhost, we’ll need to make three or four modifications to the vhost definition, depending on the SSL policy.

Non-mandatory SSL

If the SSL is not mandatory, we’ll need to:

  • enable listening on port 443 in addition to the default 80
  • choose the certificate we want to use
  • include the SSL policy file

Here’s how it translates, for our first vhost defined earlier:

$CONFDIR/nginx.conf (server block only)

server {
  listen                  0.0.0.0:80;
  listen                  0.0.0.0:443 ssl;
  server_name             enoent.fr;

  access_log              /var/log/nginx/localhost.access_log;
  error_log               /var/log/nginx/localhost.error_log info;

  root                    /srv/http/localhost;

  ssl_certificate         /etc/nginx/ssl/enoent.fr.crt;
  include                 ssl_opt.conf;
}

Mandatory SSL

If the SSL is mandatory, we’ll need to:

  • enable listening on port 443 instead of the default 80
  • choose the certificate we want to use
  • include the SSL policy file
  • redirect HTTP requests to HTTPS

And here’s the result for our first vhost:

$CONFDIR/nginx.conf (server block only)

server {
  listen                  0.0.0.0:80;
  server_name             enoent.fr;
  rewrite                 ^ https://$server_name$request_uri? permanent;
}

server {
  listen                  0.0.0.0:443 ssl;
  server_name             enoent.fr;

  access_log              /var/log/nginx/localhost.access_log;
  error_log               /var/log/nginx/localhost.error_log info;

  root                    /srv/http/localhost;

  ssl_certificate         /etc/nginx/ssl/enoent.fr.crt;
  include                 ssl.conf;
}

The first server block is here to do the redirection, as our inital server only listens on port 443.

Virtual hosts

As we saw in the SSL part, we can define as many server blocks as we want. Each of them is able to respond to requests targeting different hostnames or ports. We also saw earlier the include directive, allowing us to include a file in another.

With this in mind, it’s pretty simple to set up a vhost pool from which we can enable or disable some of them easily. Simply put a file per vhost in a directory, and include it to enable the corresponding vhost, or remove the include to disable it.

Here are some templates for different virtual hosts, each one containing only the minimum (no SSL-specific settings, for example).

Static HTML

We already saw earlier how to define a virtual host when we set up our main nginx.conf file:

_$CONFDIR/vhosts/statichtml.conf

server {
  listen                      0.0.0.0;
  server_name                 enoent.fr;

  access_log                  /var/log/nginx/localhost.access_log;
  error_log                   /var/log/nginx/localhost.error_log info;

  root                        /srv/http/localhost;
}

The only interesting directive here is the root one. It will map the root of the web server to this local folder. A request for http://enoent.fr/my_awesome_page.html will return the content of /srv/http/localhost/my_awesome_page.html.

Reverse proxy

A reverse proxy may be useful when you have a web server already running, and want to expose it somewhere else. Let’s say we have a NAS on our local network, its web ui being accessible on http://nas.local:8080, and we want to expose it on http://nas.enoent.fr, on the default HTTP port:

_$CONFDIR/vhosts/reverseproxy.conf

server {
  listen                              0.0.0.0;
  server_name                         nas.enoent.fr;

  access_log                          /var/log/nginx/nas.access_log;
  error_log                           /var/log/nginx/nas.error_log info;

  location / {
    proxy_headers_hash_max_size       1024;
    proxy_headers_hash_bucket_size    128;
    proxy_pass                        http://nas.local:8080;
  }
}

The location / block here defines a behaviour for all requests matching nas.enonet.fr/*. In our case, that’s all of them, as we only have one location block.

Inside of it, we have some settings for our reverse proxy (maximum headers size), and the really interesting part: the proxy_pass entry, which defines where are redirected the incoming requests.

PHP

To allow PHP applications to work, we’ll need a PHP interpreter. More specifically, we’ll use PHP-FPM. PHP-FPM is a FastCGI PHP processor. It’s a daemon listening on a socket, waiting for PHP scripts, and returning the PHP output. The configuration of PHP-FPM is out of this article scope, but we’ll need to have it running, and note where it can be acceded (a local Unix socket, or a TCP socket, either remote or local).

We need to define a behaviour for PHP files, telling nginx how to process them:

$CONFDIR/php.conf

location ~ ^(.+\.php)(.*)$ {
  include                 fastcgi_params;
  fastcgi_pass            unix:/run/php-fpm/php-fpm.sock;
  fastcgi_split_path_info ^(.+\.php)(.*)$;
  fastcgi_param           PATH_INFO                   $fastcgi_path_info;
  fastcgi_param           SCRIPT_FILENAME             $document_root/$fastcgi_script_name;
}

This file specifies how files with a .php extension will be processed. Nginx will split the arguments and filename, and pass them to the PHP-FPM socket, which here is listening on the Unix socket at /run/php-fpm/php-fpm.sock. For a TCP socket, the line 3 would need to be changed to something like this:

$CONFDIR/php.conf - TCP socket

location ~ ^(.+\.php)(.*)$ {
  include                 fastcgi_params;
  fastcgi_pass            127.0.0.1:9000;
  fastcgi_split_path_info ^(.+\.php)(.*)$;
  fastcgi_param           PATH_INFO                   $fastcgi_path_info;
  fastcgi_param           SCRIPT_FILENAME             $document_root/$fastcgi_script_name;
}

Next, to define a vhost hosting some PHP scripts, we simply need to include this file:

$CONFDIR/vhosts/php.conf

server {
  listen                      0.0.0.0;
  server_name                 my-awesome-php-app.enoent.fr;

  access_log                  /var/log/nginx/my-awesome-php-app.access_log;
  error_log                   /var/log/nginx/my-awesome-php-app.error_log info;

  root                        /srv/http/localhost;
  include                     php.conf;
}

Rack

Rack-based applications need Passenger to work. Passenger is pretty similar to PHP-FPM, but its configuration with nginx is easier. Note that it needs to be built in nginx.

To enable it, we need to tweak our http block in $CONFDIR/nginx.conf to specify our Passenger root directory and path to the ruby executable:

$CONFDIR/nginx.conf

worker_processes                  auto;

events {
  worker_connections              1024;
}

http {
  proxy_send_timeout              600s;
  proxy_read_timeout              600s;
  fastcgi_send_timeout            600s;
  fastcgi_read_timeout            600s;
  include                         mime.types;
  default_type                    application/octet-stream;
  sendfile                        on;
  keepalive_timeout               0;
  gzip                            on;
  index                           index.html index.htm;
  client_max_body_size            2048m;

  passenger_root                  /usr/lib/passenger;
  passenger_ruby                  /usr/bin/ruby;
}

Once this is done, to set up a Rack vhost, we just need to enable Passenger on it, and define which environment we want to use for Rails applications:

$CONFDIR/vhosts/rack.conf

server {
  listen              0.0.0.0;
  server_name         rack-app.enoent.fr;

  access_log          /var/log/nginx/rack-app.access_log;
  error_log           /var/log/nginx/rack-app.error_log info;

  root                /srv/http/rack-app/public;

  passenger_enabled   on;
  rails_env           production;
}

Note that the directory set as root must match the public directory of your Rack application.

Using all of these templates

Once we have written our vhosts definition files in $CONFDIR/vhosts, enabling or disabling one is really easy. We just need to include the corresponding file in the http block of our $CONFDIR/nginx.conf file:

$CONFDIR/nginx.conf

worker_processes                  auto;

events {
  worker_connections              1024;
}

http {
  proxy_send_timeout              600s;
  proxy_read_timeout              600s;
  fastcgi_send_timeout            600s;
  fastcgi_read_timeout            600s;
  include                         mime.types;
  default_type                    application/octet-stream;
  sendfile                        on;
  keepalive_timeout               0;
  gzip                            on;
  index                           index.html index.htm;
  client_max_body_size            2048m;

  passenger_root                  /usr/lib/passenger;
  passenger_ruby                  /usr/bin/ruby;

  include                         vhosts/static_html.conf;
  include                         vhosts/reverse_proxy.conf;
  include                         vhosts/php.conf;
  include                         vhosts/rack.conf;
}

Obviously, if we don’t include any Rack vhost, we don’t need the lines 20 and 21 as they are Passenger-specific.

We can name our vhosts files whatever we like, and create as many as we need. Having the general configuration split in reusable files allows an easy maintenance. When deploying a new PHP application, we just need to include php.conf, and not think “where is my PHP-FPM listening again?”. It just works.