PHP Deployment with Capistrano

This blog post explains how to use Capistrano to deploy PHP, including some tips for integration with Jenkins.

Background

Back when I worked at Imagini, we used the home-baked Cruftly Cloudly deployment system (built by @pikesley) to roll releases. It had some nice features:

  • new code had to be pushed to staging first (as a release candidate)
  • final deploys were always against the exact release candidate
  • it would release to a bunch of machines in one hit, with symlinks switched at the end
  • from a developer perspective, it was “push button” (we just ran a script that requested a deploy of our code)
  • it presented an ASCII art dinosaur upon successful release

Once I’d moved on I was keen to recreate the Cloudly experience, but I didn’t want to have to hand-craft my own solution. Luckily Capistrano now exists which provides a very handy tool to deploy code from SCM to one or more servers.

Cruftosaurus

What is Capistrano?

Capistrano is a developer tool for deploying web applications”

Capistrano is written in Ruby and offers up a basic DSL from which you can craft quite flexible deployment scripts. Typically, the deploy process would be to deploy a particular version (branch, commit etc.) from SCM to one or more boxes (into a new folder on the server), then switch a symlink so that they all immediately run off the new code. Support for instant rollback is provided. That said, it’s very flexible. In my current setup I have it deploying to multiple environments (dev, staging, production), building code (think Phing), running tests on the servers before finalising the deploy and then restarting worker processes on completion.

All of this functionality is driven from a simple command line interface:

cap deploy
cap deploy:rollback

We can list all the available commands with:

cap -T
cap deploy               # Deploys your project.
cap deploy:check         # Test deployment dependencies.
cap deploy:cleanup       # Clean up old releases.
cap deploy:cold          # Deploys and starts a `cold' application.
cap deploy:migrations    # Deploy and run pending migrations.
cap deploy:pending       # Displays the commits since your last deploy.
cap deploy:pending:diff  # Displays the `diff' since your last deploy.
cap deploy:rollback      # Rolls back to a previous version and restarts.
cap deploy:rollback:code # Rolls back to the previously deployed version.
cap deploy:start         # Blank task exists as a hook into which to install ...
cap deploy:stop          # Blank task exists as a hook into which to install ...
cap deploy:symlink       # Updates the symlink to the most recently deployed ...
cap deploy:update        # Copies your project and updates the symlink.
cap deploy:update_code   # Copies your project to the remote servers.
cap deploy:upload        # Copy files to the currently deployed version.
cap deploy:web:disable   # Present a maintenance page to visitors.
cap deploy:web:enable    # Makes the application web-accessible again.
cap invoke               # Invoke a single command on the remote servers.
cap shell                # Begin an interactive Capistrano session.

