Drupal

Capistrano: Drupal deployments made easy, Part 1

I'm a big fan of having an automated deployment process. It's really the web development analog to the "one step build process", as described in the Joel Test. In the past I have used various shell scripts to perform this task, but I have recently become a convert to Capistrano (or "cap" for short). With Capistrano, uploading your code to the test server is as simple as typing cap deploy. When you're ready to launch in production, it's just cap production deploy. From capify.org: Simply put, Capistrano is a tool for automating tasks on one or more remote servers. It executes commands in parallel on all targeted machines, and provides a mechanism for rolling back changes across multiple machines. In detail, here are the features that got me hooked. There's a lot more that cap can do, and I'll describe some more tricks in part 2 of this post. Atomic deployments with error checking. Cap uses a set of symlinked directories, and the links are updated during the final step. It also won't allow a deployment to keep plowing ahead if an intermediate step fails. This makes your deployment atomic; it will either fail or succeed entirely. Fast rollback. If something does go wrong, getting back to the previous state is as simple as cap deploy:rollback. Parallel execution. If you use multiple servers in a load-balanced environment, cap can make managing them easier. Multistage deployments. "Stages" are different server instances of your code. You may have different servers for development, content entry, and production. With the Multistage extension, cap can share code for common tasks between these stages.


Filed under:

I'm a big fan of having an automated deployment process. It's really the web development analog to the "one step build process", as described in the Joel Test. In the past I have used various shell scripts to perform this task, but I have recently become a convert to Capistrano (or "cap" for short). With Capistrano, uploading your code to the test server is as simple as typing cap deploy. When you're ready to launch in production, it's just cap production deploy.

From capify.org:

Simply put, Capistrano is a tool for automating tasks on one or more remote servers. It executes commands in parallel on all targeted machines, and provides a mechanism for rolling back changes across multiple machines.

In detail, here are the features that got me hooked. There's a lot more that cap can do, and I'll describe some more tricks in part 2 of this post.

  • Atomic deployments with error checking. Cap uses a set of symlinked directories, and the links are updated during the final step. It also won't allow a deployment to keep plowing ahead if an intermediate step fails. This makes your deployment atomic; it will either fail or succeed entirely.
  • Fast rollback. If something does go wrong, getting back to the previous state is as simple as cap deploy:rollback.
  • Parallel execution. If you use multiple servers in a load-balanced environment, cap can make managing them easier.
  • Multistage deployments. "Stages" are different server instances of your code. You may have different servers for development, content entry, and production. With the Multistage extension, cap can share code for common tasks between these stages.

Step-by-step

First, you'll need to install Capistrano, as well as the multistage extensions contained in capistrano-ext:

gem install capistrano
gem install capistrano-ext

Next, you'll need to add a couple of files to your project (the files' contents are listed at the end of this post). I lay out projects like this:

|-- capfile
|-- config
|   |-- deploy
|   |   |-- development.rb
|   |   `-- production.rb
|   `-- deploy.rb
`-- drupal
    `-- index.php

Now edit the paths and project name in config/deploy.rb and config/deploy/development.rb to suit your environment.

You're now ready to run cap deploy:setup. This task only needs to be run once, prior to the first deployment. It will create the necessary directories on your server, which will ultimately look something like the tree below. (Until your first deployment, there may not be anything in the releases directory, nor a link from current). Also notice the setup task created a local_settings.php file. The utility of this is described in my post on stage-specific settings. The web server document root in this example is /var/www/current.

|-- current -> /var/www/releases/20091023202918
|-- releases
|   |-- 20091023191450
|   `-- 20091023202918
`-- shared
    |-- cached-copy
    |-- default
    |   |-- files
    |   `-- local_settings.php
    `-- system

Finally, run cap deploy.

cap deploy will speedily upload a new release of your project, then link it to the current document root if all went well.

Watch for part 2, where I'll cover some extra tasks included in this Capfile, as well as managing multi-site and multi-stage deployments.

config/deploy.rb:

### This file contains project-specific settings ###
 
# The project name.
set :application, "myproject"
 
# List the Drupal multi-site folders.  Use "default" if no multi-sites are installed.
set :domains, ["default"]
 
# Set the repository type and location to deploy from.
set :scm, :subversion
set :repository,  "https://example.com/trunk/drupal"
 
# Use a remote cache to speed things up
set :deploy_via, :remote_cache
 
# Multistage support - see config/deploy/[STAGE].rb for specific configs
set :default_stage, "development"
set :stages, %w(production development)
 
