[UPDATE] Capistrano Deploy for Shared Hosting with GIT Repository

Like I said in my original post Capistrano Deploy for Shared Hosting with GIT Repository, I would add the ability to rotate log files and backup your database. Also added a couple of niceties while I was at it.

  • Updated for Capistrano 2.3
  • Added backup of mysql database to local machine
  • Added log rotation and backup to local machine
  • DRYed things up just a touch (delete_files, delete_directories)

Just run a ‘cap -T’ to see all the new functions.
[sourcecode language=”ruby”]
cap -T

CAP db:backup

‘cap db:backup’ will execute a mysqldump, so obviously, this only works on mysql databases. But you could adapt that to any db fairly easily. It dumps the file and bzips it and then downloads it into a ‘/backups’ directory in your project.

In addition, before you do any deploy function that executes a migration, the db:backup will be called first. Just a little safety net before you dive into the deep waters and potentially do something that could keep you awake at night.

CAP log:rotate

‘cap log:rotate’ will stop the server, bzip the production.log file, delete the production.log file and transfer the compressed file to your ‘/backups’ directory in your project. It should be noted that there is a slight chance that someone could use the website in the fraction of a second it takes to bzip the log file and you would lose those log entries, or rails could throw an error because it could not find the ‘production.log’ file that it thought exists. So my recommendation it to run the ‘cap log:rotate’ at slow system usage times, or rewrite the task to completely disable the application before bzipping the log file.

New /backups Directory

Both the log:rotate and db:backup task will create a new ‘/backups’ directory in your project. You will most certainly want to add “backups/**” to your .gitignore so that the dump files are not pulled into source control.

For Capistrano 2.3

Since my original post, Capistrano 2.3 has arrived. You can read all about it’s goodness here. One feature it allowed is the ability to ignore files and directory from source control. I added a ‘copy_exclude’ to ignore all GIT files (.git directory, .gitignore file) instead of removing them from the deployed release after it’s copied to the server.

So if you have not upgraded to 2.3, you will need to comment out the ‘copy_exclude’ line, and uncomment the ‘remove_git_directories’ task.

[sourcecode language=”ruby”]
ssh_options[:keys] = %w(~/.ssh/id_dsa ~/.ssh/id_rsa)

set :application, “vbsfinder”
set :repository, “git@”
set :domain, “vbsfinder.com”

# user is the login name for the shared hosting account
# it should also be the name of your home directory i.e. /home/my-user-name
set :user, “vbsfinde”

# this will place you app outside your /public_html
# and protect your app file from exposure
set :deploy_to, “/home/#{user}/#{application}”

set :scm, :git
# set :deploy_via, :remote_cache # if your server has direct access to the repository
set :deploy_via, :copy # if you server does NOT have direct access to the repository (default)
set :git_shallow_clone, 1 # only copy the most recent, not the entire repository (default:1)
set :copy_exclude, [“.git”, “.gitignore”]

ssh_options[:paranoid] = false
set :use_sudo, false

set :keep_releases, 2 # only keep a current and one previous version to save space

role :app, domain
role :web, domain
role :db, domain, :primary => true

task :update_public, :roles => [:app] do
run “chmod 755 #{release_path}/public”
run “chmod 755 #{release_path}/public/dispatch.*”

task :copy_system_stopped_files, :roles => [:app] do
run “cp -f #{release_path}/public/system_stopped/* #{shared_path}/system_stopped/”

after “deploy:update_code”, :update_public
after “deploy:update_code”, :copy_system_stopped_files

# ****** Removed for Capistrano version >= 2.3
# remove the .git directory and .gitignore from the current release
# task :remove_git_directories, :roles => [:app] do
# delete_directories “#{release_path}/.git”
# delete_files “#{release_path}/.gitignore”
# end
# this lets up keep the system_stopped files in project
# after “deploy:update_code”, :remove_git_directories

# create a symlink from the current/public to ~/public_html
task :create_public_html, :roles => [:app] do
run “ln -fs #{current_path}/public ~/public_html”
after “deploy:cold”, :create_public_html

# create a directory in shared to hold files that will be served
# when the system is stopped, and a config directory
task :create_shared_directories, :roles => [:app] do
run “mkdir -p #{shared_path}/system_stopped”
run “mkdir -p #{shared_path}/config”
after “deploy:setup”, :create_shared_directories

# remove the symlink for ~/public_html
task :remove_public_html, :roles => [:app] do
delete_directories “~/public_html”

# creates a symlink to ~/public_html to the shared/system_stopped directory
# places files you want served in this dir for when the system is stopped
task :create_public_html_to_stopped, :roles => [:app] do
run “ln -fs #{shared_path}/system_stopped ~/public_html”

# we need to override the default start/stop/restart functions
namespace :deploy do
desc “Restart the web server. Killing all FCGI processes.”
task :restart, :roles => :app do
# for most hosts, all you need to do is stop all FCGI processing running
run “killall -q dispatch.fcgi”
# but some hosts can restart by touching the dispatch file
#run “chmod 755 #{current_path}/public/dispatch.fcgi”
#run “touch #{current_path}/public/dispatch.fcgi”

desc “Start the web server. Really nothing to do for shared hosting.”
task :start, :roles => :app do

desc “Stop the web server and present maintenance page.”
task :stop, :roles => :app do

namespace :web do
desc “Make application web accessible again.”
task :enable, :roles => [:app] do

desc “Present system maintenance page to users.”
task :disable, :roles => [:app] do


# a little help here from Brandon Keepers
# http://opensoul.org/2007/2/9/automatically-backing-up-your-remote-database-on-deploy
namespace :db do
desc “Backup the remote production database”
task :backup, :roles => :db, :only => { :primary => true } do
require ‘yaml’

filename = “#{application}.dump.#{timestamp_string}.sql.bz2”
file = “/tmp/#{filename}”

on_rollback { delete_files file }
db = YAML::load(ERB.new(IO.read(File.join(File.dirname(__FILE__), ‘database.yml’))).result)[‘production’]

run “mysqldump -u #{db[‘username’]} –password=#{db[‘password’]} #{db[‘database’]} | bzip2 -c > #{file}” do |ch, stream, data|
puts data

mkdir -p #{File.dirname(__FILE__)}/../backups/
download file, “backups/#{filename}”
delete_files file

before “deploy:migrate”, “db:backup”
before “deploy:migrations”, “db:backup”

namespace :log do
desc “Compress and copy log file to local machine”
task :rotate, :roles => [:app] do
logfiles = capture( “ls -x #{shared_path}/log” ).split

if logfiles.index( ‘production.log’ ).nil?
logger.info “production.log was not found, no rotation performed”

filename = “#{application}.production.log.#{timestamp_string}.log.bz2”
logfile = “#{shared_path}/log/production.log”

deploy.restart # stop the server
run “bzip2 #{logfile}” # bzips the production.log and removes it

mkdir -p #{File.dirname(__FILE__)}/../backups/
download “#{logfile}.bz2”, “backups/#{filename}”
delete_files “#{logfile}.bz2”

def timestamp_string

def delete_files(*args)
run “rm -f #{args.join(‘ ‘)}”

def delete_directories(*args)
run “rm -rf #{args.join(‘ ‘)}”

Posted in Rails. Tagged with .

5 Responses

Comments always appreciated...