Deploying a CakePHP site with Capistrano

In the past I’ve used a variety of tools to deploy client sites, most often using version control. However, for my blog I’ve always used FTP. Its a pretty old-school approach, and something that I’ve been lacking the time to correct. Last weekend I finally took the plunge and figured out how to get Capistrano to deploy my site. Because of my hosting setup I wasn’t able to use capcake. But if you have git installed on your server, its worth taking a look at.

Installing Capistrano & getting ready

This was the easiest part of the process. Just run gem install capistrano and you’re done. I then capify’ied my site by running capify from the app root. I then spent a while getting familiar with how Capistrano actually worked and evaluating how I would have to change my current deployment to work better with Capistrano. Before using Capistrano my directory layout looked like.

Show Plain Text
  1. /home/mark/public_html/app
  2. /home/mark/public_html/app/webroot
  3. /home/mark/public_html/cake
  4. /home/mark/public_html/vendors

Far from the best setup as everything is inside the DocumentRoot. Fixing the files exposed to the outside world was another thing I wanted to address while changing how I deploy my site. I initially configured Capistrano to make the following directories.

Show Plain Text
  1. /home/mark/mark-story.com/releases # The last 5 releases
  2. /home/mark/mark-story.com/current  # The current release
  3. /home/mark/mark-story.com/shared   # Files shared among

I would then symlink ~/public_html to ~/mark-story.com/current/webroot. This would minimize the files exposed to the outside world, and work with how Capistrano likes to do things.

Other bits that needed wiring

Configuration and uploaded files posed a small hurdle, I solved both with symlinks. Configuration files were put in ~/mark-story.com/shared/config, and symlinked in on deploy. Uploaded files would follow the same pattern. After creating ~/mark-story.com/shared/webroot to hold the files, I used more symlinks to wire things together. I ended up putting my cakephp core and plugins for my site into ~/mark-story.com/shared/ as well. This resulted in a directory structure that looked like:

Show Plain Text
  1. /home/mark/mark-story.com/releases
  2. /home/mark/mark-story.com/current
  3. /home/mark/mark-story.com/shared/config
  4. /home/mark/mark-story.com/shared/cake
  5. /home/mark/mark-story.com/shared/plugins
  6. /home/mark/mark-story.com/shared/webroot

Getting all the directories and symlink wiring setup was an iterative process, I made lots of mistakes along the way. With some help from savant on IRC, and stealing some functions from the previously mentioned capcake, I ended up with the following deploy.rb

Show Plain Text
  1. set :application, "mark-story.com"
  2. set :repository,  "/Users/markstory/Sites/mark_story/site/"
  3. set :branch, "master"
  4. set :scm, :git
  5.  
  6. # Deploy settings
  7. set :deploy_to, "/home/mark/#{application}"
  8. set :deploy_via, :copy
  9.  
  10. set :copy_exclude, [".git/*", ".gitignore"]
  11. set :copy_compression, :gzip
  12.  
  13. # Use account tmp dir as /tmp is wierd.
  14. set :copy_remote_dir, '/home/mark/tmp'
  15.  
  16. # Configure which plugins are going to be updated when the code is deployed.
  17. set :plugins_dir, "/Users/markstory/Sites/cake_plugins"
  18. set :app_plugins, ['asset_compress']
  19.  
  20. # Options
  21. set :use_sudo, false
  22. set :keep_releases, 5
  23.  
  24. # Roles & servers
  25. role :app, "mark-story.com"
  26.  
  27. server 'mark-story.com', :app, :primary => true
  28. set :user, 'mark'
  29.  
  30. # Environments
  31. task :production do
  32.   set :deploy_to, '/home/mark/'
  33. end
  34.  
  35. # Deployment tasks
  36. namespace :deploy do
  37.   desc "Override the original :restart"
  38.   task :restart, :roles => :app do
  39.     clear_cache
  40.   end
  41.  
  42.   task :finalize_update, :roles => :app do
  43.     # Link cakephp. Not the ideal linking but it works.
  44.     run "ln -s #{shared_path}/cakephp #{current_release}/cake"
  45.  
  46.     # Link configuration files
  47.     run "ln -s #{shared_path}/config/core.php #{current_release}/config/core.php"
  48.     run "ln -s #{shared_path}/config/database.php #{current_release}/config/database.php"
  49.  
  50.     # Link uploaded files.
  51.     run "rm -rf #{current_release}/webroot/img/downloads;
  52.         ln -s #{shared_path}/webroot/downloads #{current_release}/webroot/img/downloads"
  53.  
  54.     run "rm -rf #{current_release}/webroot/img/portfolio;
  55.         ln -s #{shared_path}/webroot/portfolio #{current_release}/webroot/img/portfolio"
  56.  
  57.     run "ln -s #{shared_path}/webroot/files #{current_release}/webroot/files"
  58.     run "ln -s #{shared_path}/webroot/demos #{current_release}/webroot/demos"
  59.  
  60.     # Link tmp
  61.     run "rm -rf #{current_release}/tmp"
  62.     run "ln -s #{shared_path}/tmp #{current_release}/tmp"
  63.  
  64.     # Link plugins
  65.     deploy.plugins.symlink
  66.   end
  67.  
  68.   namespace :plugins do
  69.     desc "Symlinks the configured plugins for the appliction into plugins, from the shared dirs."
  70.     task :symlink, :roles => :app do
  71.       app_plugins.each { |plugin|
  72.         run "ln -s #{shared_path}/plugins/#{plugin} #{latest_release}/plugins/#{plugin}"
  73.       }
  74.     end
  75.   end
  76.  
  77.   namespace :web do
  78.     desc "Setup lock file"
  79.     task :disable, :roles => :app do
  80.         run "touch #{current_release}/webroot/.capistrano-lock"
  81.     end
  82.  
  83.     desc "Enable the current access after deployment"
  84.     task :enable, :roles => :app do
  85.       run "rm #{current_release}/webroot/.capistrano-lock"
  86.     end
  87.   end
  88. end
  89.  
  90. namespace :clear_cache do
  91.   desc <<-DESC
  92.     Blow up all the cache files CakePHP uses, ensuring a clean restart.
  93.   DESC
  94.   task :default do
  95.     # Remove absolutely everything from TMP
  96.     run "rm -rf #{shared_path}/tmp/*"
  97.  
  98.     # Create TMP folders
  99.     run "mkdir -p #{shared_path}/tmp/{cache/{models,persistent,views},sessions,logs,tests}"
  100.   end
  101. end
  102.  
  103. namespace :pending do
  104.   desc <<-DESC
  105.     Displays the 'diff' since your last deploy. This is useful if you want \
  106.     to examine what changes are about to be deployed. Note that this might \
  107.     not be supported on all SCM's.
  108.  DESC
  109.  task :diff, :except => { :no_release => true } do
  110.    system(source.local.diff(current_revision))
  111.  end
  112.  
  113.  desc <<-DESC
  114.    Displays the commits since your last deploy. This is good for a summary \
  115.    of the changes that have occurred since the last deploy. Note that this \
  116.    might not be supported on all SCM's.
  117.   DESC
  118.   task :default, :except => { :no_release => true } do
  119.     from = source.next_revision(current_revision)
  120.     system(source.local.log(from))
  121.   end
  122. end

