How to enable Brotli with Rails and Nginx

Hans Otto Wirtz edited this page May 5, 2018 · 16 revisions

With Firefox, Chrome, Opera, Safari and Edge now supporting Brotli, this tutorial will explain how to make use of it with Rails.

When you deploy a Rails app in production, you probably run bundle exec rake assets:precompile.

This takes all your assets, fingerprints them, puts them into the public/assets folder and also gzips them.

Let's say we have a javascript file, application.js. When you precompile the assets, Sprockets will first get the hash of the file, e.g. 475ed8544a27b1698f25996ed14442e4eb783d4633b685c0319777213d6db193. For this article, let's call it 4..3. Then, it copies the application.js to public/assets/application-4..3.js. Next, it compresses it, creating a public/assets/application-4..3.js.gz file.

Now, if your browser wants to get that file, it will make a GET request to public/assets/application-4..3.js. Almost every browser will include a request header: Accept-Encoding: gzip. This means we don't have to send plain text over the wire, we can just send the gzipped file.

In Nginx, when you have gzip_static on;, that's exactly what happens. When it gets the request for application-4..3.js, it will first look for application-4..3.js.gz, and if that file exists, it will send it, along with the Content-Encoding: gzip header.

If the file isn't there, it will just send the application-4..3.js file, without the Content-Encoding header. (Except if you use gzip on;, then it will compress the application-4..3.js on the fly).

The newer browsers will send a longer header: Accept-Encoding: gzip, br. Now the server can choose if it wants to send a gzip or a brotli compressed file.

With a new pull request of Sprockets, you can now hook into the precompiling process by adding an Exporter.

To install this version of sprockets, you will need to add this to your Gemfile:

gem 'sprockets', '>= 4.0.0.beta3', github: 'rails/sprockets'
gem 'sprockets-rails', '>= 3.1.0'
gem 'sprockets-exporters_pack', '~> 0.1.2'

If you rely on sass-rails, make sure it is >= 5.0.6, otherwise sprockets will complain.

Then, in your application.rb:

config.assets.configure do |env|
  env.register_exporter %w(text/css application/javascript image/svg+xml), Sprockets::ExportersPack::BrotliExporter
end

config.assets is actually a Sprockets::Environment.

Now we need to set up nginx. For this tutorial, I will use Passenger and Ubuntu 16.04, but it can be anything, really.

Follow the steps at Installing NGINX open source, but stop before the Configuring the Build Options step.

We need to add two modules to nginx, Passenger and ngx_brotli.

You need to make sure Passenger is installed on your system. Here is a guide for that: Installing Passenger.

Then, download ngx_brotli and libbrotli.

$ git clone https://github.com/bagder/libbrotli
$ git clone https://github.com/google/ngx_brotli

Once you have done all of that, your folder should look like this:

$ ls
libbrotli  nginx-1.10.1  ngx_brotli  pcre-8.39  zlib-1.2.8

Maybe there's also OpenSSL in there, but by default nginx uses the system libraries.

Execute these commands to install libbrotli:

$ sudo apt-get install libtool autoconf automake
$ cd libbrotli
$ ./autogen.sh
$ ./configure
$ make
$ sudo make install
$ ldconfig

And these to install nginx, finally:

$ cd nginx-1.10.1
$ ./configure --prefix=/etc/nginx --with-http_ssl_module --with-pcre=../pcre-8.39 --with-zlib=../zlib-1.2.8 --with-http_v2_module --with-http_gzip_static_module --add-module=$(passenger-config --nginx-addon-dir) --add-module=../ngx_brotli
$ make
$ sudo make install

(Passenger and Nginx: Installing Passenger as an Nginx module)

Now nginx should be installed in /etc/nginx. Passenger and ngx_brotli are ready to be used (along with HTTP/2, SSL and gzip)

Now edit a file in /etc/nginx/sites-enabled. I called it ottomatisch.conf:

server {
    listen 443 ssl http2;
    server_name ottomatisch.be;

    root /home/apps/ottomatisch/current/public;

    passenger_enabled on;

    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_prefer_server_ciphers on;
    ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH';
    ssl_certificate /etc/letsencrypt/live/ottomatisch.be/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/ottomatisch.be/privkey.pem;

    gzip on;
    gzip_http_version 1.1;
    gzip_vary on;
    gzip_comp_level 1;
    gzip_proxied any;
    gzip_types text/plain text/css application/json application/javascript application/x-javascript text/javascript;

    brotli on;
    brotli_types text/plain text/css application/json application/javascript application/x-javascript text/javascript;
    brotli_comp_level 1;

    location /assets/ {
        gzip_static on;
        brotli_static on; # this will make nginx use the .br files
        expires max;
        add_header Cache-Control public;
    }
}

server {
    listen 80;
    server_name ottomatisch.be www.ottomatisch.be *.ottomatisch.be;
    return 301 https://ottomatisch.be$request_uri;
}

As you can see I used Let's Encrypt, Brotli only works with https! If it's just http, the Accept-Encoding header does not include br.

Now you can deploy your Rails app. Running bundle exec rake assets:precompile will also generate .br files, and those will be found by brotli_static on;

If you test this, you should turn brotli off; and gzip off;, otherwise the files will be compressed on the fly, but you won't notice the difference from the browser. It only requires a lot more CPU on the server.

If you want to test if it works, just run this script:

#!/bin/bash

export LINK='https://ottomatisch.be/assets/...'

echo none
echo $(curl $LINK --silent --write-out "%{size_download}\n" --output /dev/null)

echo gzip
echo $(curl $LINK --silent -H "Accept-Encoding: gzip" --write-out "%{size_download}\n" --output /dev/null)

echo br
echo $(curl $LINK --silent -H "Accept-Encoding: br" --write-out "%{size_download}\n" --output /dev/null)

echo gzip, br
echo $(curl $LINK --silent -H "Accept-Encoding: gzip, br" --write-out "%{size_download}\n" --output /dev/null)

The size of br should be the smallest, and equal to gzip, br.

Clone this wiki locally
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.
Press h to open a hovercard with more details.