How We Deploy WordPress Themes

Jeremy Felt’s series on deployment workflows earlier this year inspired me to write about and document our process for deploying themes.

Most deployment processes tend to focus on deploying a site or app, whereas when we’re working on a theme, we’re primarily interested in deploying individual packages independent of the platform.


While researching potential solutions, I had a few goals and loose requirements in mind:

  • During development, we should be able to deploy a theme to a testing server so the team is always on the same page.
  • We should be able to easily update our demo servers with the latest version of a theme or push a quick bug fix. Running our themes on a live site also helps us discover any issues before pushing a release out to our customers.
  • Finally, we should be able to push a tagged release to our distribution repository.

Most of all, the solution needs to be frictionless, so it should be:

  • Easy to share among team members
  • Easy to configure and maintain
  • Reusable between projects
  • Secure by not requiring credentials in plaintext or even saved in the repository
  • Able to work with our existing tools and not introduce a new technology into our stack

Choosing a Tool

We already rely heavily on Grunt to automate development tasks, so installing Node.js packages with npm is second nature. While Grunt could handle everything we needed, configuring multiple packages to accomplish a task like this feels a little too fragmented and brittle.

Searching npm turned up Shipit and Flightplan as two promising alternatives. I ended up implementing a deployment task using both to get a feel for how they work and they turned out to be very similar.

Ultimately, I settled on Shipit for a couple of reasons. One, it depends on its own ssh-pool package for connecting to servers, which was easier to configure — it also didn’t require storing credentials in the project repo since it uses existing SSH keys.

The ssh2 dependency in Flightplan is more widely used and should allow for similar functionality, but I had a tougher time configuring it on Windows.

Both tools rely on rsync for copying files to remote servers and it was also easier to work around that requirement in Shipit on Windows.

The Process

Shipit tasks are defined in a shipitfile.js file in the project’s root directory, much like Grunt’s Gruntfile.js or Gulp’s gulpfile.js.

Essentially, all we’re doing is running commands on remote servers over SSH, so there’s really not anything fancy or groundbreaking going on.

As I mentioned earlier, we already use Grunt during development and to package our themes for distribution. Our grunt package task excludes all development files and produces a single zip file that can be uploaded directly in WordPress when installing the theme — which is perfect for deployment in our case.

We just need to copy the zip file to a server, unzip it, and move it where WordPress expects it to be. In practice, it’s slightly more complex.


deploy user is created on each server with limited access to the system. To grant developers deployment privileges, their public SSH key is added to the deploy user’s authorized_keys file.

Directory Structure

The project root directory starts at /srv/www/, with the following subdirectory structure (“Encore” is the name of a theme):

├── deploy
│   └── encore
│       └── releases
│           ├── 20150501103000
│           └── 20150430100000
└── public
    └── wp-content
        └── themes
            └── encore → /srv/www/

The real theme directory in wp-content/themes is actually a symlink that points to the most recent deployment for that particular theme.


After getting the initial set up out of the way, here’s what our deployment task does:

  1. Copies the theme zip package to the deployment location on the staging server (deploy/encore/releases)
  2. Unzips the package to a timestamped directory
  3. Removes the zipped archive file since its no longer needed
  4. Updates the symlink in wp-content/themes to point to the latest release
  5. Cleans older releases so they don’t sit around in perpetuity

Shipit allows us to run that same task against any number of servers, so if we want to share progress while developing a theme, we run shipit staging deploy in a terminal.

If we want to push a new release or fix a bug in one of our demos, we run shipit demo deploy.

And if we’re ready to publish a new release, it’s another simple command. Here’s a sample of what what our shipitfile.js looks like:

var moment = require( 'moment' ),
package = require( __dirname + '/package.json' ),
path = require( 'path' );
module.exports = function ( shipit ) {
staging: {
servers: '',
deployRoot: '/srv/www/',
publicPath: '/srv/www/'
shipit.task( 'deploy', function() {
shipit.archiveFile = + '-' + package.version + '.zip';
shipit.deployPath = shipit.config.deployRoot + + '/releases/';
return createDeploymentPath()
.then( copyProject )
.then( unpackDeployment )
.then( updateSymlink )
.then( cleanOldReleases );
function createDeploymentPath() {
return shipit.remote( 'mkdir -p ' + shipit.deployPath );
function copyProject() {
return shipit.remoteCopy( 'dist/' + shipit.archiveFile, shipit.deployPath );
function unpackDeployment() {
var cmd = [];
shipit.deployTime = moment().utc().format( 'YYYYMMDDHHmmss' );
cmd.push( 'cd ' + shipit.deployPath );
cmd.push( 'unzip -q ' + shipit.archiveFile );
cmd.push( 'mv ' + + ' ' + shipit.deployTime );
cmd.push( 'rm ' + shipit.archiveFile );
return shipit.remote( cmd.join( ' && ' ) );
function updateSymlink() {
var cmd = [];
cmd.push( 'cd ' + shipit.config.publicPath );
cmd.push( 'ln -sn ' + shipit.deployPath + shipit.deployTime + ' ' + + '-temp' );
cmd.push( 'mv -T ' + + '-temp ' + );
return shipit.remote( cmd.join( ' && ' ) );
function cleanOldReleases() {
var cmd = '';
cmd += '(';
cmd += 'ls -rd ' + shipit.deployPath + '*|head -n 5;';
cmd += 'ls -d ' + shipit.deployPath + '*';
cmd += ')';
cmd += '|sort|uniq -u|xargs rm -rf';
return shipit.remote( cmd );
view raw shipitfile.js hosted with ❤ by GitHub

Other Details

When updating symlinks, I create a new symlink before replacing the existing symlink using mv -T to make the switch atomic. One issue I’ve run is that it can take awhile for the theme to fully switch over, but I suspect this has something to do with a cache and may require tweaking the Nginx config. If anyone has any ideas, I’m all ears.

Shipit does have a deployment package that contains some nice features, but it’s somewhat opinionated and didn’t fit our workflow.


  1. I like this approach, but I feel like the Symlink approach could be replaced by version control. Or maybe I am missing something? Either way, great post. Definitely going to check out Shipit.

    1. Hi Matthew, I’ve used git for deployment, but it tends to fall apart pretty quickly when a build step is required and it doesn’t allow for atomic version switching or rollbacks. I’ve also found it more difficult to configure and keep in sync across multiple servers.

      We’ve been using this method for most of the year and it’s worked out pretty well for us, but I’m all ears if you know of a better way. Thanks for reading and commenting!

Leave a Reply

Your email address will not be published. Required fields are marked *