Posts Tagged ‘Drupal’

Deploy Drupal with Capistrano, a year later

Here’s the slides for the presentation I gave at the latest Ruby Social Club in Milano.

Two improvements to your Capfiles

I have lately started using a pattern that has become quite common among capistrano users: setting the server names and locations in a task. Doing this allows you to have multiple deployment environments, like development, staging, production, and so on.

desc "deploy to development environment"
task :development do
  set :deploy_to, "/var/apps/#{application}"
 
  role :web, "servername.mikamai.com", :primary => true
  role :app, "servername.mikamai.com", :primary => true
  role :db, "servername.mikamai.com", :primary => true
 
  set :user, "username"
  set :password, "secr3t"
  set :remote_mysqldump, "/usr/bin/mysqldump"
 
  set :db_user, "username"
  set :db_password, "secre7"
  set :db_name, "db_name"
end

This technique has proven itself to be really useful, especially when clients start to ask for deployments on their test servers, and you still want to be able to deploy to your development servers.

While refactoring my Capfiles I also took the time to rewrite the drupal:db namespace, adding the much needed tasks that allow you dump the remote databases and download them to your development box.

  namespace :db do    
    namespace :dump do
      desc "Deletes old database dumps, leaves only the latest on the server"
      task :cleanup, :roles => :db do
        dumps = capture("ls -xt #{shared_path}/dumps").split.reverse
        run "cd #{shared_path}/dumps; rm #{dumps[0..-2].join(" ")}"
      end
 
      desc "Dumps the local database"
      task :local, :roles => :db do
        raise RuntimeError.new("failed dump") unless system "#{local_mysqldump} -u #{local_db_user} --password=#{local_db_password} #{local_db_name} > dump.sql"
      end
 
      namespace :remote do
        desc "Dumps the remote database"
        task :default, :roles => :db do
          filename = "#{Time.now.to_i.to_s}.dump.sql"
          run "cd #{shared_path}/dumps; #{remote_mysqldump} -u #{db_user} --password=#{db_password} #{db_name} > #{filename}"
          run "cd #{shared_path}/dumps; bzip2 #{filename}"
        end          
 
        namespace :download do
          desc "Dumps and downloads the remote database"
          task :default do
            drupal::db::dump::remote::default
            latest
          end
 
          desc "Downloads the latest database dump"
          task :latest, :roles => :db do
            dumps = capture("ls -xt #{shared_path}/dumps").split.reverse
            get("#{shared_path}/dumps/#{dumps.last}", "./#{dumps.last}")
          end
 
        end
 
      end
 
    end

How to ease Drupal development with Capistrano

Drupal is a great piece of software, unfortunately it stores so much stuff in the db that people struggle keeping in sync the development server/box and a staging server to show their customers how the work is proceeding.

Today I will share the Capistrano tasks I use to sync my development box with the staging server. What I basically do is dumping the development db, sending it to the server via capistrano and then use the dump to replace the server’s database.

The following tasks should be used together with the tasks in my Deploying drupal with Capistrano article. I took advantage of deploy:cold not being needed with Drupal, and added a callback to it, so if you want to do a deploy that also updated the database you should use deploy:cold.

You should also have two settings files (usually stored in drupal_root/sites/default), one called settings.development.php, with your local database setup and one called settings.production.php with the remote database setup, the capistrano tasks will take care of choosing the correct one.

# Callbacks
before 'deploy:start', 'drupal:db:import:production'
before 'deploy:restart', 'drupal:configure:production'
before 'deploy:start', 'drupal:configure:production'
before 'deploy:cold', 'drupal:db:dump:development'
 
# DB Stuff
set :mysqldump, "/path/to/mysqldump"
set :local_db_user, "local_mysql_username"
set :local_db_password, "local_mysql_password"
set :local_db_name, "local_db_name"
set :db_user, "remote_mysql_username"
set :db_password, "remote_mysql_password"
set :db_name, "remote_db_name"
 
