You are here

Professional deployment of websites using Capistrano - Part 4

Combining Capistrano and Drush for deploying Drupal powered websites

Please note that since Capistrano 3's release in October 2013 this Capistrano 2 based tutorial series has been superseded by an updated Capistrano 3 tutorial series.

Welcome to Part 4 in our four part Capistrano tutorial series. In case you missed them the previous instalments were:

Part 1
What is Capistrano and why is it so good?
Part 2
Secure SSH key based Capistrano website deployment from Subversion for multi-developer teams
Part 3
Using Capistrano for deploying PHP and other none Rails based websites

This final instalment builds on what we've run through previously to demonstrate how you can tailor a Capistrano deploy script to fit your application. We've chosen a Drupal website as an example because:

  • Drupal is very popular.
  • There doesn't seem to be much by way of comprehensive 'how-tos' for combining Capistrano and Drupal.
  • We like using Drupal and so we already have a custom deploy script up our sleeves to write about.

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 functionality you'd normally run through the administration front end of a Drupal site 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.

As a starting point let's look at the Capistrano deploy script we finished with at the end of Part 3:

set :application, "example"

set :stages, %w(production staging)
set :default_stage, "staging" 
require 'capistrano/ext/multistage' 

set :scm, :subversion 
set :repository_root, "svn+ssh://svn.zodiacmedia.co.uk/example" 
set :deploy_via, :export 

#server "staging.zodiacmedia.co.uk", :app, :web, :db 
#set :deploy_to, "/var/www/staging.example.com"
 
set :user, "deploy" 
default_run_options[:pty] = true 
ssh_options[:keys] = [File.join(ENV["HOME"], ".ssh", "id_rsa")] 
ssh_options[:port] = 22 
set :use_sudo, false 

set :keep_releases, 5 
after "deploy:update", "deploy:cleanup" 

set(:tag) { Capistrano::CLI.ui.ask("Enter SVN tag to deploy (or type 'trunk' to deploy from trunk): ") } 
set(:repository) { (tag == "trunk") ? "#{repository_root}/trunk" : "#{repository_root}/tags/#{tag}" }

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

after "deploy:update", "deploy:cleanup"

What we're telling Capistrano to do here is run the task deploy:cleanup after the task deploy:update has run. In this syntax deploy is a Ruby namespace and cleanup and update are tasks within this namespace. You can see all of the tasks in the deploy namespace within this file from Capistrano's source code.

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

set :application, "example"

set :stages, %w(production staging)
set :default_stage, "staging"
require 'capistrano/ext/multistage'

set :scm, :subversion
set :repository_root, "svn+ssh://svn.zodiacmedia.co.uk/example"
set :deploy_via, :export

set :user, "deploy"
default_run_options[:pty] = true
ssh_options[:keys] = [File.join(ENV["HOME"], ".ssh", "id_rsa")]
ssh_options[:port] = 22
set :use_sudo, false

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"

after "deploy:setup",
"drupal:prepare_shared_paths", "drupal:prepare_database_backup_path"

before "deploy:update_code",
"deploy:repository_location"
"drupal:cache_clear"

before "deploy:finalize_update",
"drupal:rsync_shared_files"
after "deploy:finalize_update",
"drupal:set_permissions_for_runtime", "drupal:create_site_settings_file", "drupal:backupdb"

after "deploy:update", "drupal:updatedb", "deploy:cleanup"

namespace :deploy do
    desc "Prompt user for repository location to deploy from"
    task :repository_location, :except => { :no_release => true } do
        set :tag, Capistrano::CLI.ui.ask("Enter SVN tag to deploy (or type 'trunk' to deploy from trunk): ")
        set :tag, "trunk" if eval("#{:tag.to_s}.empty?")
        set :repository, (tag == "trunk") ? "#{repository_root}/trunk" : "#{repository_root}/tags/#{tag}"
    end
    
    desc "Overwrite equivalent native namespace from Capistrano"
    namespace :web do
        task :disable, :except => { :no_release => true } do
            drupal.site_offline
        end

        task :enable, :except => { :no_release => true } do
            drupal.site_online
        end
    end
end