I have to say that after setting up Capistrano, I’m super happy with how it works. I love being able to use one command to update my site and not have to worry about accidentally breaking something due to fat fingering something in my FTP client. While I still need to update Cake core and my plugins manually I do that pretty rarely so it shouldn’t pose a problem.

Comments

Does the clear_cache task actually work? mkdir -p never works for me when doing it through capistrano, although that may be because I am using Capistrano multi-environment.

Nice to see you got everything settled, let me know if you need any more help with Capistrano.

Jose Diaz-Gonzalez on 5/30/10

Hi, I made a railsless deploy extension for just this reason: http://github.com/leehambley/railsless-deploy –– the file replaces the deploy.rb in the main gem, with one with the tasks removed from the chain, not simple stubbed out as above… hopefully it provides whatever you need :)

Lee Hambley on 6/1/10

What do you gain with using Capistrano in deployment.

I keep a Git bare repo with my app which I push from my to from my macbook. Then I clone this .bare repo in a regular repo on the same server in my apps/ directory. For the /css and /img folder I just make a symlink in web_root and add a custom index.php also in the web_root with the right path to Cake and my app.

After I push new commits to the Git bare rep I log in via ssh and pull these commits into my live application.

Setting this up: 20-30 min
Updating: in no time..

Can Capistrano do it better?

Gerhard Sletten on 6/5/10

@gersh

I type cap deploy and my code is deployed. I don’t need to ssh into the box, cd to the folder, type “git pull origin master” etc.

Multi-environment is easy. cap dev deploy and cap prod deploy is how I deploy http://cakepackages.com

In Mark Story’s case, he had no access to git on the server, so using your method is out of the question. Capistrano saves the day.

Jose Diaz-Gonzalez on 6/5/10

Mark, did you end up having problems with getting files created under current/app/tmp/cache?

I’ve got a basic setup that currently uses file cache, but CakePHP (1.3.2) ends up looking for the tmp/cache under releases because of the definition of APP_DIR. Because I have softlinked tmp from shared to current, similar to what you have done, current/tmp/cache is never found, because CakePHP looks for it under releases/2010…/app/tmp/cache. (I think I said the same thing twice in a row)

Regards
Reuben Helms

Reuben Helms on 6/16/10

Gah, don’t mind me. I had linked tmp to /tmp instead of /app/tmp.

Reuben Helms on 6/16/10

Hello Mark,

i have created an own deployment shell for CakePHP, because i don’t feel good with mixing php and ruby ;)

I think an integrated CakePHP deployment shell could be a nice think to the comunity, could’n it?

details can find here:
http://blog.lubico.biz/en/2010/07/cakephp-deployment/
git://github.com/sassman/deployment_shell.git

sassman on 7/16/10

What is inside your releases & current folder.

Friskd on 12/2/10

Comments are not open at this time.