Deployer recipe for Drupal 8

We all know that deployment is a PITA. Fortunately, we have tools like Deployer that make our lives easier. Deployer is an open-source tool to deploy PHP applications, it is composed of "recipe" scripts with tasks that support Drupal deployments, but also of other MVC/CMS like Laravel, CakePHP, Codeigniter or WordPress.

Deployer enables the following features:

  • Parallel deployment. Roll out new features on different servers at the same time.
  • Agentless utility. Just set up SSH access to the remote machine and you are good to go.
  • Rollback. Screwed up? No problem, you can rollback applications to the previous version with a command.
  • Zero downtime. It creates a release for every new deployment so you can switch between different releases without downtime.

I won't dwell more on Deployer's presentation, this article provides a good explanation of its basic functioning. I suggest you read it and then, if you don't get stuck on mindless internet surfing, like I often do, feel free to jump to the following lines.

The basic recipe

Deployer comes with recipes for Drupal 7 and 8 and they pretty much work out of the box. You can have every definition in deploy.php recipe file, but I find useful to have the hosting definitions on hosts.yml. Let's start there:

# hosts.yml

.base: &base
  writable_mode: chmod
  port: 22
  shellCommand: bash -ls
  identityFile: ~/.ssh/yoursshhostnamekeys
  drush_path: /usr/local/bin/php -q ~/.composer/vendor/drush/drush/drush.php --php=/usr/local/bin/php # Useful if you want to perform drush tasks on remote server
  ssh_type: native
  multiplexing: false
  sshOptions:
    StrictHostKeyChecking: no

staging.yourhostname:
  <<: *base
  stage: staging
  hostname: your-hostname.com
  user: your-ssh-username
  deploy_path: /thefullpathtotheserver
  drush_alias: staging.yourhostname # I find useful to have the drush alias for the tasks that involve local drush support
  env_file: env/.env.staging # More into this later

production.yourhostname:
  <<: *base
  stage: production
  # ... You've got the drill

As you see, nothing complicated going on here. First we create a base where we define basic SSH configuration and then we define the different environments.

So let's head on to the recipe:

# deploy.php

<?php
namespace Deployer;

require 'recipe/common.php';
require 'recipe/drupal8.php';
require 'recipe/rsync.php';

// Hosts
inventory('hosts.yml'); # Import our hosting definitions

// Project name
set('application', 'yourapplicationame');

// Keep 3 releases only
set('keep_releases', 3);

set('rsync_src', '../../web');
set('rsync_dest','{{release_path}}/web');

//Set drupal site. Change if you use different site
set('drupal_site', 'default');

//Drupal 8 shared dirs
set('shared_dirs', [
    'sites/{{drupal_site}}/files',
]);

//Drupal 8 shared files
set('shared_files', [
    'sites/{{drupal_site}}/settings.local.php',
    'sites/{{drupal_site}}/settings.php',
    'sites/{{drupal_site}}/services.yml',
]);

//Drupal 8 Writable dirs
set('writable_dirs', [
    'sites/{{drupal_site}}/files',
]);

set('rsync',[
    'exclude'       => ['/sites/default/settings.local.php', '/themes/your-theme/scss/*'],
    'exclude-file'  => false,
    'include'       => [],
    'include-file'  => false,
    'filter'        => [],
    'filter-file'  => false,
    'filter-perdir' => false,
    'flags'         => 'rzcEPu', // Recursive, with compress, check based on checksum rather than time/size, preserve Executable flag
    'options'       => [], //Delete after successful transfer, delete even if deleted dir is not empty. Use 'dry-run' to test.
    'timeout'       => 3600, //for those huge repos or crappy connection
]);

desc('Deploy your project');
task('deploy', [
    'deploy:info',
    'deploy:prepare',
    'deploy:lock',
    'deploy:release',
    'rsync:warmup',
    'rsync',
    'deploy:shared',
    'deploy:writable',
    'deploy:symlink',
    'deploy:unlock',
    'cleanup'
]);

// [Optional] If deploy fails automatically unlock.
after('deploy:failed', 'deploy:unlock');

If you get an error stating that Deployer is unable to find the composer installed recipes, simply add to deploy.php:

$composerHome = getenv("COMPOSER_HOME") ?: (getenv("HOME") . '/.composer/');
if (is_dir($composerHome)) {
    include $composerHome . '/vendor/autoload.php';
} else {
    $xdgComposerHome = getenv('XDG_CONFIG_HOME') ?: (getenv("HOME") . '/.config');
    $xdgComposerHome = $xdgComposerHome . '/composer';
    if (is_dir($xdgComposerHome)) {
        include $xdgComposerHome . '/vendor/autoload.php';
    }
}

Most of the recipe is based on the Drupal 8 recipe, the main difference is the introduction of the rsync recipe, that replaces the functionality of the default update_code task. The later task pulls a git repository from a defined git source and I want to avoid this, because I want to have more control over the uploaded files. This is especially important when the list of ignored files is different from the files we want to push to the server. Using the rsync recipe, you can define the source and destiny of the files to upload, and exclude/include the files you want within the rsync task.

This recipe should get you through simple Drupal deployments, but we can build upon it to add support to other tasks, so stay tuned to the following posts in this series.