I’ve written about how we used Composer to overlay a dependency management system on our existing WordPress ecosystem. The final step was to actually deploy the site somewhere. For that we turned to Capistrano.

Capistrano

Capistrano is a Ruby application which manages the deployed state of a project. Within a “Capified” project, you define all the information needed to deploy that project somewhere:

  • The location of your remote servers
  • The path to deploy your application
  • The location of your code
  • Any tasks to run on deployment

Think of this as a developer story: we’re describing our expectations for deploying the project. If we’re onboarding a new team member, everything they need to know about deploying a project is contained in these files.

Capistrano runs from your local machine and executes commands over SSH on the remote. Assuming your local SSH environment is already well-defined, this means that you can stand up a Capistrano deployment without any additional configuration on either your local machine or the remote. It’s very lightweight in that regard. Capistrano can forward your SSH key on deploying; if you already have access to your private Git repositories then there’s no additional setup necessary to deploy from them.

Sample WordPress project layout


Capfile
composer.json
composer.lock
config/
	deploy.rb
	deploy/
		staging.rb
		production.rb
Gemfile
Gemfile.lock
public

WordPress

Having inveighed against submodules, I’m going to advocate keeping one around for WordPress core. There are three choices for handling WordPress itself:

  1. Adding it as a Composer dependency itself, using John Bloch’s composer fork.
  2. Forking from GitHub and adding all your Capistrano and Composer configuration to the repository. Whenever WordPress updates merge the updates into your project.
  3. Mounting it as a git submodule, using the official GitHub clone.

The first option has an interesting technical challenge. The gory details is that a Composer installation overwrites the path it’s installing to. Capistrano executes its shared symlink tasks before Composer installs its packages. If you’re keeping the WordPress installation paths standard, as we did, this means that the symlinks for wp-config.php, .htaccess, and the uploads directory are lost on deployment.

The second option cuts against the grain of this whole concept, which is to manage versions in their natural place. We wind up muddying WordPress’ revision history. Also, that method makes it too easy to hack core for fun and proft.

Composer

For installing the Composer dependencies, there’s a community-maintained Composer extension for Capistrano. You simply tell Capistrano where your composer.json file is.

Rollback!

Capistrano creates a releases/ directory on your remote and timestamped subdirectories for each deployment. The “active” deployment is symlinked to current. This means that “rolling back” to an earlier deployment is as simple as changing the symlink to point at a different release. Capistrano includes a built in command to do just that: cap <environment> deploy:rollback. This command also creates a tarball of the “bad” release and archives it.


current -> /var/www/sites/releases/20170424135711
releases/
	20170301101452
	20170303204702
	20170424135711
		public/
			.htaccess -> /var/www/sites/shared/public/.htaccess
			wp-config.php -> /var/www/sites/shared/public/wp-config.php
			wp-content/
				uploads -> /var/www/sites/shared/public/wp-content/uploads
repo/
revisions.log
shared/
	public/
		.htaccess
		wp-config.php
			wp-content/
				uploads/

There’s an important caveat here, pace Dan McKinley. Rollback returns you to the previous code state. Anything that’s happened to the state of the system since then—database changes, user sessions, cached pages—remains the same. I’ve found WordPress to be forgiving in that regard; even downgrading a major version on top of an upgraded database is relatively safe. On a platform such as a Moodle, you’d need to also restore from backup.

One node, two node …

Capistrano makes multi-node deployment simple. For a given environment you define all the remotes, and it executes deployments in parallel. That way you avoid race conditions, where one node has a different version of the code. Capistrano uses a concept of roles, so that you can restrict certain tasks to given nodes:

server 'node0.foo.edu', user: fetch(:user), roles: %w{app web}
server 'node1.foo.edu', user: fetch(:user), roles: %w{web}

The role names are arbitrary. Designating one node as app lets me define tasks that be run once, on one environment, like a major upgrade. Here’s a task definition for the command-line database upgrade for WordPress:

    # Run an upgrade
    task :upgrade do
        on roles(:app) do |host|
            execute "cd #{fetch(:deploy_to)}/current/public && wp core update-db --network"
        end
    end

Tasking Capistrano

You can extend this logic in all kinds of useful ways. We have an asset-heavy base theme; lots of stylesheets and javascripts. Minifying them in source control is a Bad Plan, for several reasons:

  • git diff is rendered meaningless
  • if multiple people need to alter the compiled files, you’ll merge conflict

Capistrano gave us an easy way out. We moved our existing Grunt minification tasks to a separate Gruntfile to be run on deployment, and enabled Npm support. This is the result:

# Npm
set :npm_target_path, -&gt; { release_path.join('public/wp-content/themes/marquis-b
ase') }
...
    # Minify base theme
    before :updated, :grunt do |host|
        on roles(:web) do
            execute "cd #{fetch(:release_path)}/public/wp-content/themes/marquis-base &amp;&amp; grunt deploy --gruntfile Gruntfile-deploy.js"
        end
    end

Note that this task is run on all the nodes, simultaneously, unlike the database upgrade task above.

Deployment as documentation

A Capistrano project is a developer story. Everything a developer needs to know about the project is in the config files: the primary site itself (the WordPress submodule), the themes and plugins (in composer.json), the servers to deploy to, the tasks which must and will be run on any deployment, and the tasks which may be run on or after a deployment, at the developer’s discretion. All encapsulated in a single git repository, with no additional external configuration required. Next up: automating this process within GitLab.