Capistrano: Drupal deployments made easy, Part 1
Automate your Drupal deployments with Capistrano for efficient, error-free processes. Learn setup and best practices in our step-by-step guide.
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