Boris Guéry

Hacking the web since 1997.

PHP Deployment With Capistrano 3.x

Since one year or two (I pushed a quick tutorial some months ago on Github), I intensively use Capistrano as well as its “Symfony” variant, capifony. And I’m really happy. I deploy in production ten times a day, and it really helps getting things done, securely.

For a new project I work on, I decided to go with it, again. But it’s time to evolve, so I decided to go with Capistrano 3.x. As of this day (2013-11-27) the version 3.0 seems stable, yet not complete though.

The documentation is still at its early days, and I’m obviously not a Rubyist. However, things have clearly evolved, in the good way, and now Capistrano is not shipped with all the Ruby/Rails tasks that we, PHP guys, don’t need. The tool is not bloated with all those things and that’s good for us.

Anyway, I decided to dive in the process of deploying a project with the new Capistrano 3.

As the migration note states, it is recommended to start over, do no try to make Capistrano 2.x Capfile works with Capistrano 3.x, it won’t, work, at, all.

One of the first thing I mentioned when installed Capistrano 3.0 is that I was not able to override paths! By default, Capistrano 3.x cap install (command which generates a skeleton deployment scripts) generates files in config/ folder. Maybe that’s the way to go in Ruby projects, but that’s not my taste, and I sometimes like to make things work how I want they work (yeah I’m that kind of guys).

Override the capistrano/setup.rb was not enough because the stages management, which by the way, are now bundled in Capistrano 3, is hard coded in the module.

Luckily, an issue had been posted a while ago, and luckily again, it got merged.

Being merged on an unstable branch, I had to build the gem myself from source, here we go:

1
2
3
$ git clone https://github.com/capistrano/capistrano.git
$ gem build capistrano.gemspec
$ gem install capistrano-3.1.0.gem

Easy isn’t it?

Now that we’ve a more customizable version of Capistrano, we can get back to work.

So the first thing to do is to generate the default deployment scripts shipped by Capistrano:

1
$ cap install

I like my deployment scripts to live in, guess, the deployement/ folder, I move them there.

1
2
$ mv config deployment
$ mv deployment/deploy deployment/stages

It makes more sense to me to move stages configuration there.

So far we have a Capfile which looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
# Override the path before running the setup
set :deploy_config_path, 'deployment/deploy.rb'
set :stage_config_path, 'deployment/stages/'

# Load DSL and Setup Up Stages
require 'capistrano/setup'

# Includes default deployment tasks
require 'capistrano/deploy'

# Override the default path to bundle deployments scripts and tasks
Dir.glob('deployment/tasks/*.cap').each { |r| import r }`

And our folder structure looks like this:

1
2
3
4
5
6
deployment
├── deploy.rb
├── stages
│   ├── production.rb
│   └── staging.rb
└── tasks

One deploy.rb global configuration, two stages (production & staging), no custom tasks, yet.

To get a minimal working deployment script, we have to configure some variables:

In deployment/deploy.rb:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# Give our application a name
set :application, 'app'

# The repository path, must be accessible from the remote server
set :repo_url, 'ssh://domain.tld/repositories/project.git'

# The path where to deploy things
set :deploy_to, '/var/www/domain.tld'

# The default tasks shipped by the install task
namespace :deploy do
  desc 'Restart application'
  task :restart do
    on roles(:app), in: :sequence, wait: 5 do
      # Your restart mechanism here, for example:
      # execute :touch, release_path.join('tmp/restart.txt')
    end
  end
  after :restart, :clear_cache do
    on roles(:web), in: :groups, limit: 3, wait: 10 do
      # Here we can do anything such as:
      # within release_path do
      #   execute :rake, 'cache:clear'
      # end
    end
  end
  after :finishing, 'deploy:cleanup'
end

Note that the deploy_path variable is set to /var/www/domain.tld but the actually deployed project will live in releases/[some-release-date] and symlinked to current/, so any virtualhost should have its document root sets to current/public folder.

To authenticate against the remote repository, I use an ssh-agent with a forwarding capabilities. It avoids to create unnecessary ssh keys on each servers.

Now, we need to configure the production stage (or the staging stage, no matter):

1
2
set :stage, :production
server 'production.domain.tld', user: 'borisguery', roles: %w{web app}

Dead simple.

Time to test, run cap production deploy. Capistrano should now be able to connect to the remote server, create the folders (your deployment user should have the permissions, use POSIX ACL for this), and clone the remote repository.

We are done for now, the next step is to create custom tasks. For now, I’m only interested in two things:

Composer has already a gem for this, so we’ll use it.

For the sake of simplicity I’ll start to add it to a Gemfile, it is very similar to our composer.json, it is used to define dependencies requirements for a project.

1
2
3
source 'https://rubygems.org'

gem 'capistrano-composer'

Then make sure the gem is actually loaded by Capistrano, in Capfile:

1
require 'capistrano/composer'

It will automagically execute itself after the deploy:updated tasks.

Now, let’s take a look at grunt. Grunt relies on npm/bower packages to execute tasks. In order to properly run grunt on your remote server, we need to make sure nodejs & npm are available.

Grunt itself being a library, not an actually cli command, we also need to install the grunt-cli npm package, it should be done once, and globally on the remote server. (In my case it is part of my provisioning process using puppet)

1
$ sudo npm -g install grunt-cli

And guess what, the same apply to bower:

1
$ sudo npm -g install bower

Our server is ready to get fed with some bytes of deployment process.

Now, let’s add the according gems to our Gemfile

1
2
3
require 'capistrano/npm'
require 'capistrano/bower'
require 'capistrano/grunt'

I decided to use a Capistrano task to resolve bower dependencies but it could have been done within grunt itself with the proper plug-in.

Configure the task we want grunt to run:

1
set :grunt_tasks, 'dist'

If not set, it will execute the default task, as if we ran grunt command alone.

To make sure our tasks will execute in the correct order (hence, npm:install, bower:install before grunt), we add the following statement at the end of deploy.rb:

1
after 'bower:install', 'grunt'

We are done.

Comments