namespace :drupal do
    desc "Include creation of additional folder for backups"
    task :prepare_database_backup_path, :except => { :no_release => true } do
        run "mkdir -p #{deploy_to}/database_backups"
        run "chmod 750 #{deploy_to}/database_backups"
    end

    desc "Include creation of additional Drupal specific shared folders"
    task :prepare_shared_paths, :except => { :no_release => true } do
        run "mkdir -p #{shared_path}/sites/default/files"
        run "mkdir -p #{shared_path}/tmp"
    end

    desc "Synchronise shared files"
    task :rsync_shared_files, :except => { :no_release => true } do
        on_rollback do
            run <<-EOC
                ln -s #{shared_path}/sites/default/files #{current_release}/sites/default/files
            EOC
        end

        run "rsync -av #{latest_release}/sites/default/files/ #{shared_path}/sites/default/files/"
        run "rm -rf  #{latest_release}/sites/default/files"
        run "ln -s #{shared_path}/sites/default/files #{latest_release}/sites/default/files"
    end

    desc "Update file permissions to follow best security practice: https://drupal.org/node/244924"
    task :set_permissions_for_runtime, :except => { :no_release => true } do
        run "find #{latest_release} -type f -exec chmod 640 {} ';'"
        run "find #{latest_release} -type d -exec chmod 2750 {} ';'"
        run "find #{shared_path}/sites/default/files -type f -exec chmod 660 {} ';'"
        run "find #{shared_path}/sites/default/files -type d -exec chmod 2770 {} ';'"
        run "find #{shared_path}/tmp -type d -exec chmod 2770 {} ';'"
    end

    desc "Create site settings.php"
    task :create_site_settings_file, :except => { :no_release => true } do
        run "ln -s #{release_path}/sites/default/#{drupal_settings_file} #{release_path}/sites/default/settings.php"
    end

    desc "Backup the database"
    task :backupdb, :except => { :no_release => true } do
        release_name = Time.now.utc.strftime("%Y%m%d.%H%M%S-") + tag + ".r" + real_revision.to_s
        run "drush -r #{deploy_to}/current sql-dump --gzip --result-file=#{deploy_to}/database_backups/#{release_name}.sql.gz"
    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, :except => { :no_release => true } do
        run "drush -r #{deploy_to}/current updatedb -y"
    end

    desc "Clear the drupal cache"
    task :cache_clear, :except => { :no_release => true } do
        run "drush -r #{deploy_to}/current cc all"
    end

    desc "Set the site offline"
    task :site_offline, :except => { :no_release => true } do
        on_rollback do
            run "drush -r #{deploy_to}/current vdel -y --exact maintenance_mode"
        end

        run "drush -r #{deploy_to}/current vset --exact maintenance_mode 1"
    end

    desc "Set the site online"
    task :site_online, :except => { :no_release => true } do
        run "drush -r #{deploy_to}/current vdel -y --exact maintenance_mode"
    end

    desc "Set file system variables"
    task :set_file_system_variables, :except => { :no_release => true } do
        run "drush -r #{deploy_to}/current vset --exact file_public_path #{drupal_file_public_path}"
        run "drush -r #{deploy_to}/current vset --exact file_private_path #{drupal_file_private_path}"
        run "drush -r #{deploy_to}/current vset --exact file_temporary_path #{drupal_file_temporary_path}"
    end
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!

Comments

Hi, this is a great tutorial ! But I'm a beginner in Capistrano, and many things seems to have change in the new version... For example, i've got a lot of "Don't know how to build task 'deploy:setup'"...

Hi Maxime, You can find the Cap 3 version of this tutorial series at: http://www.zodiacmedia.co.uk/blog/capistrano-3-tutorial-series. Good luck with your Cap 3 work it's great once you get it up and running. Billy

Hi, I think that you should permit copying and pasting of above code examples. Currently it puts some copyright information and inline code in whatever you copy and paste. Not so easy to reproduce :-). Also, I've met an error : "`drupal:rsync_shared_files' is only run for servers matching {:except=>{:no_release=>true}}, but no servers matched". Is it a problem of my default conf? However, nice article ! Thanks for sharing it :-)

Hi Benjamin, Thanks for the positive feedback and for pointing out the copy/paste tagging of snippets from the site. This seems to have been built in behaviour from the 'ShareThis' code we use on the site. We've updated the ShareThis library we were using and I think this is resolved now. With regards to your specific Capistrano issue you need to set some roles for your target deploy servers I believe. For example in the line: server "staging.zodiacmedia.co.uk", :app, :web, :db, :primary => true I'm setting the role of the 'staging.zodiacmedia.co.uk' server to be app, web and db. Many thanks, Billy

Add new comment