hatestheinternet
post image is floppy disks for breakfast by Blude

This Ghetto CDN

Since my dev environment runs Apache and only has 1 public IP, I use it for the entire ghetto CDN setup. This post is going to go over how I set it up and deal with things like CORS and limiting what static and content can and can't do. You should also note that using the VirtualHosts below exactly as is will automatically respond to hostnames of content and static with any domain, so you should check for any possible conflicts.

The naming convention I use for my virtual hosts is pretty simple: static and content plus environment domain. Looking at my dev environment, dev.hatestheinternet.com, we'll be referencing static.dev.hatestheinternet.com and content.dev.hatestheinternet.com.

First, let's take a look at static:

<VirtualHost *:80>
    ServerName static.domain.com
    ServerAlias static.*
    DocumentRoot /web/drupal/docs
    AccessFileName .htaccess.static
</VirtualHost>

This block defines a virtual host that will respond to anything the server receives for a host whose name starts with static. Since this will mostly be used for CSS and JavaScript, it will be serving both site-specific and multi-side-wide files, and uses the same DocumentRoot as Drupal itself.

If you don't want that behaviour feel free to change it to whatever you want, but make sure you keep AccessFileName. This instructs the virtual host to look for a completely different .htaccess file so we don't need to go tinkering with Drupal's because, well, it's much different:

Options -Indexes
Options +FollowSymLinks

DirectoryIndex doesnotexist.html

ErrorDocument 404 default

This block makes sure that we set some default options and ensure that directories will never be served, and that we use the default Apache 404 handler.

RemoveHandler .php .phtml .php3
RemoveType .php .phtml .php3
php_flag engine off

Here we completely remove any trace of the PHP engine because all we're doing with this virtual host is serving static files.

RewriteCond %{HTTP:Accept-Encoding} gzip
RewriteCond %{REQUEST_FILENAME}\.gz -s
RewriteRule ^(.*)\.css $1\.css\.gz [QSA]

RewriteCond %{HTTP:Accept-Encoding} gzip
RewriteCond %{REQUEST_FILENAME}\.gz -s
RewriteRule ^(.*)\.js $1\.js\.gz [QSA]

RewriteRule \.css\.gz$ - [T=text/css,E=no-gzip:1]
RewriteRule \.js\.gz$ - [T=application/javascript,E=no-gzip:1]

<filesmatch "(\.js\.gz|\.css\.gz)$"="">
    Header set Content-Encoding gzip
    Header append Vary Accept-Encoding
</filesmatch>

The mouthful above really serves a simple purpose: If the browser requests either a CSS or JavaScript file, accepts gzipped responses, and there's a pre-gzipped version of that asset available with a size greater than 0, serve it instead and set the Content-Encoding appropriately. Since both of these types of assets (even minified) contain many similar strings, they compress like nobody's business and gzip will save a bundle when the bandwidth bill comes around.

SetEnvIf Host ^static\.(.+)$ APEX_HOST=$1
SetEnvIf Host \.([^\.]+\..{2,6})\/?$ MAIN_DOMAIN=$1

Header always set Access-Control-Allow-Origin "http://%{APEX_HOST}e"

This is CORS, or Cross Origin Resource Sharing. Since we're technically going to be loading assets from different domains, we need to set this otherwise, instead of that pretty custom font, it'll be Times New Roman (or the web safe default you should have chose) all day erryday.

A lot of "advice" you see will recommend setting this to * under certain circumstances for certain files, but a lot of advice is bullshit: It's dead nuts simple to set this thing properly, so just do it.

RewriteEngine On

RewriteCond %{HTTP_HOST} ^static\.(.+)$
RewriteRule . - [S=1,E=APEX_HOST:%1]

RewriteRule . - [R=403,E=NOEXPIRE:1,L]

Here, we figure out our apex host. As mentioned, we'll be using static.dev.whatever.com, so we need a 0-ish knowledge way of getting dev.whatever.com for the next step. This step also returns permission denied unless it's being accessed with an HTTP Host: header of static.[something]. Note that we expect to live behind a CDN, so we set a NOEXPIRE environment variable to control caching headers.

RewriteCond %{REQUEST_FILENAME} ^\. [OR]
RewriteCond %{REQUEST_FILENAME} !\.(css|js|jpe?g|png|gif|ico|svgz?|ttf|otf|woff2?|eot|html?|txt|swf|gz|json)$ RewriteRule . - [R=404,E=NOEXPIRE:1,L]

Finally, we check against a whitelist of extensions to make sure we're allowed to serve the requested file. Anything that's not any of these we straight up 404/Not Found. It's tempting to 403/Permission Denied but, within the context of this server, if it's not something we're allowed to serve, it just doesn't exist. Period. We also do not, under any circumstances, serve dot-files.

RewriteCond %{REQUEST_URI} ^/assets/
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule . http://%{ENV:APEX_HOST}%{REQUEST_URI} [QSA,R=307,E=NOEXPIRE:1,L]

This is one of those Drupal-specific things I warned you about. I use the Advanced Aggregation module to combine and minify JS and CSS and, for its dynamic aggregate generation to work, we issue a redirect back to the apex host to trigger its generation.

Something that may also stand out is the result code I use. In addition to setting my a no cache header, I use the relatively new-ish 307/Temporary Redirect result code. This is AWS (and, I'd imagine, most CDNs)-specific in that the redirect to generate the asset won't be cached.

Header set Cache-Control "max-age=2678400" env=!NOEXPIRE
Header always set Cache-Control "max-age=0" env=NOEXPIRE

 At the very tail end of things, after it's all said and done, set the appropriate cache header. For things that expire, we set the maximum age to 2,678,400 seconds which I seem to remember being around 31 days. This seems to be a good number for Drupal and, remember, this stuff probably won't change as often as you think.

Now that I've droned on and made a post considerably longer than I'd intended, I'll tell you what: Content is basically the same thing, without a type limitation, and a 1 year expiry. If you're cool with that, buzz this page's child for the Drupal image rendition rewrite if needed and move on, otherwise let's get our content virtual host virtually hosting.