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.
- /home/mark/public_html/app
- /home/mark/public_html/app/webroot
- /home/mark/public_html/cake
- /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.
- /home/mark/mark-story.com/releases # The last 5 releases
- /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:
- /home/mark/mark-story.com/releases
- /home/mark/mark-story.com/shared/config
- /home/mark/mark-story.com/shared/cake
- /home/mark/mark-story.com/shared/plugins
- /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
- set :application, "mark-story.com"
- set :repository, "/Users/markstory/Sites/mark_story/site/"
- set :branch, "master"
- set :scm, :git
- # Deploy settings
- set :deploy_to, "/home/mark/#{application}"
- set :deploy_via, :copy
- set :copy_exclude, [".git/*", ".gitignore"]
- set :copy_compression, :gzip
- # Use account tmp dir as /tmp is wierd.
- set :copy_remote_dir, '/home/mark/tmp'
- # Configure which plugins are going to be updated when the code is deployed.
- set :plugins_dir, "/Users/markstory/Sites/cake_plugins"
- set :app_plugins, ['asset_compress']
- # Options
- set :use_sudo, false
- set :keep_releases, 5
- # Roles & servers
- role :app, "mark-story.com"
- server 'mark-story.com', :app, :primary => true
- set :user, 'mark'
- # Environments
- task :production do
- set :deploy_to, '/home/mark/'
- end
- # Deployment tasks
- namespace :deploy do
- desc "Override the original :restart"
- task :restart, :roles => :app do
- clear_cache
- end
- task :finalize_update, :roles => :app do
- # Link cakephp. Not the ideal linking but it works.
- run "ln -s #{shared_path}/cakephp #{current_release}/cake"
- # Link configuration files
- run "ln -s #{shared_path}/config/core.php #{current_release}/config/core.php"
- run "ln -s #{shared_path}/config/database.php #{current_release}/config/database.php"
- # Link uploaded files.
- run "rm -rf #{current_release}/webroot/img/downloads;
- ln -s #{shared_path}/webroot/downloads #{current_release}/webroot/img/downloads"
- run "rm -rf #{current_release}/webroot/img/portfolio;
- ln -s #{shared_path}/webroot/portfolio #{current_release}/webroot/img/portfolio"
- run "ln -s #{shared_path}/webroot/files #{current_release}/webroot/files"
- run "ln -s #{shared_path}/webroot/demos #{current_release}/webroot/demos"
- # Link tmp
- run "rm -rf #{current_release}/tmp"
- run "ln -s #{shared_path}/tmp #{current_release}/tmp"
- # Link plugins
- deploy.plugins.symlink
- end
- namespace :plugins do
- desc "Symlinks the configured plugins for the appliction into plugins, from the shared dirs."
- task :symlink, :roles => :app do
- app_plugins.each { |plugin|
- run "ln -s #{shared_path}/plugins/#{plugin} #{latest_release}/plugins/#{plugin}"
- }
- end
- end
- namespace :web do
- desc "Setup lock file"
- task :disable, :roles => :app do
- run "touch #{current_release}/webroot/.capistrano-lock"
- end
- desc "Enable the current access after deployment"
- task :enable, :roles => :app do
- run "rm #{current_release}/webroot/.capistrano-lock"
- end
- end
- end
- namespace :clear_cache do
- desc <<-DESC
- Blow up all the cache files CakePHP uses, ensuring a clean restart.
- DESC
- task :default do
- # Remove absolutely everything from TMP
- run "rm -rf #{shared_path}/tmp/*"
- # Create TMP folders
- run "mkdir -p #{shared_path}/tmp/{cache/{models,persistent,views},sessions,logs,tests}"
- end
- end
- namespace :pending do
- desc <<-DESC
- Displays the 'diff' since your last deploy. This is useful if you want \
- to examine what changes are about to be deployed. Note that this might \
- not be supported on all SCM's.
- DESC
- task :diff, :except => { :no_release => true } do
- system(source.local.diff(current_revision))
- end
- desc <<-DESC
- Displays the commits since your last deploy. This is good for a summary \
- of the changes that have occurred since the last deploy. Note that this \
- might not be supported on all SCM's.
- DESC
- task :default, :except => { :no_release => true } do
- from = source.next_revision(current_revision)
- system(source.local.log(from))
- end
- 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.
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
nice… will give it a try..
Harsha M V 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
andcap prod deploy
is how I deploy http://cakepackages.comIn 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
Hi Jose
Thanks!
Gerhard Sletten 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