Some tasks were not listed, either because they have no description,
or because they are only used internally by other tasks. To see all
tasks, type `cap -vT'.

Getting started

Step 1 – install capistrano.

apt-get install capistrano

Step 2 – “capify” one of your projects

cd /my/project/location
capify

This step creates the files Capfile and config/deploy.rb.

Taking it over for PHP

Capistrano is pretty simple, these are the basics:

  • Capistrano runs on your local machine (it’s not a server-side thing)
  • A recipe is a bunch of named tasks that combine to define your deploy process
  • The “default” process is tailored for releasing Rails applications – therefore you’ll have to customise the recipes for PHP
  • Capistrano is built around the concept of roles, but for a simple PHP setup you can just have a “web” role and think of this as meaning “where I’m going to deploy to”

To learn more about the Capistrano deployment process, I found the following useful:

So back to PHP.. I followed these directions (from the aptly named “Capistrano PHP” project). All we are doing is overriding the finalise update and migrate tasks. The restart task is actually blank by default, so we only need to define it if we want it to do something. We’ll make it reload nginx (I symlink the nginx config from the deployed code).

## php cruft ##

# https://github.com/mpasternacki/capistrano-documentation-support-files/raw/master/default-execution-path/Capistrano%20Execution%20Path.jpg
# https://github.com/namics/capistrano-php

namespace :deploy do

  task :finalize_update, :except => { :no_release => true } do
    transaction do
      run "chmod -R g+w #{releases_path}/#{release_name}"
    end
  end

  task :migrate do
    # do nothing
  end

  task :restart, :except => { :no_release => true } do
    run "sudo service nginx reload"
  end
end

Multi-stage deployments

I needed to be able to deploy to different environments – dev, staging and production. The obvious starting point was Googling, which led to the Capistrano multistage extension. I worked through this for some time, however the requirement for an extra dependency seemed more complicated than necessary. The footnote on the multistage page offered an alternative – multiple stages without the multistage extension.

With this pattern, all we have to do is define extra tasks for each of our environments. Within these tasks we define the key information about the environment, namely the roles that we want to deploy to (which servers we have).

## multi-stage deploy process ##

task :dev do
  role :web, "dev.myproject.example.com", :primary => true
end

task :staging do
  role :web, "staging.myproject.example.com", :primary => true
end

task :production do
  role :web, "production.myproject.example.com", :primary => true
end

Now when we deploy we have to include an environment name in the command. I don’t bother defining a default, so leaving it out will throw an error (you could define a default if you wanted to).

cap staging deploy

Tag selection

The next feature I wanted from my killer deploy system was the ability to release specific versions. My plan was

  • Jenkins would automatically release master on every push
  • a separate Jenkins project would automatically tag and release production-ready “builds” to a staging environment (anything pushed to release branch)
  • releasing to production would always involve manually tagging with a friendly version number

Nathan Hoad had some good advice on releasing a specific tag via Capistrano – including a snippet that makes Capistrano ask you what tag to release, defaulting to the most recent. One change I made was the addition of the unless exists?(:branch) condition, which means we can setup dev and staging releases to go unsupervised.

## tag selection ##

# we will ask which tag to deploy; default = latest
# http://nathanhoad.net/deploy-from-a-git-tag-with-capistrano
set :branch do
  default_tag = `git describe --abbrev=0 --tags`.split("\n").last

  tag = Capistrano::CLI.ui.ask "Tag to deploy (make sure to push the tag first): [#{default_tag}] "
  tag = default_tag if tag.empty?
  tag
end unless exists?(:branch)

For staging, I use this handy bit of bash foo, courtesy of @jameslnicholson (split to aid readability):

set :branch, `git tag | \
    xargs -I@ git log --format=format:"%ci %h @%n" -1 @ | \
    sort | \
    auk '{print  $5}' | \
    egrep '^b[0-9]+$' | \
    tail -n 1`

Putting it all together

Here’s a fictitious deployment script for the “We Have Your Kidneys” ad network. There are some extra nuggets in here that are worth highlighting.

Run a task once deployment has finished:

after "deploy", :except => { :no_release => true } do

Run build script, including tests. This will abort the deployment if they do not pass:

# run our build script
run "echo '#{app_environment}' > #{releases_path}/#{release_name}/config/environment.txt"
run "cd #{releases_path}/#{release_name} && phing build"

The deployment script in full

# Foo Bar deployment script (Capistrano)

## basic setup stuff ##

# http://help.github.com/deploy-with-capistrano/
set :application, "Foo Bar PHP Service"
set :repository, "git@github.com:davegardnerisme/we-have-your-kidneys.git"
set :scm, "git"
default_run_options[:pty] = true
set :deploy_to, "/var/www/we-have-your-kidneys"

# use our keys, make sure we grab submodules, try to keep a remote cache
ssh_options[:forward_agent] = true
set :git_enable_submodules, 1
set :deploy_via, :remote_cache
set :use_sudo, false

## multi-stage deploy process ###

# simple version @todo make db settings environment specific
# https://github.com/capistrano/capistrano/wiki/2.x-Multiple-Stages-Without-Multistage-Extension

task :dev do
  role :web, "dev.davegardner.me.uk", :primary => true
  set :app_environment, "dev"
  # this is so we automatically deploy current master, without tagging
  set :branch, "master"
end

task :staging do
  role :web, "staging.davegardner.me.uk", :primary => true
  set :app_environment, "staging"
  # this is so we automatically deploy the latest numbered tag
  # (with staging releases we use incrementing build number tags)
  set :branch, `git tag | xargs -I@ git log --format=format:"%ci %h @%n" -1 @ | sort | awk '{print  $5}' | egrep '^b[0-9]+$' | tail -n 1`
end

task :production do
  role :web, "prod01.davegardner.me.uk", :primary => true
  role :web, "prod02.davegardner.me.uk"
  set :app_environment, "production"
end

## tag selection ##

# we will ask which tag to deploy; default = latest
# http://nathanhoad.net/deploy-from-a-git-tag-with-capistrano
set :branch do
  default_tag = `git describe --abbrev=0 --tags`.split("\n").last

  tag = Capistrano::CLI.ui.ask "Tag to deploy (make sure to push the tag first): [#{default_tag}] "
  tag = default_tag if tag.empty?
  tag
end unless exists?(:branch)

## php cruft ##

# https://github.com/mpasternacki/capistrano-documentation-support-files/raw/master/default-execution-path/Capistrano%20Execution%20Path.jpg
# https://github.com/namics/capistrano-php

namespace :deploy do

  task :finalize_update, :except => { :no_release => true } do
    transaction do
      run "chmod -R g+w #{releases_path}/#{release_name}"
      # run our build script
      run "echo '#{app_environment}' > #{releases_path}/#{release_name}/config/environment.txt"
      run "cd #{releases_path}/#{release_name} && phing build"
    end
  end

  task :migrate do
    # do nothing
  end

  task :restart, :except => { :no_release => true } do
    # reload nginx config
    run "sudo service nginx reload"
  end

  after "deploy", :except => { :no_release => true } do
    run "cd #{releases_path}/#{release_name} && phing spawn-workers > /dev/null 2>&1 &", :pty => false
  end
end

Tags: , , ,

6 Responses to “PHP Deployment with Capistrano”

  1. codecowboy says:

    Any advice on replacing tokens in a config file? I’m using Slim for a small project. It has a configuration array and I would like to populate that with environment specific database details, for example.

  2. Dave says:

    I use per-environment application configuration files, built by a build script (eg: Phing). Cap simply pulls the strings to make this happen.

  3. Olle Jonsson says:

    The string auk is a mistyped awk, right?

  4. Khair says:

    git-deploy-diff’ could be a potential ruby gem name. since your scpirt is deploying the diff between what is already there and the current head, it is still pretty descpirtive

  5. Alex says:

    You may want to check out our deploydo (http://www.deploy.do) as a different approach to this. It takes care of the whole process and even shows you line-by-line comparisons when updating code.

  6. Andrew says:

    I just found an error:

    cd /my/project/location
    capify

    must be:

    cd /my/project/location
    capify .

    Regards

Leave a Reply