namespace :drupal do
  namespace :configure do
    task :production do
      sudo "cp #{latest_release}/sites/default/settings.production.php #{latest_release}/sites/default/settings.php"
    end
 
    task :development do
      sudo "cp #{latest_release}/sites/default/settings.development.php #{latest_release}/sites/default/settings.php"
    end
  end
 
  namespace :db do
    namespace :dump do
      task :development do
        raise RuntimeError.new("failed dump") unless system "#{mysqldump} -u #{local_db_user} --password=#{local_db_password} #{local_db_name} > dump.sql"
      end
    end
 
    namespace :import do
      task :production do
        ENV["FILES"] = "dump.sql"
        deploy::upload
        run "mysql -u #{db_user} --password=#{db_password} #{db_name} < #{latest_release}/dump.sql"
      end
    end
  end
end

Deploying Drupal with Capistrano

Mikamai, the company I work for, has just released Montalbano.tv, the companion site to one of the most successful TV shows in Italy.

I was the technical director of this Drupal based project, and while I was happy we chose Drupal, because it allowed us to deliver all the features they needed on time, I almost panicked when they told us the production setup would have two servers, both with database and web serving duties.

The database replication was standard MySql master-master setup, but I had to develop a strategy to keep the two code-bases on the two servers synchronized.

Being a Ruby programmer at heart, I selected the only tool that never fails me in circumstances like the one we had: Capistrano.

Unfortunately, while Capistrano is all easy to use with Rails, I had to write a custom Drupal-tailored Capfile.

Here it is, in its entirety, in case you ever need to deploy Drupal with cap (now I always deploy Drupal with cap, since I have the recipe ready :) ):

load 'deploy' if respond_to?(:namespace) # cap2 differentiator
 
# Standard configuration
set :user, "username"
set :password, "password"
set :application, "application.name"
 
# I like to deploy the code in /var/apps
# and then link it to the webserver directory
set :deploy_to, "/var/apps/#{application}"
 
# SCM Stuff configure to taste, just remember the repository
# here I used github as main repository
set :repository,  "git@github.com:username/project.git"
set :scm, :git
set :branch, "master"
set :repository_cache, "git_master"
set :deploy_via, :remote_cache
set :scm_verbose,  true
 
# Two servers, double fun
# You really don't need app, web and db here,
# but I used all of them just to be sure.
# Usually only web is ok.
role :app, "first.server.address.com"
role :app, "second.server.address.com", :primary => true
role :web, "first.server.address.com"
role :web, "second.server.address.com", :primary => true
role :db, "first.server.address.com"
role :db, "second.server.address.com", :primary => true
 
after 'deploy:setup', 'drupal:setup' # Here we setup the shared files directory
after 'deploy:symlink', 'drupal:symlink' # After symlinking the code we symlink the shared dirs
 
# Before restarting the webserver we fix all the 
# permissions and then symlink it to production
before 'deploy:restart', 'mikamai:permissions:fix', 'mikamai:symlink:application'
 
 
namespace :drupal do
  # shared directories
  task :setup, :except => { :no_release => true } do
    sudo "mkdir -p #{shared_path}/files"
    sudo "chown -R #{user}:#{user} #{deploy_to}"
  end
 
  # symlink shared directories
  task :symlink, :except => { :no_release => true } do
    sudo "ln -s #{shared_path}/files #{latest_release}"
  end
end
 
namespace :deploy do
  # adjusted finalize_update, removed non rails stuff
  task :finalize_update, :except => { :no_release => true } do
    sudo "chmod -R g+w #{latest_release}" if fetch(:group_writable, true)
  end
 
  task :restart do
    # nothing to do here since we're on mod-php
  end
end
 
namespace :mikamai do
  # symlinking to production
  namespace :symlink do
    task :application, :except => { :no_release => true } do
      sudo "rm -rf /var/www/montalbano"
      sudo "ln -s #{latest_release} /var/www/montalbano"
    end
  end
 
  # change ownership
  namespace :permissions do
    task :fix, :except => { :no_release => true } do
      sudo "chown -R www-data:www-data #{latest_release}"
    end
  end
 
end