While nginx at the core is designed to be a standard reverse proxy and HTTP web server, we can take it much further and use nginx as a central part in our toolchain, if we look into some of the more esoteric modules as well as the ones not included in the default compile. Thankfully, these modules are very often included in the binary packages provided by repositories, so regardless of which method was used to install nginx, they should be available for us to play with.
Compressing site assets is one of the most important methods to optimize the perceived load time of a first time visitor, and even for subsequent page loads when compressing the HTML backend response.
Gzipping the JavaScript, CSS, and HTML responses is of utmost importance if load time is considered important, which naturally means that nginx offers this as a core feature. If we include the optional gzip static module, we can optimize this process even further by compressing the assets ahead of time, so that nginx can merely serve the static gzip file instead of having to compress it on-the-fly.
To start off with, let's look at how to enable normal on-the-fly gzip compression.
gzip on; gzip_min_length 100; gzip_proxied expired no-cache no-store private auth; gzip_comp_level 5; gzip_types text/plain text/css text/xml text/javascript application/xml application/xml+rss application/x-javascript image/x-icon; gzip_disable "msie6";
These directives are valid in an http
context, which means that if we specify them in the http
block they will apply to every server
block, thus enabling us to specify compression only once. Using our knowledge of nginx inheritance from the Quick Start section we can still override this on a server or location basis if required by simply setting the gzip
directive to off
.
The different directives are as always explained in detail in the documentation; however, here's a brief overview of what each does:
Directive |
Description |
---|---|
|
On or off, that is enables or disables gzipping. |
|
This is the minimum response size in bytes before nginx will compress the response. It Defaults to 20 bytes. |
|
This defines if nginx should compress the response when nginx is behind other proxy software, such as Varnish or HAProxy. It defaults to off. |
|
This defines the gzip compression level, default being |
|
The mime types to compress. Text/html is always compressed if gzipping is enabled. To compress everything use |
|
Regex matched against the user agent to determine when to not compress in case the user agent is buggy. |
Using the pre-gzipping module has the advantage of saving CPU resources, as the site assets will already be stored in a compressed format instead of having to be compressed on each request. Making use of the pre-gzipping module is both simpler and more complicated at the same time. More complicated as we have to gzip the files ourselves, but simpler as there are far less configuration directives. To enable the precompressed gzip module we simply use the following configuration:
gzip_static on; gzip_proxied expired no-cache no-store private auth; gzip_disable "msie6";
Immediately, we'll see that the only new directive is really gzip_static
which, like the gzip
directive, takes an on
or off
value to enable or disable it.
Gzipping files is a bit outside the scope of this book. It can either be done by hand using the command line gzip application, or automated as part of a build process, but it has to be done outside of nginx.
It's noon and you've just sat down for lunch when your monitoring service sends you a text message saying your start-up's newly launched web service is down. Seconds later your cofounder texts you in a panic that the website is down, and just as his submissions to HackerNews and Reddit got on the front page too. Ars Technica and The Next Web are currently writing articles covering your start-up and the world is literally about to go under if you don't get the website online immediately.
Enter the micro cache. The concept is that any page which doesn't contain user specific information should be cached in nginx, so that the backend application isn't even touched. This relieves load on the backend and allows most applications to handle far more traffic. Normally, an application will have to be written with caching in mind to handle invalidation of cached pages whenever content updates. The micro cache concept handles this by only caching things for a short period of time. If traffic spikes to 20 requests per second, and the micro cache is set to expire after 10 seconds, that's 200 requests the backend application did not have to handle, which makes micro caching a good tool to use when in a pinch.
While the concept of micro cache is simple, the execution can be a bit more complicated depending on the application. The key aspect is to only cache pages that contain no user specific information. If no such thing exists, it's very simple, otherwise we'll need to control when to cache and when not to cache.
There are two approaches to do this. The first is to use the built-in FastCGI cache or the equivalent for the other modules, such as proxy, uWSGI, SCGI, and so on. The second is to use Memcached as a cache, which is agnostic to the proxy method.
The difference between the two methods is that the built-in FastCGI cache is read and write, while Memcached cache is read-only. Essentially, it becomes a question of where the responsibility for writing to the cache lies. With the built-in FastCGI cache the logic is placed in the nginx config, while with Memcached the logic is placed in the application, as it will need to write to the cache itself.
Lets start with the Memcached scenario, as that's simpler from an nginx point of view and largely similar in construct for us to build on later. A basic Memcached micro cache would look like the following in the nginx configuration:
server { root /var/www; location / { try_files $uri /index.php$is_args$args; } location ~* \.php$ { default_type text/html; charset utf-8; if ($request_method = GET) { set $memcached_key $request_method$request_uri; memcached_pass host:11211; error_page 404 502 504 = @nocache; } if ($request_method != GET) { fastcgi_pass backend; } } location @nocache { fastcgi_pass backend; } }
In the preceding configuration, the important aspects take place inside the location to handle PHP requests. Specifically, the variable $memcached_key
is the most important, as this defines the key to request from Memcached.
A potential complication here is if pages with user data and without user data share the same request URI. In this case, extra configuration is needed to check if a page contains user data. This is always application specific, but common methods are checking for cookies via $http_cookie
or checking the URL arguments through $args
.
Another thing to notice is that only GET
requests use the cache, anything not a GET
request will instead fastcgi_pass
to our backend, thus bypassing the cache.
If a request passes all the validation and is sent to Memcached and Memcached returns a 404 not found status, error_page
will send the request to the @nocache
named location, which will fastcgi_pass
to our backend. The backend is then responsible for populating the proper key in Memcached for the next request to use.
As the application is writing to the cache here, remember to set the cache expire time to something low enough that we won't have stale cache entries for long when the application date updates.
Using the built-in caches is very similar in construct to the previous config example. The main difference is that not only do we have to define when to read from the cache, but also when to write to it. A typical configuration would look like the following:
fastcgi_cache_path /var/cache/nginx levels=1:2 keys_zone=microcache:5m max_size=500m; server { root /var/www; location / { try_files $uri /index.php$is_args$args; } location ~* \.php$ { set $no_cache ""; # Verify request method is GET or HEAD. if ($request_method !~ ^(GET|HEAD)$) { set $no_cache "1"; } # Check if a nocache cookie is set, for instance after handling a POST. if ($http_cookie ~* "_nocache") { set $no_cache "1"; } fastcgi_no_cache $no_cache; fastcgi_cache_bypass $no_cache; fastcgi_cache microcache; fastcgi_cache_key $request_method$request_uri; fastcgi_cache_valid 200 5s; fastcgi_cache_use_stale updating; fastcgi_pass backend; } }
As can be seen, the concept is largely the same. Set up the cache keys_zone
, figure out whether to cache or not and finally set the cache key. To fully explain what's going on, let's have a look at what the different directives actually do.
Directive |
Description |
---|---|
|
Sets the path to store cached responses under. Also names the key zone associated with this cache path and specifies how much metadata and data can be stored there. In this example, |
|
Specifies whether to write to the cache or not. Anything other than an empty string or the value numeric |
|
Specifies whether to read from the cache or not. Anything other than an empty string or the numeric value |
|
Specifies |
|
The key used to identify data in the cache. |
|
Sets the caching time for a given response code. In this example, we want to cache only 200 responses and we will cache them for 5 seconds. Our application can override this directive using the |
|
Specifies when the cache will use a cache entry after it's expired. In this example, we use |
While nginx can certainly be used as the only reverse proxy in our stack, there are scenarios where we might want to use alternative software in front of nginx because we have in-house expertise or are already using them. Popular choices here are Varnish and HAProxy.
In this case we can have nginx handle such a scenario transparently using the optional module Real IP. With this we can have nginx transparently replace the variables referencing an IP with the IP of the proxy, thus keeping logs and the configuration of the same while giving us the ability to turn frontends on and off.
There are only three directives associated with the real IP module, thus making it fairly simple to implement and understand.
set_real_ip_from 192.168.1.0/24; set_real_ip_from 192.168.2.1; set_real_ip_from 2001:0db8::/32; real_ip_header X-Forwarded-For; real_ip_recursive on;
Directive |
Description |
---|---|
|
This specifies an IP to enable the real IP module from. Preventing random people from pretending to be a frontend to nginx. This can be specified multiple times. |
|
This specifies the header to get the real IP from. X-Forwarded-For and X-Real-IP are the most commonly used. This defaults to X-Real-IP. |
|
This specifies the IP to use. If off, this will use the last address in header defined by |
nginx has a feature called X-Accel which is meant as a replacement for the mod_sendfile
functionality found in Apache httpd and lighttpd. The concept is mostly the same. A request is sent to a backend application, which can then do whatever it needs to do, for instance it might log a download or validate user credentials. Once the backend application is done doing its work it issues a non-standard HTTP header X-Accel-Redirect
with a path to the file relative to the document root. nginx will detect this header and look for a matching location based on the path sent. An example of this would be a PHP backend application issuing a header X-Accel-Redirect, that is, /video/birthday/dad.mp4
.
In nginx, we would then have the following configuration:
server { root /var/www; location /video { root /mnt/data; } }
In this scenario, nginx would then look for the file at the path /mnt/data/video/birthday/dad.mp4
. If the file is not found it will send a 404 status error; if the file is found it will start sending the file to the end user, thus relieving the backend application of this.
nginx has a number of X-Accel headers available.
Header |
Description |
---|---|
X-Accel-Redirect |
Specifies a URI relative to the root directive in nginx configuration to the file to send to the user. |
X-Accel-Buffering |
Specifies whether to allow nginx to buffer the connection or not. Turn off if doing Comet style application. Defaults to yes. |
X-Accel-Charset |
Specifies the character set of the connection. Defaults to utf-8. |
X-Accel-Expires |
Used to control whether nginx will cache the application response or not. Defaults to off. |
X-Accel-Limit-Rate |
Specifies a rate limit for the connection. |
To do a GeoIP lookup, nginx will need the MaxMind GeoIP database. Both the paid and free versions are compatible with this module. The free version can be downloaded from:
http://dev.maxmind.com/geoip/geolite
Every directive in this module has to be defined in the http
section and looks like the following:
geoip_country /var/data/GeoIP.dat; geoip_city /var/data/GeoLiteCity.dat; geoip_proxy 192.168.2.0/24; geoip_proxy_recursive on;
Directive |
Description |
---|---|
geoip_country
|
Specifies the path to the country level GeoIP database. |
geoip_city
|
Specifies the path to the city level GeoIP database. This database also contains the data from the country database. |
geoip_org
|
Specifies the path to the organization level GeoIP database. The GeoIP organization database is a paid-only database that nginx also supports. |
geoip_proxy
|
When nginx is used behind other proxy software, this can be used to specify the IP of that proxy and have nginx do a lookup on the IP in X-Forwarded-For instead. |
geoip_proxy_recursive
|
Functionally similar to |
When the proper database is loaded into nginx, the following variables will become available to be used through the config, for instance in the access log or to be passed on to a backend.
Variable |
Description |
---|---|
$geoip_country_code $geoip_city_country_code |
Variable name depends on the database specified. Contains the two letter country code. |
$geoip_country_code3 $geoip_city_country_code3 |
Variable name depends on the database specified. Contains the three letter country code. |
$geoip_country_name $geoip_city_country_name |
Variable name depends on the database specified. Contains the full country name. |
$geoip_city_continent_code
|
Contains the two letter code for the continent. Only available in city database. |
$geoip_dma_code
|
Contains US region DMA code. Only available in city database. |
$geoip_latitude
|
Contains the latitude of the users location. Only available in city database. |
$geoip_longitude
|
Contains the longitude of the users location. Only available in city database. |
$geoip_region
|
Contains the two symbol country region code. Only available in city database. |
$geoip_region_name
|
Contains the full country region name. Only available in city database. |
$geoip_city
|
Contains the full city name. Only available in city database. |
$geoip_postal_code
|
Contains the postal code of the city. Only available in city database. |
$geoip_org
|
Contains the organization name. Could for instance be a university. Only available in organization database. |
There are two ways to limit requests in nginx, concurrent requests and frequency of requests. Both can be used simultaneously and multiple times on different factors. For instance, we can limit concurrent requests per IP while we limit concurrent requests per server block.
To achieve this, nginx has two modules; one which limits concurrency and the other which limits frequency.
To limit concurrent requests, we use the limit conn module. The concept is fairly simple, we create a memory zone based on a variable and nginx will then track concurrent requests grouped by this variable. We could, for instance, use the $server_name
variable to limit concurrent requests per vhost, or we could use $binary_remote_addr
to limit on a users IP.
limit_conn_zone $binary_remote_addr zone=perip:5m; server { location /download/ { limit_conn perip 1; limit_conn_log_level error; } }
Directive |
Description |
---|---|
|
This creates the memory zone. This also specifies the variable to limit based on as well as the maximum size of the memory zone. |
|
This specifies which zone to limit by and how many concurrent connections to allow. |
|
This specifies the log level required before the module will log that the concurrent connection limit was exceeded. This defaults to error. Generally, it is not advised to set it lower unless needed, as it can quickly flood the error log and hide more useful data. |
To limit the frequency of connections we can use the limit req module. It's syntactically similar with only some minor changes to control rate instead of concurrency.
limit_req_zone $binary_remote_addr zone=one:5m rate=1r/s; server { location /search/ { limit_req zone=one burst=5; limit_req_log_level error; } }
Directive |
Description |
---|---|
|
This creates the memory zone. This specifies the variable to limit based on the variable used as well as the maximum size of the memory zone and the rate at which connections should be allowed. Requests exceeding this rate will be buffered until they reach the limit set by burst at which point they will return 503 instead. |
|
This specifies which zone to limit by and the size of the request buffer, called burst. |
|
This specifies the log level required before module will log that the connection frequency limit was exceeded. This defaults to error. Generally, it is not advised to set it lower unless needed, as it can quickly flood the error log and hide more useful data. |
Streaming videos with nginx is extremely easy. nginx has two optional modules for streaming videos, FLV and MP4, which enable it to stream flash video formats and MP4 containers with H.264/AAC encoding. These modules are compatible with all the traditional Flash and HTML5 players available today.
The FLV module is the simplest of the two and contains only a single directive. To enable it, simply specify flv
in a location as follows:
location ~ \.flv$ { root /var/www/video; flv; }
That's literally everything there is to FLV streaming on the nginx side. If the .flv
files are properly prepared with metadata and keyframes, they should stream smoothly and be seekable with this.
The MP4 module is pretty much exactly the same with only a few extra directives for additional control.
location ~ \.mp4$ { root /var/www/video; mp4; mp4_buffer_size 512k mp4_max_buffer_size 10m; }
The buffers control how much memory nginx can use to process the file. This is only limiting during metadata parsing where a large buffer may be required. For this the maximum buffer size becomes relevant. If it's set too small, nginx will output a 500 status error and log the error as:
/var/www/video/file.mp4" mp4 moov atom is too large: 12583268, you may want to increase mp4_max_buffer_size
Version 1.3.13 introduced connection upgrading support to nginx, which means WebSocket support. As WebSockets use the standard HTTP protocol for the initial handshake, nginx can make WebSocket support part of the standard proxy module. This means that all the features available to normal HTTP backends are also available to WebSocket proxying.
The configuration required for handling connection upgrading is as follows:
location /chat/ { proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; }
A few things to notice about WebSocket support are that they can time out just like any other HTTP proxied request. WebSockets are affected by proxy_read_timeout
that defaults to 60 seconds. The keepalive feature in nginx is not of use here, as keepalive pings are empty packets and as such contain no data for nginx to pass to the backend. To combat this, you either need to raise the time out, or implement your own keepalive ping message. The added benefit of the latter solution is that it will also function as a health check for the connection itself.