Multi-stage deployment with capistrano

I want to talk a bit about capistrano. It took me a while to get on the boat, well here I am, and I am very happy with it. I am going to show an example of a simple multi-stage deployment setup, developing on a mac and then deploy to staging and production. I will also show how to clone your production data for debugging purposes.

I am assuming that your staging environment and production environment is located at the same server. I am also assuming that you have a private public key pair, and that the public key has been transferred to the aforementioned server.

Prerequisites

Install Capistrano and multistage extension:

gem install capistrano

If you are using Bundler, put this is your Gemfile:

group :development do
  gem "capistrano"
  gem "capistrano_transmit"
end

Capify your project by issuing this command in the root folder of your project

capify .

Your project is now capified!

Config files

When capifying your project, Capistrano creates a file in your config directory, called deploy.rb. This is the main configuration file. We also need to crete a subfolder called deploy/, with two files in it, staging.rb and production.rb. You should end up with this structure:

...
|
|- config
    |
    ...
    |
    |- deploy.rb
    |
    |- deploy
    |   |
    |   |- production.rb
    |   |
    |   |- staging.rb
    ...

Edit deploy.rb

open the file deploy.rb in your text editor:

require 'capistrano/ext/multistage'
require "bundler/capistrano"
require 'capistrano/transmit'

set :application, "name-of-application"

set :scm, :git
set :repository, "https//github.com/username/repository.git"

# use private/public keys for repo connection, otherwise set username and password here and uncomment
# set :scm_username, ""
# set :scm_passphrase, ""
set :user, "deploy"

set :deploy_via, :remote_cache
set :ssh_options, { :forward_agent => true }

set :stages, ["staging", "production"]
set :default_stage, "staging"
set :use_sudo, false
set :default_shell, "bash --login"

after "deploy:restart", "deploy:cleanup"

# task to backup the production database before each deploy to production
task :backup, :roles => :db, :only => { :primary => true } do
  filename = "#{deploy_to}/backups/#{application}-#{rails_env}.dump.#{Time.now.to_f}.sql.bz2"
  text = capture "cat #{release_path}/config/database.yml"
  yaml = YAML::load(text)

  on_rollback { run "rm #{filename}" }
  run "mysqldump -u#{yaml[rails_env]['username']} -p#{yaml[rails_env]['password']} #{yaml[rails_env]['database']} | bzip2 -c > #{filename}"
end

# deployment tasks
namespace :deploy do
  task :start do ; end
  task :stop do ; end

  # restart the server
  task :restart, :roles => :app, :except => { :no_release => true } do
    run "#{try_sudo} touch #{File.join(current_path,'tmp','restart.txt')}"
  end

  # clone the production database to your environment
  task :clone_production_database, :except => { :no_release => true } do
    text = capture "cat #{release_path}/config/database.yml"
    yaml = YAML::load(text)
    run "mysql -u#{yaml[rails_env]['username']} -p#{yaml[rails_env]['password']} --execute='CREATE DATABASE IF NOT EXISTS #{yaml[rails_env]['database']}';"
    run "mysqldump -u#{yaml['production']['username']} -p#{yaml['production']['password']} #{yaml['production']['database']} | mysql -u#{yaml[rails_env]['username']} -p#{yaml[rails_env]['password']} #{yaml[rails_env]['database']}"
  end
end

after "deploy:update_code", "deploy:migrate"

open the file /config/deploy/production.rb and put this in there:

server "servername-of-production", :app, :web, :db, :primary => true
set :deploy_to, "/path/to/production/on/server"
set :rails_env, "production"

before "deploy:migrate", "backup"

open the file /config/deploy/staging.rb and put this content in there:

server "servername-of-staging", :app, :web, :db, :primary => true
set :deploy_to, "/path/to/staging/on/server"
set :rails_env, "staging"

set :keep_releases, 1

before "deploy:migrate", "deploy:clone_production_database"
after "deploy:update_code", "deploy:copy_assets"

Remember to replace all the dummy values above with your own values.

Okay - that was a lot of code. What’s happening here. The scripts you just made configure capistrano for a couple of things:

  • We are deploying using a git repository
  • There are two stages: staging and production. You preview your code on staging and if it’s working you deploy to production.
  • The default deploy is to staging
  • No releases are kept on staging
  • When deploying to staging, first the production database is cloned to staging. Then the assets are copied from production to staging (with capistrano-transmit). So you are previewing on a fresh copy of production.
  • When deploying to production, the database is backed up. It is backed up to a subdir of the deploy dir called backups. The file name is the a mixture of the application name, environment and date. It’s a bzipped sql file.

Good luck using this script.

Comments

comments powered by Disqus