Hosting different kinds of apps on nginx
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.