A little over a year ago the ActiveLAMP website had undergone a major change -- we made the huge decision of moving away from using Drupal to manage its content in favor of building it as a static HTML site using Jekyll, hosted on Amazon S3. Not only did this extremely simplify our development stack, it also trimmed down our server requirements to the very bare minimum. Now, we are just hosting everything on a file storage server like it's 1993.
A few months ago we identified the need to restructure our URL schemes as part of an ongoing SEO campaign. As easy as that sounds, this, however, necessitates the implementation of 301 redirects from the older URL scheme to their newer, more SEO-friendly versions.
I'm gonna detail how I managed to (1) implement these redirects
quite easily using an nginx
service acting as a proxy, and (2) achieve parity between our local and production environments while keeping everything light-weight with the help of Docker
.
Nginx vs Amazon S3 Redirects
S3 is a file storage service offered by AWS that not only allows
you to store files but also allows you to host static websites in conjunction with Route 53.
Although S3 gives you the ability to specify redirects, you'll need to use S3-specific configuration and routines.
This alone wouldn't be ideal because not only
would it tie us to S3 by the hips, but it is not a methodology that we could
apply to any other environment (i.e. testing and dev environments on our local
network and machines). For these reasons, I opted to use nginx
as a very
thin reverse proxy to accomplish the job.
Configuring Nginx
Rather than compiling the list of redirects manually, I wrote a tiny Jekyll
plugin that can do it faster and more reliably. The plugin allows me to specify certain things
within the main Jekyll configuration file and it will generate the
proxy.conf
file for me:
# _config.yml
nginx:
proxy_host: <S3 bucket URL>;
proxy_port: 80
from_format: "/:year/:month/:day/:title/"
# Optional array of custom redirects:
redirects:
- { from: "^/splash(.*)", to: "/$1" type: redirect }
With this in place, I am able to generate the proxy.conf
by simply issuing this
command:
> jekyll nginx_config > proxy.conf
This command will produce a proxy.conf
file which will look like this:
# Redirects from old URLs to new ones.
rewrite ^/2008/09/21/drupalcampla\-revision\-control\-presentation/?(\?.*)?$ /blog/drupal/drupalcampla-revision-control-presentation$1 permanent;
# ... plus approx. 40 more `rewrite` directives
# Typo fix: aysnc => async
rewrite ^/blog/development/aysnchronous\-php\-with\-message\-queues/?(\?.*)?$ /blog/development/asynchronous-php-with-message-queues/$1 permanent;
# Reverse proxy logic:
location / {
proxy_set_header Host <S3 bucket URL>;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://<S3 bucket URL>:80;
}
You probably noticed that this is not a complete nginx configuration. However
all that is need to be done is to define a server
directive which will import
this file:
# server.conf
server {
listen 80;
include /etc/nginx/proxy.conf;
}
Here we have an nginx proxy listening on port 80
that knows how to redirect old URLs to the new ones
and pass on any request to the S3 bucket on AWS.
From this point, I was able to change the DNS records on our domain to point to the nginx proxy service instead of pointing directly at S3.
Check out the documentation for more ways of specifying redirects and more advanced usage.
Docker
Spinning up the proxy locally is a breeze with the help of Docker. Doing this in Vagrant and a provisioner would require a lot of boilerplate and code. With Docker, everything (with the exception of the config file that is automatically generated), is under 10 lines!
The Dockerfile
Here I am using the official nginx
image
straight from DockerHub but added some minor modifications:
# build/nginx/Dockerfile
FROM nginx
# Remove the "default" server
RUN rm /etc/nginx/conf.d/default.conf
# Add our own server -- the reverse proxy + the config
ADD server.conf /etc/nginx/conf.d/
ADD proxy.conf /etc/nginx/
The build/nginx
directory will contain everything the nginx proxy will need:
the server.conf
that you saw from the previous section, and the proxy.conf
file which was
generated by the jekyll nginx_config
command.
Automating it with Grunt
Since we are using generator-jekyllrb
, a Yeoman generator for Jekyll sites which uses Grunt to run a gamut
of various tasks, I just had to write a grunt proxy
task which does all the
needed work when invoked:
// Gruntfile.js
...
grunt.initConfig({
...
local_ip: process.env.LOCAL_IP,
shell: {
nginx_config: {
command: 'jekyll nginx_config --proxy_host=192.168.0.3
--proxy_port=8080 --config=_config.yml,_config.build.yml --source=app > build/nginx/proxy.conf'
}
docker_build: {
command: 'docker build -t jekyll-proxy build/nginx'
},
docker_run: {
command: 'docker run -d -p 80:80 jekyll-proxy'
}
},
...
});
...
grunt.registerTask('proxy', [
'shell:nginx_config',
'shell:docker_build',
'shell:docker_run'
]);
This requires
grunt-shell
With this in place, running grunt proxy
will prepare the configuration, build
the image, and run the proxy on http://192.168.99.100
where
192.168.99.100
is the address to the Docker host VM on my machine.
Note that this is a very simplified version of the actual Grunt task config that we actually use which just serves to illustrate the meager list of commands that is required to get the proxy configured and running.
I have set up a GitHub repository that replicates this set-up plus the actual Grunt task configuration we use that adds more logic around things like an auto-rebuilding the Docker image, cleaning up of stale Docker processes, configuration for different build parameters for use in production, etc. You can find it here: bezhermoso/jekyll-nginx-proxy-with-docker-demo.