Page Speed Module: Nginx Setup on Linode

Posted on April 13, 2015 in

Recently I had written an article on the benefits and how to setup Google’s PageSpeed module on Apache. Based on the some of the feedback I had received, I felt it necessary to write a follow up on how to achieve the same results with the nginx web server. I am a big fan of nginx. I won’t go into the details on why I prefer it over Apache, but it’s a new level of performance and speed. Here’s a list of the parts we’ll cover in this article. Please skip ahead to  the section most relevant to you.

Create Linode Instance

I’ve only used Linode in a few instances, but after chatting with Richard Baxter, this seemed like a great place to start. I chose their cheapest plan for this test: a 1GB RAM with 24GB of SSD storage for $10 a month is an incredible deal and sufficient for our test. I’ve been putting off migrating some sites over to Linode, so writing out these instructions are going to finally force me to do it. I won’t walk you through the steps to sign up and create an account as they’ve already have a getting started guide here. For my test install, I chose the Linode 1024 in their Dallas, TX data center.

Once the account was setup, I went to the dashboard and clicked “Deploy an Image” and selected the Ubuntu 14.10 image. Once it’s created, click the “Boot” button on your dashboard. Then you can click the “Remote Access” tab at the top and get your public IP address to login to the shell.

 

Install Nginx with PageSpeed and PHP5-FPM

While there are multiple ways you can install PageSpeed module (PSM) on Apache, with  Nginx you will have to install from source. We will be following these instructions with some minor changes to get our new nginx install ready to go with PSM.

Install dependencies:
RedHat, CentOS, or Fedora

sudo yum install gcc-c++ pcre-dev pcre-devel zlib-devel make unzip

Ubuntu or Debian

sudo apt-get install build-essential zlib1g-dev libpcre3 libpcre3-dev unzip

Then download the most current ngx_pagespeed release. At the time of this writing, it’s 1.9.32.3.

cd
NPS_VERSION=1.9.32.3
wget https://github.com/pagespeed/ngx_pagespeed/archive/release-${NPS_VERSION}-beta.zip
unzip release-${NPS_VERSION}-beta.zip
cd ngx_pagespeed-release-${NPS_VERSION}-beta/
wget https://dl.google.com/dl/page-speed/psol/${NPS_VERSION}.tar.gz
tar -xzvf ${NPS_VERSION}.tar.gz  # extracts to psol/

Download and build nginx with pagespeed support. Update: Added HTTPS support.

cd
# check http://nginx.org/en/download.html for the latest version
NGINX_VERSION=1.6.3
wget http://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz
tar -xvzf nginx-${NGINX_VERSION}.tar.gz
cd nginx-${NGINX_VERSION}/
./configure --add-module=$HOME/ngx_pagespeed-release-${NPS_VERSION}-beta \
  --with-http_ssl_module --with-http_spdy_module \
  --prefix=/usr/local/share/nginx --conf-path=/etc/nginx/nginx.conf \
  --sbin-path=/usr/local/sbin --error-log-path=/var/log/nginx/error.log
make
sudo make install

There are examples on this page for startup scripts for different environments. Since we’re using Ubuntu, this is the command I ran to get our startup script.

#copy/download/curl/wget the init script
sudo wget https://raw.github.com/JasonGiedymin/nginx-init-ubuntu/master/nginx -O /etc/init.d/nginx
sudo chmod +x /etc/init.d/nginx

The following lines are the ones I changed in our new /etc/init.d/nginx setup:

NGINXPATH=${NGINXPATH:-/usr/local}      # root path where installed
NGINX_CONF_FILE="/etc/nginx/nginx.conf" # config file path
PIDSPATH="/var/run"

Inside the /etc/nginx directory, I create a global folder and added two files: common.conf and wordpress.conf. The WordPress file just has a couple typical rules necessary for to run it, but the important part is obviously where it adds PHP support.

/etc/nginx/global/common.conf

# Global configuration file.
# ESSENTIAL : Configure Nginx Listening Port
listen 80;