# Generally don't need sudo for this deploy setup
set :use_sudo, false

config/deploy/development.rb

### This file contains stage-specific settings ###
 
# Set the deployment directory on the target hosts.
set :deploy_to, "/var/www/#{application}"
 
# The hostnames to deploy to.
role :web, "devel.example.com"
 
# Specify one of the web servers to use for database backups or updates.
# This server should also be running Drupal.
role :db, "devel.example.com", :primary => true
 
# The username on the target system, if different from your local username
# ssh_options[:user] = 'alice'
 
# The path to drush
set :drush, "cd #{current_path} ; /usr/bin/php /data/lib/php/drush/drush.php"

Capfile

This file does the real work. If you aren't a ruby programmer, don't panic; neither am I (yet). Hopefully, this is general-purpose enough that it will work for you out of the box. Additionally, individual tasks can be overridden in deploy.rb or development.rb – so once you have a Capfile you like the same file can be reused across all your projects.

load 'deploy' if respond_to?(:namespace) # cap2 differentiator
Dir['vendor/plugins/*/recipes/*.rb'].each { |plugin| load(plugin) }
load 'config/deploy.rb'
 
require 'capistrano/ext/multistage'
 
namespace :deploy do
 
  # Overwritten to provide flexibility for people who aren't using Rails.
  desc "Prepares one or more servers for deployment."
  task :setup, :except => { :no_release => true } do
    dirs = [deploy_to, releases_path, shared_path]
    domains.each do |domain|
      dirs += [shared_path + "/#{domain}/files"]
    end
    dirs += %w(system).map { |d| File.join(shared_path, d) }
    run "umask 02 && mkdir -p #{dirs.join(' ')}" 
  end
 
  desc "Create settings.php in shared/config" 
  task :after_setup do
    configuration = <<-EOF
<?php
$db_url = 'mysql://username:password@localhost/databasename';
$db_prefix = '';
EOF
    domains.each do |domain|
      put configuration, "#{deploy_to}/#{shared_dir}/#{domain}/local_settings.php"
    end
  end
 
  desc "link file dirs" 
  task :after_update_code do
    domains.each do |domain|
    # link settings file
      run "ln -nfs #{deploy_to}/#{shared_dir}/#{domain}/local_settings.php #{release_path}/sites/#{domain}/local_settings.php"
      # remove any link or directory that was exported from SCM, and link to remote Drupal filesystem
      run "rm -rf #{release_path}/sites/#{domain}/files"
      run "ln -nfs #{deploy_to}/#{shared_dir}/#{domain}/files #{release_path}/sites/#{domain}/files"
    end
  end
 
  # desc '[internal] Touches up the released code.'
  task :finalize_update, :except => { :no_release => true } do
    run "chmod -R g+w #{release_path}"
  end
 
  desc "Flush the Drupal cache system."
  task :cacheclear, :roles => :db, :only => { :primary => true } do
    domains.each do |domain|
      run "#{drush} --uri=#{domain} cache clear"
    end    
  end
 
  namespace :web do
    desc "Set Drupal maintainance mode to online."
    task :enable do
      domains.each do |domain|
        php = 'variable_set("site_offline", FALSE)'
        run "#{drush} --uri=#{domain} eval '#{php}'"
      end
    end
 
    desc "Set Drupal maintainance mode to off-line."
    task :disable do
      domains.each do |domain|
        php = 'variable_set("site_offline", TRUE)'
        run "#{drush} --uri=#{domain} eval '#{php}'"
      end
    end
  end
 
  after "deploy", "deploy:cacheclear"
  after "deploy", "deploy:cleanup"
 
 
  # Each of the following tasks are Rails specific. They're removed.
  task :migrate do
  end
 
  task :migrations do
  end
 
  task :cold do
  end
 
  task :start do
  end
 
  task :stop do
  end
 
  task :restart do
  end
 
end
 
 
desc "Download a backup of the database(s) from the given stage."
task :download_db, :roles => :db, :only => { :primary => true } do
  domains.each do |domain|
    filename = "#{domain}_#{stage}.sql"
    run "#{drush} --uri=#{domain} sql dump --structure-tables-key=common > ~/#{filename}"
    download("~/#{filename}", "db/#{filename}", :via=> :scp)
  end
end

Similar posts

Get notified on new marketing insights

Be the first to know about new B2B SaaS Marketing insights to build or refine your marketing function with the tools and knowledge of today’s industry.