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.
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 😬
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:
- Deployment files are uploaded to a directory on the server (not the live site directory). Sometimes called
- The contents of
/deploy-cacheare copied to a new directory in the
/releasesdirectory contains a list of recent deployments, each with a unique name to identify them (normally a timestamp or git commit hash).
/currentdirectory is then symbolic linked to the new version directory in
/currentdirectory is the directory that your web server points to for the live site. This is the “atomic” step.
- Older releases are then removed from the
/releasesdirectory 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:
When you upload a file to WordPress it will be stored in:
However, when you deploy your site again the
/current directory symlink will point to:
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?
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:
We can then symlink the
/wp-content directory to the
/releases deployment directory to make sure WordPress can still access this directory.
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
/releasesdeployment directory and copy the contents of
- 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
- Next, we remove the
/currentsymlink and create the new
/currentsymlink pointing to our new
- Finally, we remove any old
/releasesdirectories, 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.
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.