WordPress Deployment Part 5: Atomic Deployments

WordPress Deployment Part 5: Atomic Deployments

In my last article we looked at how to use some popular hosted services to set up automated deployments for our WordPress site.

In this article, we’re going to look at a specific type of deployment that can be used to eliminate downtime during a deployment and make your site always available to your users.

Atomic Deployments

The idea of atomicity comes from the world of databases where a transaction executes completely or not at all (to avoid partial updates being executed). In deployment terms, this means that a deployment will only be “published” once it has finished successfully without having any effect on the existing code running on the server.

Why Use Atomic Deployments?

Atomic deployments are particularly useful in situations when a deployment might take some time (e.g. when you have lots of files to deploy or slow migration to run) or if you have a requirement of minimal downtime.

For example, if you run a high traffic e-commerce site the last thing you want is for your customers to see the dreaded White Screen Of Death during your deployment (especially if they are in the middle of checkout). In theory, this could happen at any point during the deployment. While uploading files to the server, while running commands such as composer install etc. We have even run into this issue ourselves for deliciousbrains.com 😬

Errors during a deployment

Another advantage is that you no longer need to worry about setting maintenance windows and having to hold off on rolling out updates until specific times when your site is quiet. This might be especially important if you need to deploy a time-sensitive security fix, for example.

How does it work?

Usually, atomic deployments work by triggering a script on your server after all of your files have been uploaded to your server. The process looks like this:

  1. Deployment files are uploaded to a directory on the server (not the live site directory). Sometimes called /deploy-cache.
  2. The contents of /deploy-cache are copied to a new directory in the /releases directory. The /releases directory contains a list of recent deployments, each with a unique name to identify them (normally a timestamp or git commit hash).
  3. The /current directory is then symbolic linked to the new version directory in /releases. The /current directory is the directory that your web server points to for the live site. This is the “atomic” step.
  4. Older releases are then removed from the /releases directory to save on disk space.

One of the advantages of keeping a short history of releases in the /releases directory is that it makes it easy to rollback to an old version of your site if you need to. As a rollback would literally just be step 3 from the list above, it means that a rollback is also an “atomic” step and would have no downtime.

One Big Caveat

This all sounds great. But before you jump in headfirst you should be aware of one big caveat of structuring your site using the above format. If the /current directory is just a symlink to a directory in the /releases directory, any files that are not part of your deployed code (i.e. not stored in your Git repo) will be stored in the /releases deployment directory. This means that, when you come to deploy your application next time, any files stored in your old /releases deployment directory won’t be moved across to the new /releases deployment directory when the /current directory symlink is updated.

Using a WordPress example, say you have deployed your site and have the /current directory symlinked to:

/releases/1

When you upload a file to WordPress it will be stored in:

/releases/1/wp-content/uploads

However, when you deploy your site again the /current directory symlink will point to:

/releases/2

WordPress will then, of course, fail to find any files in /releases/2/wp-content/uploads as those files don’t exist on the filesystem. So how do we solve this issue?

Preparing WordPress

The solution to this problem is actually fairly simple. We just need to move the wp-content directory out of the /releases deployment directory and up to a level when it can be shared by every release. In the end, the directory structure might look like this:

  • /current
  • /deploy-cache
  • /releases
  • /wp-content

We can then symlink the /wp-content directory to the /releases deployment directory to make sure WordPress can still access this directory.

Deployment Script

So let’s get down to it. Here is the deployment script we are going to use to enable atomic deployments on our WordPress site:

# Using a timestamp here but this can be any unique identifier
DEPLOYMENT_ID=`date +%s`

echo "Creating: releases/$DEPLOYMENT_ID"
mkdir releases/$DEPLOYMENT_ID
cp -r deploy-cache/ releases/$DEPLOYMENT_ID;

echo "Copying wp-content plugins + themes"
rm -rf wp-content/mu-plugins
rm -rf wp-content/plugins
rm -rf wp-content/themes
cp -r deploy-cache/wp-content/mu-plugins wp-content/mu-plugins
cp -r deploy-cache/wp-content/plugins wp-content/plugins
cp -r deploy-cache/wp-content/themes wp-content/themes

echo "Linking wp-content"
rm -rf releases/$DEPLOYMENT_ID/wp-content
ln -s $(pwd)/wp-content releases/$DEPLOYMENT_ID/wp-content

echo "Linking current to release: $DEPLOYMENT_ID"
rm -f current
ln -s releases/$DEPLOYMENT_ID current

echo "Removing old releases"
cd releases && ls -t | tail -n +11 | xargs rm -rf
cd ../

Stepping through this script:

  • First, we need an identifier for the current deployment DEPLOYMENT_ID. I’ve used a timestamp as an example but this can be any unique identifier (e.g. a git commit hash).
  • Next, we create /releases deployment directory and copy the contents of deploy-cache into it.
  • Next, we copy the latest versions of the plugins and themes directories to the top level wp-content directory. This ensures any plugins/themes stored in Git or installed by composer are correctly deployed.
  • Next, we symlink the wp-content directory to the new /releases deployment directory.
  • Next, we remove the /current symlink and create the new /current symlink pointing to our new /releases deployment directory.
  • Finally, we remove any old /releases directories, leaving the 10 most recent deployments.

Note: I’m assuming here that you have already uploaded the deployment files to the /deploy-cache directory and this script is being triggered after the upload is complete. The actual triggering of the deployment script can be done in many ways. I’ve discussed a few possibilities in previous articles in this series, such as using WebHooks, a CI service or using a hosted service.

Atomicadabra

Hopefully, by this point, you’ll be able to see how easy it is to set up atomic deployments for your own WordPress site. For most sites, atomic deployments might be overkill and just add unnecessary complexity to the deployment process. However, if you run a high traffic e-commerce site, for example, atomic deployments might be a great way to reduce downtime while deploying your WordPress site.

One other thing I should mention is that the script above is very basic and only covers the actual “atomic deployment” part of the process. Most deployment scripts will probably end up being more advanced and need to add extra steps (e.g. running composer install, running migrations, clearing the cache etc.).

Have you ever used atomic deployments to deploy WordPress before? After reading this, are you interested in trying atomic deployments? Got any hints or tips to share? Let us know in the comments.

Author

Gilbert Pellegrom

Gilbert loves to build software. From jQuery scripts to WordPress plugins to full blown SaaS apps, Gilbert has been creating elegant software his whole career. Probably most famous for creating the Nivo Slider.

100% No-Risk 30-Day Money Back Guarantee

If for any reason you are not happy with our product or service, simply let us know within 30 days of your purchase and we'll refund 100% of your money. No questions asked.