
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
Maxime (not verified) on 25th April 2014 - Permalink
Billy Davies on 22nd September 2014 - Permalink
Benjamin Jeanjean (not verified) on 6th October 2014 - Permalink
Billy Davies on 22nd February 2016 - Permalink