# ESSENTIAL : Default file to serve. If the first file isn't found,
index index.php index.html index.htm;

# ESSENTIAL : no favicon logs
location = /favicon.ico {
    log_not_found off;
    access_log off;
}

# ESSENTIAL : robots.txt
location = /robots.txt {
    allow all;
    log_not_found off;
    access_log off;
}

# ESSENTIAL : Configure 404 Pages
error_page 404 /404.html;

# ESSENTIAL : Configure 50x Pages
error_page 500 502 503 504 /50x.html;
location = /50x.html {
    root /usr/share/nginx/www;
}

# SECURITY : Deny all attempts to access hidden files .abcde
location ~ /\. {
    deny all;
}

# PERFORMANCE : Set expires headers for static files and turn off logging.
location ~* ^.+\.(js|css|swf|xml|txt|ogg|ogv|svg|svgz|eot|otf|woff|mp4|ttf|rss|atom|jpg|jpeg|gif|png|ico|zip|tgz|gz|rar|bz2|doc|xls|exe|ppt|tar|mid|midi|wav|bmp|rtf)$ {
    access_log off; log_not_found off; expires 30d;
}

/etc/nginx/global/wordpress.conf

# WORDPRESS : Rewrite rules, sends everything through index.php and keeps the appended query string intact
location / {
    try_files $uri $uri/ /index.php?q=$uri&$args;
}

# SECURITY : Deny all attempts to access PHP Files in the uploads directory
location ~* /(?:uploads|files)/.*\.php$ {
    deny all;
}

# REQUIREMENTS : Enable PHP Support
location ~ \.php$ {
    # SECURITY : Zero day Exploit Protection
    try_files $uri =404;
    # ENABLE : Enable PHP, listen fpm sock
    fastcgi_split_path_info ^(.+\.php)(/.+)$;
    fastcgi_pass unix:/var/run/php5-fpm.sock;
    fastcgi_index index.php;
    include fastcgi_params;
}

Then this is what my /etc/nginx/nginx.conf looks like. There’s more things I can add here obviously, like gzip support, but we’ll leave that for later since we’re just testing PSM.

