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 of the 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:
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:
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:
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:
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
It 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:
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:
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:
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:
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:
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:
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:
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:
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:
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.