Capistrano for Expression Engine 2

UPDATE: There is actually a non-rails version of Capistrano here, I haven’t tried it yet but it’s probably a much more similar approach.  Also it gets rid of loads of pointless rails rake tasks.

Assumptions:


This is a guide to capifying Expression Engine 2 or any non rails site for that matter.  Capistrano is a lovely clean way to deploy websites, you can find more about it here.
Now if we assume you have a website on your local machine and want to deploy it to some remote server.  After you’ve installed the Capistrano gem, you should set up your ssh keys.  You basically need to be able connect to passwordlessly in the following ways.

  1. From your local server connect to the repository server(SVN,git, etc);
  2. From your local server connect to the remote server;
  3. From your remote server connect to the repository server

Ok with the keys setup you just need to create the folder structure on the remote site.  Connect to the remote server and navigate to the directory where your site will be deployed to. Create two directories ‘releases’ and ‘shared’ here, make sure their permissions is writable for you.

With that setup drop down to your local machine again, you need to run the following command in the root directory of the website, the example I’m using is at /var/www/vhosts/example.local/.

capify .

This will create the following files

/Capfile
/config/deploy.rb

Now open up the deploy.rb file in your favourite text editor.  You need to change four values in this file.

The first :application - This name doesn’t really matter, just pick something appropriate.

Second is :repository, this should be the command you use to check-out the website.  So something like

svn+ssh://svn-repository.co.uk/example

Thirdly :deploy_to, set this to the file path on the remote server you’re going to deploy to.

set :deploy_to, "/var/www/vhosts/example/"

Finally just add the website address to :web.  This is what to ssh to when deploying.

role :web, "example.co.uk"

Also I don’t have sudo on my current hosting package so I include the following.

set :use_sudo, false

Now if you run

cap deploy

the site will be deployed but there will be a number of errors, it will also finish with a reaper error.  These errors are a result of capistrano’s assumption that it’s deploying a rails site.  For reference my trace is below.

  * executing `deploy'
  * executing `deploy:update'
 ** transaction: start
  * executing `deploy:update_code'
    executing locally: "svn+ssh://svn-repository.co.uk/example  -rHEAD"
  * executing "svn checkout -q  -r147 svn+ssh://svn-repository.co.uk/example /var/www/vhosts/www.example.co.uk/releases/20110414084921 && (echo 147 > /var/www/vhosts/www.example.co.uk/releases/20110414084921/REVISION)"
    servers: ["example.co.uk"]
    [example.co.uk] executing command
    command finished
  * executing `deploy:finalize_update'
  * executing "chmod -R g+w /var/www/vhosts/www.example.co.uk/releases/20110414084921"
    servers: ["example.co.uk"]
    [example.co.uk] executing command
    command finished
  * executing "rm -rf /var/www/vhosts/www.example.co.uk/releases/20110414084921/log /var/www/vhosts/www.example.co.uk/releases/20110414084921/public/system /var/www/vhosts/www.example.co.uk/releases/20110414084921/tmp/pids &&\\\n      mkdir -p /var/www/vhosts/www.example.co.uk/releases/20110414084921/public &&\\\n      mkdir -p /var/www/vhosts/www.example.co.uk/releases/20110414084921/tmp &&\\\n      ln -s /var/www/vhosts/www.example.co.uk/shared/log /var/www/vhosts/www.example.co.uk/releases/20110414084921/log &&\\\n      ln -s /var/www/vhosts/www.example.co.uk/shared/system /var/www/vhosts/www.example.co.uk/releases/20110414084921/public/system &&\\\n      ln -s /var/www/vhosts/www.example.co.uk/shared/pids /var/www/vhosts/www.example.co.uk/releases/20110414084921/tmp/pids"
    servers: ["example.co.uk"]
    [example.co.uk] executing command
    command finished
  * executing "find /var/www/vhosts/www.example.co.uk/releases/20110414084921/public/images /var/www/vhosts/www.example.co.uk/releases/20110414084921/public/stylesheets /var/www/vhosts/www.example.co.uk/releases/20110414084921/public/javascripts -exec touch -t 201104140849.32 {} ';'; true"
    servers: ["example.co.uk"]
    [example.co.uk] executing command
