MODx deployment with Capistrano

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

  1. Server: Create DB.
  2. Local: Upload archive to server.
  3. Server: Unzip archive.
  4. Server: Edit [modx_home]/manager/includes/config.inc.php to add DB config.
  5. Local: Load setup page in browser and complete steps.
  6. Server: Remove installation directory.

Steps with Capistrano

Assumptions

We have a local working MODx installation in subversion. DB config is local.

  1. Create directories:
    project
    |__ config = capistrano config.
    |__ public = php app.
    
  2. cd project. capify . You now have:
    project
    |__ Capfile
    |__ config
        |__ deploy.rb
    |__ public
    
  3. Unzip modx into public.
  4. capify .

deploy:setup

  1. Create local tmp directories.
  2. Create remote tmp directories.
  3. Configure web server to see modx (e.g. in public_html/modx).
  4. Create local DB.
  5. Create remote DB.
  6. 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.

  1. Start local Apache.
  2. Complete local MODx online setup tool.
  3. Check that local MODx installation directory was correctly removed and that config.inc.php is not visible.
  4. Dump local DB.
  5. deploy:cold. This copies MODx files and DB to server (except local config file).
  6. Load DB dump into remote DB.
  7. Edit remote MODx config file.
  8. Protect remote MODx config file so that it’s not overwritten on later deploy by moving it into Capistrano shared directory.
  9. Create public application link (e.g. ~/public_html/modx -> ~/modx/current) on server.
  10. Test MODx.

deploy

  1. 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

Last modified: 10/06/2008 Tags: ,

Related Pages

Other pages possibly of interest:

This website is a personal resource. Nothing here is guaranteed correct or complete, so use at your own risk and try not to delete the Internet. -Stephan

Site Info

Privacy policy

Go to top