Skip to main content

Capistrano 3 Tutorial Series Part 3 - Combining Capistrano 3 and Drush for deploying Drupal powered websites

It's time for the long overdue final installment of our Capistrano 3 tutorial series. To recap the preceding parts have been:

This installment will provide a more 'real world' example of using Capistrano to deploy a Drupal website. This allows us to illustrate how you might tailor a Capistrano deploy script to your own website.

It's worth noting that this tutorial was written for Capistrano v3.0.1. The current version of Capistrano is v3.2.1 but the contents of this tutorial is still applicable.

The main deploy script we finished with in Part 2 was as follows:

set :application, 'example'
set :repo_url, "svn+ssh://svn.zodiacmedia.co.uk/example"

set :ssh_options, {
    user: 'deploy'
}

set :scm, :svn

set :format, :pretty
set :log_level, :debug

set :keep_releases, 5

namespace :deploy do
    after :finishing, 'deploy:cleanup'
end

And our staging deploy script was as follows:

set :stage, :staging

server 'staging.zodiacmedia.co.uk', roles: %w{web app db}, port: 22

set :deploy_to, '/var/www/staging.example.com'

For the purpose of this tutorial we're assuming you're already familiar with how to use Drush. For those of you who aren't, Drush is short for 'Drupal Shell', basically it's a tool for running Drupal administration functionality on the Unix command line. If you're responsible for running a Drupal site and aren't using Drush yet I strongly recommend you look into adopting it.

Writing a tailored Cap deploy script is all about using Capistrano hooks. The following line from our deploy script is an example of a Capistrano hook in action:

namespace :deploy do
    after :finishing, 'deploy:cleanup'
end

What we're telling Capistrano to do here is run the task deploy:cleanup after the task deploy:finishing has run. In this syntax deploy is a Ruby namespace and cleanup and finishing are tasks within this namespace. You can see all of the tasks in the deploy namespace within the deploy.rb file in Capistrano's source code. Looking at this code you can see that the cleanup task is in fact called by default in the deploy:finishing task. This means we don't need to make this call in our standard deploy script so we can remove it. It's slightly misleading that the default generated deploy script makes this call. Investigating a bit further reveals that this trivial bug has since been resolved in Capistrano's codebase (https://github.com/capistrano/capistrano/commit/aae79dbd80960a08e2940c4…) and is no longer present in Capistrano release 3.1.0 onwards.

Cutting to the chase, if 'Example.com' were a Drupal powered site we'd use the following deploy script:

set :application, 'example'
set :repo_url, "svn+ssh://svn.zodiacmedia.co.uk/example"

set :ssh_options, {
    user: 'deploy'
}

set :scm, :svn

set :format, :pretty
set :log_level, :debug

set :keep_releases, 5

set :drupal_file_public_path, "sites/default/files"
set :drupal_file_private_path, "sites/default/files/private"
set :drupal_file_temporary_path, "../../shared/tmp"

