Skip to content

Instantly share code, notes, and snippets.

@saikat
Created August 28, 2009 00:01
Show Gist options
  • Save saikat/176669 to your computer and use it in GitHub Desktop.
Save saikat/176669 to your computer and use it in GitHub Desktop.
require 'capistrano/recipes/deploy/strategy/copy'
set :application, "YOUR APPLICATION NAME"
set :location, "YOUR DOMAIN (e.g. myapp.com)"
set :repository, "YOUR SOURCE CONTROL REPOSITORY"
set :user, "THE USER ON YOUR SERVER THAT WILL BE DEPLOYING"
set :local_user, "LOCAL USER"
set :database_name, "YOUR DATABASE NAME"
set :scm, SET WHAT SOURCE CONTROL METHOD YOU USE
set :use_sudo, false
set :deploy_to, "/home/#{user}/#{application}"
set :deploy_via, :copy
set :copy_dir, "/Users/#{local_user}/capistrano"
set :copy_remote_dir, "/home/#{user}/capistrano"
set :copy_cache, "#{copy_dir}/#{application}"
set :copy_exclude, [".hg", ".hgignore", "*.DS_Store", "Icon"]
set :version_dir, "production_releases"
set :current_dir, "current_production"
set :local_database_backup_path, "/Users/#{local_user}/capistrano/db_backups/"
set :database_dir, "production_database_backups"
set :database_backups_path, File.join(deploy_to, database_dir)
set :database_backup_path, File.join(database_backups_path, release_name)
_cset(:database_backups) { capture("ls -xt #{database_backups_path}").split }
_cset(:previous_database_backup) { database_backups.length > 0 ? File.join(database_backups_path, database_backups[0]) : nil }
_cset(:previous_migration_path) { File.join(previous_release, "#{application}/backend/migrations") }
_cset(:previous_migrations) { capture("ls -xt #{previous_migration_path}/*.py").split }
_cset(:previous_migration_base_name) { previous_migrations.length > 1 ? File.basename(previous_migrations[-2]) : nil }
_cset(:previous_migration_name) { previous_migration_base_name ?
previous_migration_base_name.chomp(File.extname(previous_migration_base_name)) : nil }
role :web, location
role :app, location
role :db, location, :primary => true
# Author: Saikat Chakrabarti, saikat1@gmail.com
# Written: 8-26-2009
#
# This recipe is used to deploy a Cappuccino application running with a Django backend using
# PostgreSQL as the database. This recipe assumes a lot, but should be a good starting point
# for others trying to write similare deploy recipes. I've tried to list these assumptions out
# below:
# 1. You are using PostgreSQL
# 2. You have installed Lee Hambley's railsless deploy (http://github.com/leehambley/railsless-deploy/tree/master)
# 3. Your directory structure is as follows:
# root
# |-- application
# |-- manage.py
# |-- env_config (your environment config goes here - see comments on Capistrano::Deploy::Strategy::Copy below)
# |-- backend (your django application that handles backend operations for your capp app)
# |-- frontend (your cappuccino application)
# |-- other django files... (urls.py, settings.py, etc.)
# |-- Capfile (it should look like the one in Lee Hambley's railsless deploy)
# |-- config
# |-- deploy.rb (this file)
# 4. You will need to change the server_tasks.restart method to fit your server's setup
# 5. You are using South (http://south.aeracode.org/) to maintain your database migrations.
#
# Read the comments in the methods below before using them.
# Custom tasks for our hosting environment.
namespace :remote do
desc <<-DESC
Create directory required by copy_remote_dir.
DESC
task :create_copy_remote_dir, :roles => :app do
print " creating #{copy_remote_dir}.\n"
run "mkdir -p #{copy_remote_dir}"
end
end
# Custom tasks for our local machine.
namespace :local do
desc <<-DESC
Create directory required by copy_dir. This will create the local cache where capistrano
will keep a repository of your code that it just pulls changes into. It also sets up
the directory where database backups will be stored locally.
DESC
task :create_copy_dir do
print " creating #{copy_dir}.\n"
system "mkdir -p #{copy_dir}"
logger.debug "creating #{local_database_backup_path}"
system "mkdir -p #{local_database_backup_path}"
end
end
namespace :db do
desc <<-DESC
Backup the database. This will backup the database on to a path on the server as well
as send a copy of the database to :local_database_backup_path. Ideally, you should probably
have a separate server or something like Amazon EC3 to maintain your database backups.
DESC
task :backup, :roles => :db, :only => { :primary => true } do
on_rollback { run "rm -rf #{database_backup_path}; true" }
logger.debug "Backing up database into #{database_backup_path}"
run "pg_dump #{database_name} > #{database_backup_path}"
logger.debug "Transferring database to local machine for redundancy"
system "scp -P #{port} #{user}@#{location}:#{database_backup_path} #{local_database_backup_path}"
end
desc <<-DESC
WARNING - this will drop the database and then restore it from the latest database backup
stored on the server at :database_backups_path. It will fail (without dropping the database)
if you have no database backups to restore from.
DESC
task :restore, :roles => :db, :only => { :primary => true } do
if previous_database_backup
logger.debug "Restoring database from #{previous_database_backup}..."
logger.debug "Dropping the current database"
run "dropdb #{database_name}"
logger.debug "Recreating the database"
run "createdb -T template0 #{database_name}"
logger.debug "Restoring the database from backup"
run "psql #{database_name} < #{previous_database_backup}"
logger.debug "Removing restored backup"
run "rm -f #{previous_database_backup}"
else
logger.important "No previous database backup to restore from."
end
end
desc <<-DESC
This will delete the old database backups on the server, keeping only the latest 5. It
will not touch the duplicate database backups that are stored locally.
DESC
task :cleanup, :roles => :db, :only => { :primary => true } do
count = fetch(:keep_releases, 5).to_i
if count >= database_backups.length
logger.important "no old releases to clean up"
else
logger.info "keeping #{count} of #{database_backups.length} deployed releases"
directories = (database_backups - database_backups.last(count)).map { |backup|
File.join(database_backups_path, backup) }.join(" ")
try_sudo "rm -rf #{directories}"
end
end
end
namespace :server_tasks do
desc <<-DESC
Restarts your webserver. The code right now assumes nginx and fastcgi, so you should
change this code.
DESC
task :restart do
logger.debug "Restarting nginx and #{application} fastcgi process"
sudo "/etc/init.d/nginx restart"
sudo "/usr/local/bin/myapp-restart"
end
desc <<-DESC
Cleans up old database backups and old releases.
DESC
task :cleanup do
deploy.cleanup
db.cleanup
end
end
namespace :deploy do
desc <<-DESC
This will ONLY release your code to the server and restart the server. This will not run
any migrations you have.
DESC
task :code do
update
server_tasks.restart
end
desc <<-DESC
This is the command that you will usually want to run to do a deploy. This will deploy
code and also run migrations.
DESC
task :default do
transaction do
update_code
symlink
db.backup
migrate
end
server_tasks.restart
end
desc <<-DESC
This will run the migrations from :current_release in a transaction.
DESC
task :migrations do
transaction do
migrate
end
end
desc <<-DESC
Deletes all but the last five releases.
DESC
task :cleanup, :except => { :no_release => true } do
count = fetch(:keep_releases, 5).to_i
if count >= releases.length
logger.important "no old releases to clean up"
else
logger.info "keeping #{count} of #{releases.length} deployed releases"
directories = (releases - releases.last(count)).map { |release|
File.join(releases_path, release) }.join(" ")
try_sudo "rm -rf #{directories}"
end
end
desc <<-DESC
Prepares one or more servers for deployment. Before you can use any \
of the Capistrano deployment tasks with your project, you will need to \
make sure all of your servers have been prepared with `cap deploy:setup'. When \
you add a new server to your cluster, you can easily run the setup task \
on just that server by specifying the HOSTS environment variable:
$ cap HOSTS=new.server.com deploy:setup
It is safe to run this task on servers that have already been set up; it \
will not destroy any deployed revisions or data.
DESC
task :setup, :except => { :no_release => true } do
dirs = [deploy_to, releases_path, shared_path, database_backups_path]
dirs += shared_children.map { |d| File.join(shared_path, d) }
run "#{try_sudo} mkdir -p #{dirs.join(' ')} && #{try_sudo} chmod g+w #{dirs.join(' ')}"
end
desc <<-DESC
[internal] Migrate the database to the :current_release. You will usually want to use
deploy:migrations instead of calling this directly, as it does not run in a transaction.
DESC
task :migrate, :roles => :db, :only => { :primary => true } do
on_rollback do
if previous_migration_name
logger.debug "Migrating database to #{previous_migration_name}"
run "#{previous_release}/#{application}/manage.py migrate backend #{previous_migration_name}"
else
logger.debug "Migrating database to zero"
run "#{previous_release}/#{application}/manage.py migrate backend zero"
end
end
logger.debug "Migrating database to #{current_release}"
run "#{current_release}/#{application}/manage.py migrate backend"
end
namespace :rollback do
desc <<-DESC
Rollback code to the previous release. WARNING - This will NOT rollback migrations, so
make sure you rollback any migrations BEFORE calling this. If you call this without
rolling back migrations, calling rollback:migrations will migrate to the release before
the previous release.
DESC
task :code do
revision
server_tasks.restart
cleanup
end
desc <<-DESC
Rollback migrations to the previous release. This doesn't actually delete the current
release, so calling this multiple times will have the same effect.
DESC
task :migrations do
if previous_migration_name
logger.debug "Migrating database to #{previous_migration_name}"
run "#{previous_release}/#{application}/manage.py migrate backend #{previous_migration_name}"
else
logger.debug "Migrating database to zero"
run "#{previous_release}/#{application}/manage.py migrate backend zero"
end
end
desc <<-DESC
Rolls back the migrations, followed by the code.
DESC
task :default do
migrations
revision
server_tasks.restart
cleanup
end
end
end
class Capistrano::Deploy::Strategy::Copy
# This is a copy of strategy.deploy! to insert some custom code before the deployment is
# copied to the server. Here, after the code is fetched, I first setup the production
# configuration. This is very specific to my setup - I have my environment configuration
# settings (like the server to use, the secure server to use, the database to hit, etc.)
# in production_settings.py and ProductionSettings.j, and in my Django and Cappuccino
# code, I import enviornment_settings.py and EnvironmentSettings.j to get the configuration
# settings. You should add your custom config setup code in the area after
# logger.debug "setting up configuration files for production". After setting up the
# config, I press the cappuccino code and then continue with the normal deploy process
def deploy!
if copy_cache
if File.exists?(copy_cache)
logger.debug "refreshing local cache to revision #{revision} at #{copy_cache}"
system(source.sync(revision, copy_cache))
else
logger.debug "preparing local cache at #{copy_cache}"
system(source.checkout(revision, copy_cache))
end
logger.debug "copying cache to deployment staging area #{destination}"
Dir.chdir(copy_cache) do
FileUtils.mkdir_p(destination)
queue = Dir.glob("*", File::FNM_DOTMATCH)
while queue.any?
item = queue.shift
name = File.basename(item)
next if name == "." || name == ".."
next if copy_exclude.any? { |pattern| File.fnmatch(pattern, item) }
if File.symlink?(item)
FileUtils.ln_s(File.readlink(File.join(copy_cache, item)), File.join(destination, item))
elsif File.directory?(item)
queue += Dir.glob("#{item}/*", File::FNM_DOTMATCH)
FileUtils.mkdir(File.join(destination, item))
else
FileUtils.ln(File.join(copy_cache, item), File.join(destination, item))
end
end
end
###### CUSTOM DEPLOY STEPS START HERE #######
logger.debug "setting up configuration files for production"
FileUtils.mv(File.join(destination, "#{application}/env_config/production_settings.py"),
File.join(destination, "#{application}/environment_settings.py"))
FileUtils.mv(File.join(destination, "#{application}/env_config/ProductionSettings.j"),
File.join(destination, "#{application}/frontend/EnvironmentSettings.j"))
logger.debug "adding frameworks link to frontend directory"
FileUtils.mkdir(File.join(destination, "#{application}/frontend/Frameworks"))
system "ln -s $CAPP_BUILD/Release/AppKit #{destination}/#{application}/frontend/Frameworks/AppKit"
system "ln -s $CAPP_BUILD/Release/Foundation #{destination}/#{application}/frontend/Frameworks/Foundation"
system "ln -s $CAPP_BUILD/Release/Objective-J #{destination}/#{application}/frontend/Frameworks/Objective-J"
logger.debug "starting cappuccino press"
system "press #{destination}/#{application}/frontend #{destination}/#{application}/frontend-pressed"
logger.debug "removing index-debug"
FileUtils.rm(File.join(destination, "#{application}/frontend-pressed/index-debug.html"))
logger.debug "replacing frontend with pressed code"
FileUtils.rm_rf(File.join(destination, "#{application}/frontend"))
FileUtils.mv(File.join(destination, "#{application}/frontend-pressed"),
File.join(destination, "#{application}/frontend"))
###### CUSTOM DEPLOY STEPS END HERE #######
else
logger.debug "getting (via #{copy_strategy}) revision #{revision} to #{destination}"
system(command)
if copy_exclude.any?
logger.debug "processing exclusions..."
if copy_exclude.any?
copy_exclude.each do |pattern|
delete_list = Dir.glob(File.join(destination, pattern), File::FNM_DOTMATCH)
# avoid the /.. trap that deletes the parent directories
delete_list.delete_if { |dir| dir =~ /\/\.\.$/ }
FileUtils.rm_rf(delete_list.compact)
end
end
end
end
File.open(File.join(destination, "REVISION"), "w") { |f| f.puts(revision) }
logger.trace "compressing #{destination} to #{filename}"
Dir.chdir(tmpdir) { system(compress(File.basename(destination), File.basename(filename)).join(" ")) }
upload(filename, remote_filename)
run "cd #{configuration[:releases_path]} && #{decompress(remote_filename).join(" ")} && rm #{remote_filename}"
ensure
FileUtils.rm filename rescue nil
FileUtils.rm_rf destination rescue nil
end
end
# Callbacks.
before 'deploy:setup', 'local:create_copy_dir', 'remote:create_copy_remote_dir'
@stephenmckinney
Copy link

line 24 should be

_cset(:database_backups) { capture("ls -xt #{database_backup_dir}").split.reverse }

otherwise you delete the newest database backups

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment