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!