user www-data;
worker_processes  6;
pid        /var/run/nginx.pid;
events {
    worker_connections  1024;
}
http {
    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    keepalive_timeout  65;
    #ngx_pagespeed module settings
    pagespeed on;
    pagespeed FileCachePath /var/ngx_pagespeed_cache;
    include /etc/nginx/sites-enabled/*;
}

Based on this config we need to create a couple directories.

mkdir /var/ngx_pagespeed_cache
mkdir /etc/nginx/sites-available
mkdir /etc/nginx/sites-enabled

To enable/disable sites, I found this little script in a StackOverflow Answer that should handle the job nicely for us.

#!/bin/bash

##
#  File:
#    nginx_modsite
#  Description:
#    Provides a basic script to automate enabling and disabling websites found
#    in the default configuration directories:
#      /etc/nginx/sites-available and /etc/nginx/sites-enabled
#    For easy access to this script, copy it into the directory:
#      /usr/local/sbin
#    Run this script without any arguments or with -h or --help to see a basic
#    help dialog displaying all options.
##

# Copyright (C) 2010 Michael Lustfield <mtecknology@ubuntu.com>

# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.

##
# Default Settings
##

NGINX_CONF_FILE="$(awk -F= -v RS=' ' '/conf-path/ {print $2}' <<< $(nginx -V 2>&1))"
NGINX_CONF_DIR="${NGINX_CONF_FILE%/*}"
NGINX_SITES_AVAILABLE="$NGINX_CONF_DIR/sites-available"
NGINX_SITES_ENABLED="$NGINX_CONF_DIR/sites-enabled"
SELECTED_SITE="$2"

##
# Script Functions
##

ngx_enable_site() {
    [[ ! "$SELECTED_SITE" ]] &&
        ngx_select_site "not_enabled"

    [[ ! -e "$NGINX_SITES_AVAILABLE/$SELECTED_SITE" ]] && 
        ngx_error "Site does not appear to exist."
    [[ -e "$NGINX_SITES_ENABLED/$SELECTED_SITE" ]] &&
        ngx_error "Site appears to already be enabled"

    ln -sf "$NGINX_SITES_AVAILABLE/$SELECTED_SITE" -T "$NGINX_SITES_ENABLED/$SELECTED_SITE"
    ngx_reload
}

ngx_disable_site() {
    [[ ! "$SELECTED_SITE" ]] &&
        ngx_select_site "is_enabled"

    [[ ! -e "$NGINX_SITES_AVAILABLE/$SELECTED_SITE" ]] &&
        ngx_error "Site does not appear to be \'available\'. - Not Removing"
    [[ ! -e "$NGINX_SITES_ENABLED/$SELECTED_SITE" ]] &&
        ngx_error "Site does not appear to be enabled."

    rm -f "$NGINX_SITES_ENABLED/$SELECTED_SITE"
    ngx_reload
}

ngx_list_site() {
    echo "Available sites:"
    ngx_sites "available"
    echo "Enabled Sites"
    ngx_sites "enabled"
}

##
# Helper Functions
##

ngx_select_site() {
    sites_avail=($NGINX_SITES_AVAILABLE/*)
    sa="${sites_avail[@]##*/}"
    sites_en=($NGINX_SITES_ENABLED/*)
    se="${sites_en[@]##*/}"

    case "$1" in
        not_enabled) sites=$(comm -13 <(printf "%s\n" $se) <(printf "%s\n" $sa));;
        is_enabled) sites=$(comm -12 <(printf "%s\n" $se) <(printf "%s\n" $sa));;
    esac

    ngx_prompt "$sites"
}

ngx_prompt() {
    sites=($1)
    i=0

    echo "SELECT A WEBSITE:"
    for site in ${sites[@]}; do
        echo -e "$i:\t${sites[$i]}"
        ((i++))
    done

    read -p "Enter number for website: " i
    SELECTED_SITE="${sites[$i]}"
}

ngx_sites() {
    case "$1" in
        available) dir="$NGINX_SITES_AVAILABLE";;
        enabled) dir="$NGINX_SITES_ENABLED";;
    esac

    for file in $dir/*; do
        echo -e "\t${file#*$dir/}"
    done
}

ngx_reload() {
    read -p "Would you like to reload the Nginx configuration now? (Y/n) " reload
    [[ "$reload" != "n" && "$reload" != "N" ]] && invoke-rc.d nginx reload
}

ngx_error() {
    echo -e "${0##*/}: ERROR: $1"
    [[ "$2" ]] && ngx_help
    exit 1
}

ngx_help() {
    echo "Usage: ${0##*/} [options]"
    echo "Options:"
    echo -e "\t<-e|--enable> \tEnable site"
    echo -e "\t<-d|--disable> \tDisable site"
    echo -e "\t<-l|--list>\t\tList sites"
    echo -e "\t<-h|--help>\t\tDisplay help"
    echo -e "\n\tIf  is left out a selection of options will be presented."
    echo -e "\tIt is assumed you are using the default sites-enabled and"
    echo -e "\tsites-disabled located at $NGINX_CONF_DIR."
}

##
# Core Piece
##

case "$1" in
    -e|--enable)    ngx_enable_site;;
    -d|--disable)   ngx_disable_site;;
    -l|--list)  ngx_list_site;;
    -h|--help)  ngx_help;;
    *)      ngx_error "No Options Selected" 1; ngx_help;;
esac

Add that script in a new file /usr/bin/nginx_modsite and make it executable.

#To list all the sites
$ sudo nginx_modsite -l
#To enable site "test_website"
$ sudo nginx_modsite -e test_website
#To disable site "test_website"
$ sudo nginx_modsite -d test_website

Depending on where your test site is, create a new “test_website” in /etc/nginx/sites-available and add code similar to this:

server {
    listen 80;
    root /var/www/test/web;
    server_name www.rankhammer.com;
    index index.php;
    include global/wordpress.conf;
    access_log /var/log/nginx/www.rankhammer.com.access.log;
    error_log /var/log/nginx/www.rankhammer.com.error.log;

    # Ensure requests for pagespeed optimized resources go to the pagespeed handler
    # and no extraneous headers get set.
    location ~ "\.pagespeed\.([a-z]\.)?[a-z]{2}\.[^.]{10}\.[^.]+" {
        add_header "" "";
    }
    location ~ "^/pagespeed_static/" { }
    location ~ "^/ngx_pagespeed_beacon$" { }
    pagespeed EnableFilters prioritize_critical_css;
    pagespeed EnableFilters combine_css,combine_javascript;
    #pagespeed EnableFilters defer_javascript;
    pagespeed EnableFilters sprite_images;
    pagespeed EnableFilters rewrite_images;
    pagespeed EnableFilters recompress_png;
    pagespeed EnableFilters convert_png_to_jpeg,convert_jpeg_to_webp;
    pagespeed EnableFilters collapse_whitespace,remove_comments;
}

Notice I have already added some PSM configurations in here. We’re not ready to run it yet, but those are the configurations I typically start with. Here is a list of filters you can use. You might want to experiment with defer_javascript before enabling it. Also, if you don’t have a test site to play with yet, try this really fast WordPress setup based on the roots.io package.

Add PHP Support

Now let’s install the php5-fpm module

sudo apt-get install php5-fpm

Open your /etc/nginx/fastcgi_params file, and if this line is not in there, add it.

fastcgi_param  SCRIPT_FILENAME    $document_root$fastcgi_script_name;

Then open your /etc/php5/fpm/pool.d/www.conf file. If any of these lines are commented out, remove the semi-colon to uncomment them.

listen = /var/run/php5-fpm.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660

Now that this is all setup, restart the two services, and visit the public IP address of your Linode server.

service php5-fpm restart
service nginx restart

You should see this in your response headers:

nginx-psm-response

 

And that’s it! Your website’s will now be auto optimized by the PageSpeed module from Google. I wanted to see the score we’re starting with on a fresh install, and you can’t argue with these numbers. The screenshots below show my mobile and desktop scores using the fast WordPress with Bootstrap install.

psi-mobile

 

psi-desktop

 

 

Additional Configurations

Since we used the same setup as our Apache install from the Moz article I wrote, feel free to check up on the benefits of those configuration settings there. But it this area, I will add a couple additional options that you can add if relevant to your environment.

Mapping Rewrite Domains

I ran across this a while back when trying to use PSM on a site that used Varnish. Some URLs were being rewritten to include port 8080 on the varnish URLs, so I prefer to explicitly map this in my configuration to fix that issue. More information here on mapping domains for things like CDNs, etc.

pagespeed MapRewriteDomain mydomain.com mydomain.com:8080;

 

ASync Google Analytics

I use Google’s Tag Manager wherever I can, but in case you just directly use Google analytics, there’s a filter for that. It does what it says: if the page is setup to load the tracking tag synchronously, it asynchronously loads it instead.

pagespeed EnableFilters make_google_analytics_async;

DNS Prefetch

From the filter documentation, this filter reduces DNS lookup time by adding hints at the beginning of the HTML.

pagespeed EnableFilters insert_dns_prefetch;

Conclusion

And that should be it! The PSM is great for automating the web performance steps we often take (or forget about). This is by no means a reason to skip those steps intentionally, but it’s great to know if this powerful little module is installed, it’s going to make your site that much faster. Post any questions or comments below, and let me know about your experience with Google’s PageSpeed module with nginx.

By Nathan Byloff

Nathan is the CTO for RankHammer. His area of expertise is technical SEO and everything to do with data - collection, analysis, etc. He is driven by automating any reporting task that has to be done more than once.