This blog post explains how to use Capistrano to deploy PHP, including some tips for integration with Jenkins.
- Background
- What is Capistrano?
- Getting started
- Taking it over for PHP
- Multi-stage deployments
- Tag selection
- Putting it all together
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.
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:
- This diagram gives a good overview of the deployment process (which tasks are run when you do a deploy)
- The wiki is a good place to go
- Reading the code is actually very helpful
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