Nginx / Wordpress

WordPress Static Files Cookieless Domain with Nginx

by , , revisited on


We have by far the largest RPM repository with dynamic stable NGINX modules and VMODs for Varnish 4.1 and 6.0 LTS. If you want to install nginx, Varnish and lots of useful modules for them, this is your one stop repository to get all performance related software.
You have to maintain an active subscription in order to be able to use the repository!

Note about HTTP/2

Before we get started, some introduction. With the prevalence of HTTP/2 traffic in 2018, this technique becomes controversial. Take note of the following about separate domain for static file:

  • It will reduce the data payload sent to server in HTTP/1 connections, thus network latency will be improved
  • HTTP/2 clients represent the majority now
  • A separate domain for static will result in an extra DNS lookup
  • You have to use single SSL certificate (or a certificate that lists both domains) for both main and static in order to benefit from connection coalescing
  • HTTP/2 features headers compressions and doesn’t need this technique that much, but you may want to use this technique for other reasons, see below.

I would say you should use cookie-less/separate domain for static if you plan to use a CDN and any of:

  • The CDN you want to use introduces a too high TTFB increase for uncached pages. It means you should not put CDN against the main domain / PHP pages, and it’s better to have requests hit the uncached pages directly.
  • The CDN you want to use simply doesn’t allow to route all requests through it. Cloudflare can serve everything through its servers, but KeyCDN requires a subdomain to be setup.

Either way, you have to be aware that this technique will require extra DNS lookup and reduced HTTP/2 request multiplexing, if coalescing is not available. Reduced multiplexing will happen:

  • For Safari browsers. Safari simply doesn’t do request multiplexing between different hostnames
  • If certificates are different between main and static domains (e.g. when using CDN or other cases when certificates are simply not the same, as in “DNS and certs do not agree”).

Cookieless domain for static WordPress files ensures better cacheability of your website. It’s quite simple to understand how this works on example:

  • Your website will operate on www.example.com subdomain. WordPress will set cookie on that level and not for domain in whole.
  • Your asset files (Javascript, images, CSS files) will be hosted at static.example.com subdomain. This way, WordPress cookies will not apply to those files.

Using cookieless domains makes the static files to be better cached by browsers and content delivery networks. The obvious requirement is that your WordPress site URL should be configured with www prefix, i.e.: https://www.example.com would be your Site URL.

Nginx changes

You will need to configure server block for static subdomain of your website. Then simply point its document root to wp-content directory of your WordPress installation. The following boilerplate configuration is a good starting point. Adjust as necessary:

server {

    listen 8080; 
    listen [::]:8080; 

    server_name static.example.com;

    root /var/www/html/blog/wp-content;

    # Disallow access to any PHP files. We only serve static files here
    location ~ \.php$ {
        return 403;
    }

    etag off;

    add_header Expires "Thu, 31 Dec 2037 23:55:55 GMT";
    add_header Cache-Control "public, max-age=315360000";

    # Allow WordPress to access fonts and other assets from the static subdomain
    location ~* \.(eot|otf|ttf|woff|woff2|cur|gif|ico|jpg|jpeg||png|svgz|webp)$ {
        add_header Access-Control-Allow-Origin "https://www.example.com";
        add_header Expires "Thu, 31 Dec 2037 23:55:55 GMT";
        add_header Cache-Control "public, max-age=315360000";   
    }
}

Note that we have disabled ETag generation for static files. There’s really nothing wrong with ETags in nginx. Nginx doesn’t include inodes in ETag value, only file size and mod time, so it is safe in multi-server environments.

However, we chose to validate static files by their Last-Modified header value instead.

We set the expiration time to maximum. There, we’ve also addressed best practices of far future Expires header. And we simply reduced our HTTP header size by eliminating ETags from response.

Someone might wonder why we’re relying on add_header directive when we could use expires max;. We do it this way in order to ensure that public keyword is included to Cache-Control header. This marks the files to be cacheable better by proxy servers.

We also repeat our add_header directives to deal with common configuration pitfall of its inheritance.
If you care for ancient browsers which are capable of HTTP/1.0 only, then you would also include add_header "Pragma" "public"; to the bunch.

But it doesn’t end there. Let’s review few more necessary steps.

Make changes to Google Analytics

Chances are, you use Google Analytics to track your website visitor statistics. Google Analytics will by default create a cookie that is bound to your domain and all of its subdomains. You need to adjust your tracking Javascript code to fix this. We will tell Google Analytics to use cookie which will apply to WordPress www domain only:

Change ga('create', 'UA-XXXXXXXX-X', 'auto'); to ga('create', 'UA-XXXXXXXX-X', 'static.example.com');.

Apply SSL for your static domain

Static subdomains needs SSL, same to your parent domain. We recommend to use LetsEncrypt for the job. Once you have it, it’s easy to generate SSL certificate with a one line command. You can go further and generate single certificate which be valid for both www and static subdomains:

certbot-auto certonly --webroot \
  -w /var/www/html/ -d example.com -d www.example.com \
  -w /var/www/html/wp-content/ -d static.example.com 

This creates single SSL certificate that you will use on both www and static.

We will now adjust both nginx server blocks to include SSL configuration:

server {

    listen 443 ssl http2; 
    listen [::]:443 ssl http2; 
    ssl_certificate      /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key  /etc/letsencrypt/live/example.com/privkey.pem;
    ...
}

Adjust cookie domain

Open up wp-config.php and put right below opening <?php tag:

define("COOKIE_DOMAIN", "www.example.com");

This tells WordPress that we don’t want the cookies to be bound to all subdomains, only to www.

Change WordPress HTML to link files from static subdomain

Now it’s time to make WordPress actually use our static subdomain for good purpose. We want the static files, i.e. javascript files, images, so on to be linked as https://example.com/example.jpg instead of the main domain. How do we approach this? There are more than a couple of ways to achieve this. Choose the way that is most appropriate for you.

Option #1. Native WordPress way to change static file links

We can instruct WordPress to link all static files from wp-content directory using our static subdomain. Additionally we can “shorten” our URLs a bit by providing plugins directory URL.

Open up wp-config.php and put right below opening <?php tag:

define("WP_CONTENT_URL", "https://static.example.com"); 
define("WP_PLUGIN_URL", "https://static.example.com/plugins");

Adjust existing wp-content URLs to link to static subdomain

I should be honest with you. I don’t like WordPress for storing absolute URLs across its database. It makes changing URLs a cumbersome task to deal with. But we can use the excellent WP-CLI command line tool to work around this design failure 🙂

wp search-replace 'https://www.example.com/wp-content/uploads/' 'https://static.example.com/uploads/'

Now your website will have all the images and plugins assets linked from static.example.com.

Option #2. Plugin to the rescue.

You might have noticed that we changed URL to wp-content directory only. But there are quite more files which are typically linked from wp-includes directory. Those would still be linked from main domain should we stick to option #1.

We can fix re-link as many static files as possible (including wp-content and wp-includes) by using CDN Enabler plugin. It will rewrite every link to non-PHP file to specified domain name, and it’s not limited to wp-content. If you use that, you’d have to adjust document root of static nginx server block to match with the main domain.

Using the plugin requires slightly more load to your server, because it has to buffer the whole page and replace content in the HTML.

Option #3. ngx_pagespeed

Personally, I hate to see long wp-content/uploads links so I try to combine several approaches into an ultimate solution.

We maintain a CentOS repository for ngx_pagespeed plugin for nginx. It has the feature to rewrite assets links found in HTML to whatever we want. Here’s how we would approach this:

pagespeed MapRewriteDomain https://static.example.com/ https://www.example.com/;

Now create a symbolic link from ./wp-content/wp-includes to ./wp-includes (remember, static server block has the root pointed to wp-content).

The WordPress config from option #1 will already output all of the wp-content links from static subdomain.

Now ngx_pagespeed will pick up the generated HTML and fix all the wp-includes assets by putting them under our static subdomain as well.

I’m quite happy with this approach because it allows to shorten WordPress links and still have most of the static files linked properly for both directories in question.

Known issues with using static subdomain in general

  • Autoptimize or W3TC plugins would not know how to handle optimizing assets from static subdomain
  • Query string removal (functions.php hack) might stop working for you. An update is needed to the code
  1. villyskovvilly

    i also need a ssl certificat for static.example.com. http://www.exapple.com can not be used.

    Reply
    • Danila Vershinin

      Yes. That is why in “Apply SSL for your static domain” section, we generate one SSL certificate for everything: static, www and non-www.

      Reply
      • villyskovvilly

        Yes i understand but extension of an certificate is not easy 🙁

        I get the :

        Type: unauthorized
        Detail: Invalid response

        from cerbot

        Reply
        • Danila Vershinin

          It only means that the nginx root for domain that failed authorization did not match to what you have put in -w switch. And there maybe few other reasons, i.e. you have not pointed DNS for static. to the server IP, etc.

          Make sure that your server block for static website root directive is same to what you use in certbot-auto command.

          Basic Certbot stuff! 🙂

          Reply
  2. Vidyut

    Will this work with http/2 Push? Not sure the server can push content hosted on a different subdomain.

    Reply
    • Danila Vershinin

      It won’t. From nginx documentation for http2_push:

      Only relative URIs with absolute path will be processed, for example:

      http2_push /static/css/main.css;
      
      Reply
  3. May

    I did everything explained here but I don’t know the GTmetrix still shows my static.domain.com under use cookie free domain. I don’t know what went wrong.

    Reply
  4. Mejorar la performance de tu WordPress – Luloxi

    […] Utilizar Nginx para corregir Serve Static Content […]

    Reply
  5. AtulHost

    Awesome, just worked in very first attempt.

    Reply

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.