Notes on how to deploy MODx content management system with Capistrano, as a rough overview followed by example deploy.rb config.
Version info
Capistrano 2.3.0.
Useful info
To see a list of tasks:
cap -T
To see the description of a particular task:
cap -e [task name]
Depending on your system, code for the default tasks is in somewhere like /usr/lib/ruby/gems/1.8/gems/capistrano-2.3.0/lib/capistrano/recipes/deploy.rb
To get user input:
print "Do stuff then type 'y' when done: "
while STDIN.gets.chomp != 'y'; end
Steps to deploy MODx
- Server: Create DB.
- Local: Upload archive to server.
- Server: Unzip archive.
- Server: Edit [modx_home]/manager/includes/config.inc.php to add DB config.
- Local: Load setup page in browser and complete steps.
- Server: Remove installation directory.
Steps with Capistrano
Assumptions
We have a local working MODx installation in subversion. DB config is local.
- Create directories:
project |__ config = capistrano config. |__ public = php app.
- cd project.
capify .
You now have:project |__ Capfile |__ config |__ deploy.rb |__ public
- Unzip modx into public.
capify .
deploy:setup
- Create local tmp directories.
- Create remote tmp directories.
- Configure web server to see modx (e.g. in public_html/modx).
- Create local DB.
- Create remote DB.
deploy:setup
. This creates necessary Capistrano files on server.
deploy:cold
Since when making changes to remote MODx installation we have to work with database from remote machine, we might as well setup MODx on remote server, dump it then load into local database.
- Start local Apache.
- Complete local MODx online setup tool.
- Check that local MODx installation directory was correctly removed and that config.inc.php is not visible.
- Dump local DB.
deploy:cold
. This copies MODx files and DB to server (except local config file).- Load DB dump into remote DB.
- Edit remote MODx config file.
- Protect remote MODx config file so that it’s not overwritten on later deploy by moving it into Capistrano shared directory.
- Create public application link (e.g. ~/public_html/modx -> ~/modx/current) on server.
- Test MODx.
deploy
deploy
. This copies files to server.
A before :deploy ...
hook can be used to dump DB and after :deploy
hook to replace the remove database with the contents of the dump etc.
Note: There may very well be other steps here - my notes were corrupted :-/
rollback
To allow rollback, dump db into previous release on deploy. Load db on rollback.
Problems
Can not use symbolic link (aka soft link) to link public_html/app_name to app_name/current because MODx setup makes use of dirname(__FILE__)
which uses the location of the destination file rather than the link file. Hence, when MODx populates the database with various path settings it will write the location of the first release to the database and it will break when there are later releases.
Must not run a remote command that includes a password, because it’s being logged locally and even if it isn’t being logged anywhere on the remote machine, it will show up in the process list.
deploy.rb
And now the config. I’ve set things up in a particular way, so change the variables to account for your setup accordingly.
set :client_name, "client-name"
set :application, "modx"
set :location, "ssh.client-name.com"
set :user, "remote-username"
#set :port, 6134
set :db_name, "remote-db-name"
set :db_user, user
set :db_pass, "remote-db-pass"
set :home, "/home/#{user}"
set :local_user, "local-username" # Define a custom variable to hold local user name.
set :local_db_name, "local-db-name"
set :local_db_user, "local-db-user"
set :local_db_pass, "local-db-pass" # Would be better not to have password here. Could instead configure my.cnf file for mysql auth without pass.
set :local_home, "/home/#{local_user}"
role :app, location
role :web, location
role :db, location, :primary => true
set :working_copy, "#{local_home}/wcs/clients/#{client_name}/website/trunk/public"
set :repository, "file://#{local_home}/repositories/clients/#{client_name}/website/trunk/public"
set :deploy_to, "#{home}/#{application}"
# Define a custom variable to hold location of application's public directory (e.g. public_html/modx).
# On shared hosting, if creating an 'addon' domain or a subdomain, this is normally a directory in public_html with the same name as the 'addon' domain or subdomain created. It is later replaced with a link to the current release after every deploy:cold or deploy.
set :public_application, "#{home}/public_html/#{application}"
set :deploy_via, :copy
set :copy_dir, "#{local_home}/tmp/capistrano" # Otherwise it uses /tmp. Note that I had problems with /tmp because of not being able to create a hard link.
set :copy_remote_dir, "#{home}/tmp/capistrano"
set :copy_cache, "#{local_home}/tmp/capistrano/caches/#{application}" # Otherwise it uses /tmp. Only in v2.3.
set :copy_exclude, ["manager/includes/config.inc.php", ".svn", "**/.svn"] # Prevent modx config and .svn directories being uploaded.
set :runner, user
set :use_sudo, false # On shared hosting the user does not have access to sudo.
default_run_options[:pty] = true # Necessary to prevent "*** [err :: whatever.com] stdin: is not a tty" error on 'cap deploy:setup'.
# Override default tasks which are not relevant to a non-rails app.
namespace :deploy do
# Tasks used by deploy:cold.
task :migrate do
puts " Preventing migrate because not a Rails application."
end
task :start do
puts " Preventing start because not a Rails application."
end
task :finalize_update do
puts " Preventing finalize_update because not a Rails application."
end
# Tasks used by deploy.
task :restart do
puts " Preventing restart because not a Rails application."
end
end
# Custom tasks to create resources required by custom configuration.
namespace :custom_config do
desc <<-DESC
Create directories on remote machine to hold temporary data used by Capistrano.
DESC
task :create_tmp_dirs, :roles => :app do
print " Creating #{home}/tmp/capistrano on application server...\n"
run "mkdir -p #{home}/tmp/capistrano"
end
desc <<-DESC
Create directories on local machine to hold temporary data used by Capistrano.
DESC
task :create_local_tmp_dirs do
print " Creating #{local_home}/tmp/capistrano on local machine...\n"
system "mkdir -p #{local_home}/tmp/capistrano"
end
end # custom_config namespace
# Custom tasks for deployment to shared hosting.
namespace :shared_hosting do
desc <<-DESC
Prompt the user to configure their server to use the application link that will be created in the public_html directory.
DESC
task :configure_server, :roles => :web do
print " Before deploy:cold a link will be created #{public_application} -> #{current_path} on the web server.\n"
print " Configure the server to use this link (e.g. create an 'addon' domain or a subdomain) then input 'y' when done: "
while STDIN.gets.chomp != 'y'; end
end
desc <<-DESC
Create a link on the web server in the public_html directory with the same name as the application, pointing to the current release.
Any existing file/directory is backed up.
DESC
task :create_app_links, :roles => :web do
# timestamp = Time.now.utc.strftime("%Y%m%d%H%M%S")
# print " Backing up #{public_application} to #{public_application}.bak-#{timestamp}...\n"
# run "mv #{public_application} #{public_application}.bak-#{timestamp}"
print " Creating link #{public_application} -> #{current_path} on #{web}...\n"
run "ln --backup=numbered -snf #{current_path} #{public_application}"
end
end # shared_hosting namespace
# Custom tasks for MODx deployment.
namespace :modx do
desc <<-DESC
Prompt the user to create the remote database.
DESC
task :create_db, :roles => :db do
print " Create database called #{db_name} on DB server then input 'y' when done: "
while STDIN.gets.chomp != 'y'; end
end
desc <<-DESC
Create the local database.
DESC
task :create_local_db do
print " Creating database called #{local_db_name} on local machine... "
system "mysqladmin -u#{local_db_user} -p create #{local_db_name}"
end
=begin
desc <<-DESC
Generate the MODx config file.
DESC
task :generate_config, :roles => :app do
file = File.join(File.dirname(__FILE__), "templates", "config.inc.php.erb")
template = File.read(file)
buffer = ERB.new(template).result(binding)
put buffer, "#{deploy_to}/public/manager/includes/config.inc.php", :mode => 0444
end
=end
desc <<-DESC
Start local Apache.
DESC
task :start_local_apache do
print " Starting Apache on local machine..."
system "sudo /etc/init.d/apache2 start"
end
desc <<-DESC
Create link in local user's public_html directory that points to MODx.
DESC
task :create_local_apache_link_to_modx do
print " Creating link #{local_home}/public_html/#{client_name} -> #{working_copy}"
system "ln -s #{working_copy} #{local_home}/public_html/#{client_name}"
end
desc <<-DESC
Prompt the user to run the MODx online setup tool on the local machine.
DESC
task :do_local_modx_setup, :roles => :web do
print " Run the online setup tool (possibly at http://localhost/~#{local_user}/#{client_name}/install/), choosing 'New Installation' when asked, then input 'y' when completed: "
while STDIN.gets.chomp != 'y'; end
end
desc <<-DESC
Prompt the user to run the MODx online setup tool on the web server.
DESC
task :do_modx_setup, :roles => :web do
begin_setup
print " Run the online setup tool (possibly at http://#{location}/#{application}/install/), choosing 'New Installation' when asked, then input 'y' when completed: "
while STDIN.gets.chomp != 'y'; end
end_setup
end
desc <<-DESC
Allow the user to run the MODx online upgrade tool.
DESC
task :do_modx_upgrade do # TODO: What role(s) - app or web?
begin_setup
print " Run the online setup tool (possibly at http://#{location}/#{application}/install/, choosing 'Upgrade Existing Install' when asked, then input 'y' when completed: "
while STDIN.gets.chomp != 'y'; end
end_setup
end
desc <<-DESC
Before running the MODx online setup tool, we move the latest release to its public application directory (e.g. public_html/modx) so that
it sets the proper path settings in the database (rather than using the symlink's path).
DESC
task :begin_setup do # What role(s) - app or web?
print " Replacing public application symlink #{public_application} with latest release #{latest_release}...\n"
run "rm #{public_application}"
run "mv #{latest_release} #{public_application}"
end
desc <<-DESC
After running the MODx online setup tool, move the latest release back to the releases directory (from the public application directory)
and re-create the public application symlink.
DESC
task :end_setup do # What role(s) - app or web?
print " Moving #{public_application} back to #{latest_release}...\n"
run "mv #{public_application} #{latest_release}"
print " Creating symlink #{public_application} -> #{current_path}...\n"
run "ln -snf #{current_path} #{public_application}"
end
desc <<-DESC
Prompt the user to check that the install directory was correctly deleted from the local machine and the config file is not visible online.
DESC
task :local_security_check do # TODO: What role(s) - app or web?
print " Check on the local machine that neither the install directory nor the MODx configuration file can be accessed (possibly at #{working_copy}/install and http://localhost/~#{local_user}/#{client_name}/manager/includes/config.inc.php), then input 'y' when done: "
while STDIN.gets.chomp != 'y'; end
end
desc <<-DESC
Prompt the user to check that the install directory was correctly deleted and the config file is not visible online.
DESC
task :security_check do # TODO: What role(s) - app or web?
print " Check that neither the install directory nor the MODx configuration file can be accessed (possibly at http://#{location}/#{application}/install/ and http://#{location}/#{application}/manager/includes/config.inc.php), then input 'y' when done: "
while STDIN.gets.chomp != 'y'; end
end
desc <<-DESC
Prevent MODx config file (current/manager/includes/config.inc.php) being removed on deploy by moving it to the shared directory and linking to it.
DESC
task :protect_config, :roles => :app do
print " Moving #{current_path}/manager/includes/config.inc.php into #{shared_path}...\n"
run "mv #{current_path}/manager/includes/config.inc.php #{shared_path}/"
link_to_config
end
desc <<-DESC
Create link to MODx config file current/manager/includes/config.inc.php -> shared/config.inc.php.
A hard link must be used because the config file sets path variables relative to its location.
DESC
task :link_to_config, :roles => :app do
print " Creating hard link #{current_path}/manager/includes/config.inc.php -> #{shared_path}/config.inc.php...\n"
run "ln -f #{shared_path}/config.inc.php #{current_path}/manager/includes/config.inc.php"
end
desc <<-DESC
Dump the local database into the working copy trunk.
DESC
task :dump_local_database do
print " Dumping local database into the working copy trunk...\n"
system "mysqldump -u#{local_db_user} -p #{local_db_name} > #{working_copy}/../db_dump.sql"
end
desc <<-DESC
Dump the database into the current release directory.
DESC
task :dump_database, :roles => :db do
print " Dumping current database into current release directory...\n"
run "mysqldump -u#{db_user} -p #{db_name} > #{current_path}/db_dump.sql" do |ch, stream, data|
if data =~ /password: /
ch.send_data(db_pass)
end
end
end
desc <<-DESC
Restore the database of the previous release.
DESC
task :rollback_database, :roles => :db do
print " Restoring database from previous release...\n"
run ""
end
end # modx namespace
before 'deploy:setup', 'custom_config:create_tmp_dirs', 'custom_config:create_local_tmp_dirs', 'shared_hosting:configure_server', 'modx:create_db', 'modx:create_local_db'
before 'deploy:cold', 'modx:create_local_apache_link_to_modx', 'modx:start_local_apache', 'modx:do_local_modx_setup', 'modx:local_security_check', 'modx:dump_local_database'
#after 'deploy:cold', 'shared_hosting:create_app_links', 'modx:do_modx_setup', 'modx:protect_config', 'modx:security_check', 'modx:dump_database'
before 'deploy', 'modx:dump_database'
after 'deploy', 'modx:link_to_config', 'modx:do_modx_upgrade', 'modx:security_check'
before 'deploy:rollback', 'modx:rollback_database'
References
- http://capify.org/getting-started/
- http://manuals.rubyonrails.com/read/chapter/103 (I believe this is out of date)
- http://weblog.jamisbuck.org/2008/5/2/capistrano-2-3-0
- http://www.simplisticcomplexity.com/2006/8/16/automated-php-deployment-with-capistrano/
- http://www.madebymany.co.uk/using-capistrano-with-php-specifically-wordpress-0087
- http://archive.jvoorhis.com/articles/2006/07/07/managing-database-yml-with-capistrano
- http://ruby-doc.org/core/classes/File.html#M002603
- http://capify.org/upgrade/gotchas
- http://groups.google.com/group/capistrano/browse_thread/thread/6469db0af25f341b/d7c4865f3faa7978?lnk=gst&q=hooks+namespace#d7c4865f3faa7978
- http://blog.innerewut.de/2007/9/28/capturing-output-in-capistrano