PHP+Capistrano+Shared Host = Easy Deployments

I wrote a Rails app that was deployed with Capistrano and I loved it. Every time I made a change, one file or a dozen, I could get all the files up in a single command. In addition, if anything was broken I could revert to the previous deploy with cap deploy:rollback.

While using Capistrano is considered a “standard” with Rails, it’s something that I think should be used more with PHP as well. When I built my last website with PHP I reverted back to my FTP client deploys, and it was a nightmare. I worried about the order in which I uploaded each file and making sure I uploaded all of my changes.

I’m aware of the services like Beanstalk that let you do a git push and will move the modified files for you. But, I’m not a big fan of sharing my entire repository or giving access to my server. Plus, I’m a little cheap, especially when I know I can write my own system for free. It may not be as fancy and it may require a little more work, but I like having complete control.

The Requirement

I don’t want to explain how to make deployments with Capistrano. There are a lot of tutorials on the web that do a much better job than I can. So, if you’re not familiar with Capistrano, you will need to follow a tutorial. The Capistrano Wiki has quite a few resources that you may find helpful.

However, there is one modification that is far different than most Capistrano tutorials. On my shared host, I wasn’t allowed to change the DocumentRoot for my primary domain. As a result, I had to create a symlink from ~/public_html to the latest release on every deploy. While it sounds difficult, it’s actually very easy.

The Deployment Strategy

This is where all the magic of Capistrano takes place. It’s where you get to decide what takes place and when. You can define your entire deploy strategy here and Capistrano will make sure it happens, every time.

set :user, "USERNAME"

# about the application
set :application, "example.com"
set :port, 2222

# the type and location of the repository
set :scm, :git
set :repository, "."

# how and where it should be uploaded
set :deploy_to, "/home/#{user}/sites/#{application}"
set :deploy_via, :copy

# what should be left behind in the copy
set :copy_exclude, %w[.git .DS_Store .gitignore .gitmodules .sass-cache sass Capfile config config.rb]

# which server to use
server application, :app

# shared hosting != sudo privileges
set :use_sudo, false

# only keep the last four releases on the server
set :keep_releases, 4

namespace :deploy do
  desc "Add symlink to `public_html`"
  task :public_html_symlink, :rols => :app do
    on_rollback do
      if previous_release
        run "rm -f ~/public_html; ln -s #{previous_release} ~/public_html; true"
      else
        logger.important "no previous release to rollback to, rollback of symlink skipped for ~/public_html"
      end
    end
    
    run "rm -f ~/public_html && ln -s #{release_path} ~/public_html"
  end
  
  desc "Remove group write privileges"
  task :finalize_update do
    transaction do
      run "chmod -R g-w #{release_path}"
      
       # copy all items from the shared folder
      shared_children.map do |d|
        if d.rindex('/')
          run "rm -rf #{latest_release}/#{d} && mkdir -p #{latest_release}/#{d.slice(0..(d.rindex('/')))}"
        else
          run "rm -rf #{latest_release}/#{d}"
        end
        
        run "ln -s #{shared_path}/#{d.split('/').last} #{latest_release}/#{d}"
      end
    end
  end
  
  # disable rails stuff
  task :migrate do; end
  task :restart do; end
end

# removes older releases
after "deploy:update", "deploy:cleanup"

after "deploy:create_symlink", "deploy:public_html_symlink"

The most important change to the default deployment is on line 38. It deletes the default ~/public_html directory and replaces it with a symlink to the latest release. A very simple command that makes the whole thing work.

The result is when I want to push the changes I made I can run cap deploy and my entire application is uploaded. I don’t have to worry about individual files, and if something breaks I can always return to the previous release with cap deploy:rollback.