namespace :drupal do
    desc "Include creation of additional Drupal specific shared folders"
    task :prepare_shared_paths do
        on release_roles :app do
            execute :mkdir, '-p', "#{shared_path}/sites/default/files"
            execute :mkdir, '-p', "#{shared_path}/tmp"
        end
    end

    desc "Include creation of additional folder for backups"
    task :prepare_database_backup_path do
        on release_roles :app do
            execute :mkdir, '-p', "#{deploy_to}/database_backups"
            execute :chmod, '750', "#{deploy_to}/database_backups"
        end
    end

    desc "Link shared files"
    task :link_shared_files do
        on release_roles :app do
            execute :ln, '-s', "#{shared_path}/sites/default/files", "#{release_path}/sites/default/files"
        end
    end

    desc "Update file permissions to follow best security practice: https://drupal.org/node/244924"
    task :set_permissions_for_runtime do
        on release_roles :app do
            execute :find, "#{release_path}", '-type f -exec', :chmod, "640 {} ';'"
            execute :find, "#{release_path}", '-type d -exec', :chmod, "2750 {} ';'"
            execute :find, "#{shared_path}/sites/default/files", '-type f -exec', :chmod, "660 {} ';'"
            execute :find, "#{shared_path}/sites/default/files", '-type d -exec', :chmod, "2770 {} ';'"
            execute :find, "#{shared_path}/tmp", '-type d -exec', :chmod, "2770 {} ';'"
        end
    end

    desc "Create site settings.php"
    task :create_site_settings_file do
        on release_roles :app do
            execute :find, "#{release_path}/sites/default/ -maxdepth 1 -type f -not -name '#{fetch(:drupal_settings_file)}' | xargs rm"
            execute :ln, '-s', "#{release_path}/sites/default/#{fetch(:drupal_settings_file)}", "#{release_path}/sites/default/settings.php"
        end
    end

    desc "Backup the database"
    task :backupdb do
        on release_roles :app do
            release_name = Time.now.utc.strftime("%Y%m%d.%H%M%S")
            execute :drush, '-r', "#{deploy_to}/current sql-dump --gzip --result-file=#{deploy_to}/database_backups/#{release_name}.sql.gz"
        end
    end

    desc "Run Drupal database migrations if required. This applies database updates for modules installed on filesystem but DB updates haven't been run yet."
    task :updatedb do
        on release_roles :app do
            execute :drush, "-r #{deploy_to}/current updatedb -y"
        end
    end

    desc "Clear the drupal cache"
    task :cache_clear do
        on release_roles :app do
            execute :drush, "-r #{deploy_to}/current cc all"
        end
    end
        
    desc "Set the site offline"
    task :site_offline do
        on release_roles :app do
            execute :drush, "-r #{deploy_to}/current vset --exact maintenance_mode 1"
        end
    end

    desc "Set the site online"
    task :site_online do
        on release_roles :app do
            execute :drush, "-r #{deploy_to}/current vdel -y --exact maintenance_mode"
        end
    end

    desc "Set file system variables"
    task :set_file_system_variables do
        on release_roles :app do
            execute :drush, "-r #{deploy_to}/current vset --exact file_public_path #{drupal_file_public_path}"
            execute :drush, "-r #{deploy_to}/current vset --exact file_private_path #{drupal_file_private_path}"
            execute :drush, "-r #{deploy_to}/current vset --exact file_temporary_path #{drupal_file_temporary_path}"
        end
    end
end

namespace :deploy do
    desc "Set file system variables"
    task :after_deploy_check do
        invoke "drupal:prepare_shared_paths"
        invoke "drupal:prepare_database_backup_path"
    end
        
    desc "Set file system variables"
    task :after_deploy_updated do
        invoke "drupal:link_shared_files"
        invoke "drupal:set_permissions_for_runtime"
        invoke "drupal:create_site_settings_file"
        invoke "drupal:cache_clear"
        invoke "drupal:backupdb"
        invoke "drupal:updatedb"
    end
        
    after :check, "deploy:after_deploy_check"

    after :started, "drupal:site_offline"
    
    after :updated, "deploy:after_deploy_updated"

    after :finished, "drupal:site_online"
end

And the corresponding 'live' and 'staging' multistage deploy scripts are respectively as follows:

server "www.example.com", :app, :web, :db, :primary => true

set :deploy_to, "/var/www/www.example.com"
set :drupal_settings_file, "settings.php.live"
server "staging.zodiacmedia.co.uk", :app, :web, :db, :primary => true

set :deploy_to, "/var/www/staging.example.com"
set :drupal_settings_file, "settings.php.staging"

A lot to digest here! I believe the task names and inline task descriptions mean that the script is accessible to competent developers so I won't go through it line by line because that will be too painful. Salient points include:

We define our Drupal website's 'shared paths', (the files folders we want to maintain across releases) at the start of the file. We then incorporate their creation into the deploy:setup Capistrano task and also synchronise files exported from SVN into these locations upon deploy.

We're using a symbolic link to create the settings.php config file for the Drupal site so that staging and live have their own unique version of this file stored in SVN.

We define what to do on_rollback for some tasks. These lines of code are executed if there is an error in the release process and the task they are defined in has already run.

Happy deploying!