*** [err :: example.co.uk] find: /var/www/vhosts/www.example.co.uk/releases/20110414084921/public/images: No such file or directory
*** [err :: example.co.uk] find: /var/www/vhosts/www.example.co.uk/releases/20110414084921/public/stylesheets: No such file or directory
*** [err :: example.co.uk] find: /var/www/vhosts/www.example.co.uk/releases/20110414084921/public/javascripts: No such file or directory
    command finished
  * executing `deploy:symlink'
  * executing "rm -f /var/www/vhosts/www.example.co.uk/current && ln -s /var/www/vhosts/www.example.co.uk/releases/20110414084921 /var/www/vhosts/www.example.co.uk/current"
    servers: ["example.co.uk"]
    [example.co.uk] executing command
    command finished
 ** transaction: commit
  * executing `deploy:restart'
  * executing "/var/www/vhosts/www.example.co.uk/current/script/process/reaper"
/usr/lib/ruby/gems/1.8/gems/capistrano-2.5.8/lib/capistrano/configuration/servers.rb:75:in `role_list_from': unknown role `app' (ArgumentError)
	from /usr/lib/ruby/gems/1.8/gems/capistrano-2.5.8/lib/capistrano/configuration/servers.rb:73:in `map'
	from /usr/lib/ruby/gems/1.8/gems/capistrano-2.5.8/lib/capistrano/configuration/servers.rb:73:in `role_list_from'
	from /usr/lib/ruby/gems/1.8/gems/capistrano-2.5.8/lib/capistrano/configuration/servers.rb:45:in `find_servers'
	from /usr/lib/ruby/gems/1.8/gems/capistrano-2.5.8/lib/capistrano/configuration/servers.rb:9:in `find_servers_for_task'
	from /usr/lib/ruby/gems/1.8/gems/capistrano-2.5.8/lib/capistrano/configuration/connections.rb:133:in `execute_on_servers'
	from /usr/lib/ruby/gems/1.8/gems/capistrano-2.5.8/lib/capistrano/configuration/actions/invocation.rb:171:in `run_tree'
	from /usr/lib/ruby/gems/1.8/gems/capistrano-2.5.8/lib/capistrano/configuration/actions/invocation.rb:143:in `run'
	from /usr/lib/ruby/gems/1.8/gems/capistrano-2.5.8/lib/capistrano/configuration/actions/invocation.rb:89:in `send'
	from /usr/lib/ruby/gems/1.8/gems/capistrano-2.5.8/lib/capistrano/configuration/actions/invocation.rb:89:in `invoke_command'
	from /usr/lib/ruby/gems/1.8/gems/capistrano-2.5.8/lib/capistrano/recipes/deploy.rb:123:in `try_sudo'
	from /usr/lib/ruby/gems/1.8/gems/capistrano-2.5.8/lib/capistrano/recipes/deploy.rb:136:in `try_runner'
	from /usr/lib/ruby/gems/1.8/gems/capistrano-2.5.8/lib/capistrano/configuration/namespaces.rb:186:in `send'
	from /usr/lib/ruby/gems/1.8/gems/capistrano-2.5.8/lib/capistrano/configuration/namespaces.rb:186:in `method_missing'
	from /usr/lib/ruby/gems/1.8/gems/capistrano-2.5.8/lib/capistrano/recipes/deploy.rb:302:in `load'
	from /usr/lib/ruby/gems/1.8/gems/capistrano-2.5.8/lib/capistrano/configuration/execution.rb:139:in `instance_eval'
	from /usr/lib/ruby/gems/1.8/gems/capistrano-2.5.8/lib/capistrano/configuration/execution.rb:139:in `invoke_task_directly_without_callbacks'
	from /usr/lib/ruby/gems/1.8/gems/capistrano-2.5.8/lib/capistrano/configuration/callbacks.rb:27:in `invoke_task_directly'
	from /usr/lib/ruby/gems/1.8/gems/capistrano-2.5.8/lib/capistrano/configuration/execution.rb:89:in `execute_task'
	from /usr/lib/ruby/gems/1.8/gems/capistrano-2.5.8/lib/capistrano/configuration/namespaces.rb:186:in `send'
	from /usr/lib/ruby/gems/1.8/gems/capistrano-2.5.8/lib/capistrano/configuration/namespaces.rb:186:in `method_missing'
	from /usr/lib/ruby/gems/1.8/gems/capistrano-2.5.8/lib/capistrano/configuration/namespaces.rb:104:in `restart'
	from /usr/lib/ruby/gems/1.8/gems/capistrano-2.5.8/lib/capistrano/recipes/deploy.rb:154:in `load'
	from /usr/lib/ruby/gems/1.8/gems/capistrano-2.5.8/lib/capistrano/configuration/execution.rb:139:in `instance_eval'
	from /usr/lib/ruby/gems/1.8/gems/capistrano-2.5.8/lib/capistrano/configuration/execution.rb:139:in `invoke_task_directly_without_callbacks'
	from /usr/lib/ruby/gems/1.8/gems/capistrano-2.5.8/lib/capistrano/configuration/callbacks.rb:27:in `invoke_task_directly'
	from /usr/lib/ruby/gems/1.8/gems/capistrano-2.5.8/lib/capistrano/configuration/execution.rb:89:in `execute_task'
	from /usr/lib/ruby/gems/1.8/gems/capistrano-2.5.8/lib/capistrano/configuration/execution.rb:101:in `find_and_execute_task'
	from /usr/lib/ruby/gems/1.8/gems/capistrano-2.5.8/lib/capistrano/cli/execute.rb:45:in `execute_requested_actions_without_help'
	from /usr/lib/ruby/gems/1.8/gems/capistrano-2.5.8/lib/capistrano/cli/execute.rb:44:in `each'
	from /usr/lib/ruby/gems/1.8/gems/capistrano-2.5.8/lib/capistrano/cli/execute.rb:44:in `execute_requested_actions_without_help'
	from /usr/lib/ruby/gems/1.8/gems/capistrano-2.5.8/lib/capistrano/cli/help.rb:19:in `execute_requested_actions'
	from /usr/lib/ruby/gems/1.8/gems/capistrano-2.5.8/lib/capistrano/cli/execute.rb:33:in `execute!'
	from /usr/lib/ruby/gems/1.8/gems/capistrano-2.5.8/lib/capistrano/cli/execute.rb:14:in `execute'
	from /usr/lib/ruby/gems/1.8/gems/capistrano-2.5.8/bin/cap:4
	from /usr/bin/cap:19:in `load'
	from /usr/bin/cap:19

