Mascote

notes to self.

Crazy Fast Deployment of Composer Based Applications With Capistrano 3

If you already use capistrano you already know about the linked_dirs and probably use something like this to keep your PHP libraries across releases:

1
set :linked_dirs, fetch(:linked_dirs, []).push('vendor')

But sometimes you need to run composer to keep add another dependency and sometimes takes a lot of time to download everything or some composer server stop to respond or suffer a slowdown and your deploy breaks.

I developed a deployment strategy to not forget to run composer or install all dependencies everytime.

The ideia is have the composer.phar available before the deploy starts and faster deploy times. (< 30 seconds)

The magic start with the rake file task who is only invoked one time and only if necessary, so we can install composer just once:

1
2
3
file "/tmp/composer.phar" do |f|
  sh "wget -q https://getcomposer.org/composer.phar -O #{f.name}"
end

Here is another trick, if you do not put the composer.lock file on your repository, composer will try to install all the dependencies and create the lock file, so here whe create the file and let composer update their content later to speed next deployments:

1
2
3
file "/tmp/composer.lock" do |f|
  touch f.name
end

The remote_file set the first argument as a prerequisite and if the file no exist the file task with the name '/tmp/composer.phar' will be called. The '/tmp/composer.phar' is a rake task name and also a path. You can see more about remote_file here.

The file task will run and the file will be created on the shared_path, otherwise, nothing happens.

1
2
remote_file 'composer.phar' => '/tmp/composer.phar', roles: :app
remote_file 'composer.lock' => '/tmp/composer.lock', roles: :app

And we link these tasks above on the check flow who happens before the deploy to ensure the capistrano.phar and capistrano.lock will be available on the shared_path and linked to the release_path later:

1
2
3
4
5
namespace :deploy do
  namespace :check do
    task :linked_files => ["composer.phar", "composer.lock"]
  end
end

Finally we always run composer to ensure the dependencies will be on available to the application:

1
2
3
4
5
6
7
8
9
10
11
namespace :deploy do
  #...
  desc "updates composer"
  after :updated, :vendor do
    on roles(:app), in: :groups, limit: 3 do
      within release_path do
        execute :php, "composer.phar install --no-dev --optimize-autoloader"
      end
    end
  end
end

TL;DR

The whole thing:

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
29
30
#...

set :linked_files, fetch(:linked_files, []).push('composer.phar', 'composer.lock')
set :linked_dirs, fetch(:linked_dirs, []).push('vendor')

file "/tmp/composer.phar" do |f|
  sh "wget -q https://getcomposer.org/composer.phar -O #{f.name}"
end

file "/tmp/composer.lock" do |f|
  touch f.name
end

remote_file 'composer.phar' => '/tmp/composer.phar', roles: :app
remote_file 'composer.lock' => '/tmp/composer.lock', roles: :app

namespace :deploy do
  namespace :check do
    task :linked_files => ["composer.phar", "composer.lock"]
  end

  desc "updates composer"
  after :updated, :vendor do
    on roles(:app), in: :groups, limit: 3 do
      within release_path do
        execute :php, "composer.phar install --no-dev --optimize-autoloader"
      end
    end
  end
end

Try to remove one or another dependency from the vendor directory and see composer installing only the missing piece on the next deploy!

You can find me on Twitter as @fgbreel.

See you next time!