Removing the Errors

To remove these errors we need to override some of the default tasks.  So we need to identify the tasks that contain the problem actions.  If you scan over the trace you’ll see a number of “* executing `deploy:finalize_update’” lines, some contain the word deploy, these are the tasks in deploy script.  So the deployment script performs the following tasks.

These are the series of tasks that get performed during deployment.  There are two main errors, one calling find on public/images, public/stylesheets and   public/javascripts.  The second is `role_list_from’: unknown role `app’ (ArgumentError) error. 

The first problem with the find command occurs in “deploy:finalize_update”, so we’ll add that task to our own deploy.rb script to stop the ‘find’ call being made.  finalize_update doesn’t really contain anything relevent to a Php site but if you’re curious you should navigate to the directory containing the capistrano gem for me that’s /usr/lib/ruby/gems/1.8/gems/capistrano-2.5.8/lib/capistrano.  Then use the following:

grep 'finalize_update' . -R --color -A20

It should look something like this

  task :finalize_update, :except => { :no_release => true } do
    run "chmod -R g+w #{latest_release}" if fetch(:group_writable, true)

    # mkdir -p is making sure that the directories are there for some SCM's that don't
    # save empty folders
    run <<-CMD
      rm -rf #{latest_release}/log #{latest_release}/public/system #{latest_release}/tmp/pids &&
      mkdir -p #{latest_release}/public &&
      mkdir -p #{latest_release}/tmp &&
      ln -s #{shared_path}/log #{latest_release}/log &&
      ln -s #{shared_path}/system #{latest_release}/public/system &&
      ln -s #{shared_path}/pids #{latest_release}/tmp/pids
    CMD

    if fetch(:normalize_asset_timestamps, true)
      stamp = Time.now.utc.strftime("%Y%m%d%H%M.%S")
      asset_paths = %w(images stylesheets javascripts).map { |p| "#{latest_release}/public/#{p}" }.join(" ")
      run "find #{asset_paths} -exec touch -t #{stamp} {} ';'; true", :env => { "TZ" => "UTC" }
    end
  end

We should probably just include the first line of the task in our deploy.rb, so we have something like this.

role :web, "example.co.uk"

set :use_sudo, false

namespace :deploy do 
	
	desc 'Complete final actions before switching over symlink'
	task :finalize_update, :except => { :no_release => true } do
		run "chmod -R g+w #{latest_release}" if fetch(:group_writable, true)
	end

end


So if we try to deploy now we’ll no longer get the find errors.  We just have the reaper error to solve now.  This error occurs during deploy:restart, if you grep for that task it will look something like this.

  task :restart, :roles => :app, :except => { :no_release => true } do
    try_runner "#{current_path}/script/process/reaper"
  end

Now our php site won’t have this /script/process/reaper script so we just add an empty task to our deploy.rb.  So it should look something like this.

namespace :deploy do 
	
	desc 'Complete final actions before switching over symlink'
	task :finalize_update do 
		run "chmod -R g+w #{latest_release}" if fetch(:group_writable, true)
	end

	desc 'Overrides default action to restart the server - not needed for Expression Engine site'
	task :restart, :roles => :app, :except => { :no_release => true } do
	end

end

Out deployment script will work fine now.  This is just a couple of extra actions we’re going to include so deployment better suits an Expression Engine site. I add the following commands to finalize_update task.

run "cp #{shared_path}/.htaccess #{release_path}/."
run "cp #{shared_path}/database.php #{release_path}/system/expressionengine/config/."
run "cp #{shared_path}/config.php #{release_path}/system/expressionengine/config/."


NOTE: You should replace system with whatever you used for your own expression engine install.

I don’t like to version the environment specific files, those that differ depending on what server they’re on.  For expression engine thats the files .htaccess, database.php and config.php.  I leave these files in shared directory we created at the start of the tutorial and copy them to the correct location during the deployment process. 

Because capistrano creates a fresh new site each time you deploy the site you need to make sure any uploaded content is kept in the shared directory.  You do this by adding a symlink to the image directories.

run "ln -s #{shared_path}/uploads #{release_path}/images/uploads"
run "ln -s #{shared_path}/sized #{release_path}/images/sized"
run "ln -s #{shared_path}/captchas #{release_path}/images/captchas"


So heres the final script, I know setting all this is a long winded process just to deploy a website but it is highly recommended.  It means you don’t need to know anything about the site to deploy it, you just need to use cap deploy.  Also if you need to deploy to more than one server it is a breeze to include. 

The final deploy.rb

set :application, "example.co.uk"
set :repository,  "svn+ssh://svn-repository.co.uk/example"

# If you have previously been relying upon the code to start, stop 
# and restart your mongrel application, or if you rely on the database
# migration code, please uncomment the lines you require below

# If you are deploying a rails app you probably need these:

# load 'ext/rails-database-migrations.rb'
# load 'ext/rails-shared-directories.rb'

# There are also new utility libaries shipped with the core these 
# include the following, please see individual files for more
# documentation, or run `cap -vT` with the following lines commented
# out to see what they make available.

# load 'ext/spinner.rb'              # Designed for use with script/spin
# load 'ext/passenger-mod-rails.rb'  # Restart task for use with mod_rails
# load 'ext/web-disable-enable.rb'   # Gives you web:disable and web:enable

# If you aren't deploying to /u/apps/#{application} on the target
# servers (which is the default), you can specify the actual location
# via the :deploy_to variable:
set :deploy_to, "/var/www/vhosts/www.example.co.uk/"

# If you aren't using Subversion to manage your source code, specify
# your SCM below:
# set :scm, :subversion
# see a full list by running "gem contents capistrano | grep 'scm/'"

role :web, "www.example.co.uk"

set :use_sudo, false

namespace :deploy do 
	
	desc 'Complete final actions before switching over symlink'
	task :finalize_update do 
		run "chmod -R g+w #{release_path}"
		run "cp #{shared_path}/.htaccess #{release_path}/."
		run "cp #{shared_path}/database.php #{release_path}/system/expressionengine/config/."
		run "cp #{shared_path}/config.php #{release_path}/system/expressionengine/config/."
		run "ln -s #{shared_path}/uploads #{release_path}/images/uploads"
		run "ln -s #{shared_path}/sized #{release_path}/images/sized"
		run "ln -s #{shared_path}/captchas #{release_path}/images/captchas"
	end

	desc 'Overrides default action to restart the server - not needed for Expression Engine site'
	task :restart do 
	end

end