Here's Deployer recipes to deploy Bedrock

I recently had to deploy Bedrock using Deployer. Therefore I wrote some recipes to handle deployment, push/pull of database and files, .env file generation, Sage assets, Trellis an more.

Maybe these are useful to someone else as well: https://github.com/FlorianMoser/bedrock-deployer

I even managed to deploy to a shared hosting (Plesk, chroot) by applying some modifications to the common Deployer recipes: https://github.com/FlorianMoser/plesk-deployer

Enjoy!

9 Likes

I don’t suppose you intend on having a version of this which doesn’t force users to be using Trellis? Currently its more of a Bedrock-Trellis recipe than just bedrock :smiley:

1 Like

There is only one (optional) recipe for Trellis: trellis:remove. And all it does is remove Trellis form the deploy path. This is useful if you have Trellis checked into your repo, but don’t use it for deployment.

The pull:db and push:db recipes currently require a running Vagrant server. But not necessarily Trellis.

But maybe I should try to rewrite these recipe to use normal SSH.

I have Deployer recipes for virgin Bedrock, if you’re interested. Usable with Valet, for example.

See my fork of the unmaintained deployer-wp-recipes. I can provide a sample deploy deployer.php and servers.yml if you’re interested.

@danielroe A sample deploy files would be awesome! I’m new to the Deployer world, but would rather go that route then capistrano again! Thanks

Here are a few examples:

Sample deploy.php
<?php

 // composer require --dev cstaelen/deployer-wp-recipes
 // composer require vlucas/phpdotenv

namespace Deployer;
require __DIR__ . '/vendor/autoload.php';

require 'recipe/common.php';
require 'vendor/cstaelen/deployer-wp-recipes/recipes/assets.php';
require 'vendor/cstaelen/deployer-wp-recipes/recipes/cleanup.php';
require 'vendor/cstaelen/deployer-wp-recipes/recipes/db.php';
require 'vendor/cstaelen/deployer-wp-recipes/recipes/uploads.php';
require 'vendor/vlucas/phpdotenv/src/Dotenv.php';

/**
 * Server Configuration
 */

// Define servers
inventory('servers.yml');

// Default server
set('default_stage', 'production');

// Temporary directory path
set('tmp_path', '/tmp/deployer');

set('wp-recipes', [
  'theme_name'        => 'my-theme',
  'theme_dir'         => 'web/app/themes/',
  'shared_dir'        => '{{deploy_path}}/shared',
  'assets_dist'       => 'web/app/themes/my-theme/',
  'local_wp_url'      => '',
  'remote_wp_url'     => '',
  'clean_after_deploy'=>  [
    'deploy.php',
    '.gitignore',
    '*.md'
  ]
]);

/**
 * Bedrock Configuration
 */

// Bedrock project repository
set('repository', 'git@github.com:me/my-theme.git');

// Bedrock shared files
set('shared_files', ['.env', 'web/.htaccess']);

// Bedrock shared directories
set('shared_dirs', ['web/app/uploads', 'web/app/w3tc-config']);

// Bedrock writable directories
set('writable_dirs', ['web/app/uploads']);

task('setup:vars', function () {
  set('http_user', $user);
  set('http_group', $user);
  set('tmp_path', '/tmp/deployer-' . get('theme_name'));
})->desc('Setup variables');

/**
 * Backup all shared files and directories
 */
task('setup:backup', function () {
    $currentPath = '{{deploy_path}}/current';
    $tmpPath = get('tmp_path');

    // Delete tmp dir if it exists.
    run("if [ -d $tmpPath ]; then rm -R $tmpPath; fi");

    // Create tmp dir.
    run("mkdir -p $tmpPath");

    foreach (get('shared_dirs') as $dir) {
        // Check if the shared dir exists.
        if (test("[ -d $(echo $currentPath/$dir) ]")) {
            // Create tmp shared dir.
            run("mkdir -p $tmpPath/$dir");

            // Copy shared dir to tmp shared dir.
            run("cp -rv $currentPath/$dir $tmpPath/" . dirname($dir));
        }
    }

    foreach (get('shared_files') as $file) {
        // If shared file exists, copy it to tmp dir.
        run("if [ -f $(echo $currentPath/$file) ]; then cp $currentPath/$file $tmpPath/$file; fi");
    }
})->desc('Backup all shared files and directories');


/**
 * Purge all files from the deploy path directory
 */
task('setup:purge', function () {
    // Delete everything in deploy dir.
    run('rm -R {{deploy_path}}/*');
})->desc('Purge all files from the deploy path directory');


/**
 * Restore backup of shared files and directories
 */
task('setup:restore', function() {
    $sharedPath = "{{deploy_path}}/shared";
    $tmpPath = get('tmp_path');

    foreach (get('shared_dirs') as $dir) {
        // Create shared dir if it does not exist.
        if (!test("[ -d $sharedPath/$dir ]")) {
            // Create shared dir if it does not exist.
            run("mkdir -p $sharedPath/$dir");
        }

        // If tmp shared dir exists, copy it to shared dir.
        run("if [ -d $(echo $tmpPath/$dir) ]; then cp -rv $tmpPath/$dir $sharedPath/" . dirname($dir) . "; fi");
    }

    foreach (get('shared_files') as $file) {
        // If tmp shared file exists, copy it to shared dir.
        run("if [ -f $(echo $tmpPath/$file) ]; then cp $tmpPath/$file $sharedPath/$file; fi");
    }
})->desc('Restore backup of shared files and directories');


/**
 * Configure known_hosts for git repository
 */
task('setup:known_hosts', function () {
    $repository = get('repository');
    $host = '';

    if (filter_var($repository, FILTER_VALIDATE_URL) !== FALSE) {
        $host = parse_url($repository, PHP_URL_HOST);
    // } elseif (preg_match('/^git@(?P<host>\w+?\.\w+?):/i', $repository, $matches)) {
    } elseif (preg_match('/^git@(.*):/i', $repository, $matches)) {
        $host = $matches[1];
    }

    if (empty($host)) {
        throw new \RuntimeException('Couldn\'t parse host from repository.');
    }

    run("ssh-keyscan -H -T 10 $host >> ~/.ssh/known_hosts");
})->desc('Configure known_hosts for git repository');

/**
 * Setup success message
 */
task('setup:success', function () {
    Deployer::setDefault('terminate_message', '<info>Successfully setup!</info>');
})->once()->setPrivate();


/**
 * Reload php-fpm service
 */
task('php-fpm:reload', function () {
   run('sudo /etc/init.d/php7.0-fpm reload');
})->desc('Reload php-fpm service');


/**
 * Reload varnish service
 */
task('varnish:reload', function () {
   run('sudo /etc/init.d/varnish reload');
})->desc('Reload varnish service');


/**
 * Deploy task
 */
task('deploy', [
    'deploy:prepare',
    'deploy:lock',
    'deploy:release',
    'deploy:update_code',
    'deploy:shared',
    // 'sage:install',
    'deploy:vendors',
    'deploy:assets',
    'deploy:writable',
    'deploy:symlink',
    'deploy:unlock',
    'cleanup',
    'varnish:reload',
    'php-fpm:reload',
])->desc('Deploy your Bedrock project');
after('deploy', 'success');


/**
 * Setup task
 */
task('setup', [
    'setup:backup',
    'setup:purge',
    'deploy:prepare',
    'setup:restore',
    'setup:known_hosts',
])->desc('Setup your Bedrock project');
after('setup', 'setup:success');

before('db:cmd:pull', 'env:uri');
before('db:cmd:push', 'env:uri');

after('deploy', 'deploy:cleanup');

before('deploy', 'setup:vars');
before('setup', 'setup:vars');
before('deploy:unlock', 'setup:vars');
before('uploads:push', 'setup:vars');
before('db:cmd:push', 'setup:vars');
before('db:cmd:pull', 'setup:vars');
Sample servers.yml
# servers.yml

.base: &base
  branch: master
  theme_name: my-theme
  deploy_path: '/var/www/{{ hostname }}'

my-client.staging.example.com:
  <<: *base
  user: staging
  stage: staging
  deploy_path: '/home/staging/web/{{ hostname }}'

my-client.org.uk:
  <<: *base
  user: my-theme
  stage: production
  deploy_path: '/home/my-client/web/{{ hostname }}'

As I mentioned, these are heavily based on the samples from the original deployer-wp-recipes and rely on my updated fork.

You can use my fork by running:

composer config repositories.deployer-wp-recipes vcs https://github.com/danielroe/deployer-wp-recipes && composer require --dev cstaelen/deployer-wp-recipes=dev-master

Awesome, thanks! This is very help to see as well. Learning how to use a fork in composer is helpful too!

Hello all - just trying this out but would like to confirm where you install (and run) this in the roots / sage / trellis / bedrock project [https://github.com/roots/roots-example-project.com]?

I want to deploy to a plesk server. D.

Hi Daniel. These are recipes for Deployer, so you must have Deployer installed first.

You will not be able to deploy Trellis to a Plesk environment, so you can only use Trellis for local development. And because you will want to deploy Bedrock, this is the package, where you install Deployer (in the same directory where you find the composer.json file for Bedrock, not Sage). In a setup like

/your-project/trellis
/your-project/site
/your-project/site/web/app/themes/your-theme

you would install Deployer in /your-project/site. This is also the place, where the recipes go. You may also be interested in my recipes to deploy Bedrock with additional examples.

1 Like

thanks for the response and info @flomo - will be testing this out soon. D.

Morning @flomo - so made some progress with this. Connection to the server works but then i get an error… Its a big ask but if you have a mo’ it’d very appreciated!

It has uploaded some files and created some directories. I’m uncertain about the paths used in the example. Namely deploy/ and current/. I’m presuming we can use the default public httpdocs?

In Client.php line 103:

  The command "cd httpdocs && (mkdir httpdocs/releases/1)" failed.

  Exit Code: 1 (General error)

  Host Name: xxx.xxx.xxx.xxx

  ================
  mkdir: cannot create directory `httpdocs/releases/1': No such file or directory

The domains’ public folder is httpdocs. Maybe there’s something wrong with the paths?

require 'recipe/common.php';
require 'vendor/florianmoser/plesk-deployer/recipe/chroot_fixes.php';

// plesk
// /var/www/vhosts/humanrelationsjournal.org/httpdocs/ (from test.php)
set('chroot_path_prefix', '/var/www/vhosts/humanrelationsjournal.org'); // checked & correct
set('chroot_index_file', 'httpdocs'); 

// host  ? IP works, user correct.  path - some files being uploaded [see grab]
host('xxx.xxx.xxx.xxx')
    ->user('xxxx')
    ->port('2020') 
    ->set('deploy_path', 'httpdocs');   // tried /httpdocs & get different error.

dep LOG

[94.229.171.80] > echo $0
[94.229.171.80] < bash
[94.229.171.80] > if [ ! -d httpdocs ]; then mkdir -p httpdocs; fi
[94.229.171.80] > if [ ! -L httpdocs/current ] && [ -d httpdocs/current ]; then echo 'true'; fi
[94.229.171.80] > cd httpdocs && if [ ! -d .dep ]; then mkdir .dep; fi
[94.229.171.80] > cd httpdocs && if [ ! -d releases ]; then mkdir releases; fi
[94.229.171.80] > cd httpdocs && if [ ! -d shared ]; then mkdir shared; fi
[94.229.171.80] > if [ -f httpdocs/.dep/deploy.lock ]; then echo 'true'; fi
[94.229.171.80] > touch httpdocs/.dep/deploy.lock
[94.229.171.80] > cd httpdocs && (if [ -h release ]; then echo 'true'; fi)
[94.229.171.80] > cd httpdocs && (if [ -d releases ] && [ "$(ls -A releases)" ]; then echo 'true'; fi)
[94.229.171.80] > cd httpdocs && (if [ -d httpdocs/releases/1 ]; then echo 'true'; fi)
[94.229.171.80] > cd httpdocs && (date +"%Y%m%d%H%M%S")
[94.229.171.80] < 20190703095704
[94.229.171.80] > cd httpdocs && (echo '20190703095704,1' >> .dep/releases)
[94.229.171.80] > cd httpdocs && (mkdir httpdocs/releases/1)
[94.229.171.80] < [error] mkdir: cannot create directory `httpdocs/releases/1': No such file or directory
[Deployer\Exception\RuntimeException] The command "cd httpdocs && (mkdir httpdocs/releases/1)" failed.

Exit Code: 1 (General error)

It creates some of the folders and files but then fails when it tries to create something in releases/

I’ve tried adding a slash before or after the deploy_path (/httpdocs, or httpdocs/). But then i get a different error.

  The command "if [ ! -d /httpdocs ]; then mkdir -p /httpdocs; fi" failed.

Any pointers appreciated!

I see two problems with your deploy settings: The chroot_index_file should point to a file and the deploy path should be prefixed with a slash. Try these changes:

set('chroot_index_file', 'web/index.php'); // The PHP index-file for Bedrock is inside the /web dir
host('xxx.xxx.xxx.xxx')
    ->user('xxxx')
    ->port('2020') 
    ->set('deploy_path', '/httpdocs');

I also suggest you delete the .dep, releases and shared dirs in /httpdocs before trying to deploy again.

1 Like

Thanks! Got this going but actually had to use ~/

set('deploy_path', '~/httpdocs/deploy'); 

Now messing up the ‘current’ symlink or there’s something wrong with that area.

I didn’t realise that the files are pulled from git? Or are they? Seems like everything is uploaded including .git/ and trellis/. I had to add ssh keys as its a private repo to get deployer to complete.

Generally i’m starting to see how this could work but obv still a few issues with symlinks and/or path — thanks for the help.

  • Do i need to run composer in site/ on the server manually or upload the plugins via deployer. Perhaps it would run if the symlink was correct?
  • do i set the server DB details in .env ? Or create one called .env.production. Not sure how the server would know its a production server
  • Git is told to ignore the production files so no locally compiled files like js or css uploaded. How would this work in this deployment method?
  • can or should deployer upload / download plugins and wordpress uploads. Or perhaps composers would sort some of this.

Symlink

domain.abc/releases/4/site/web/test.php works but not domain.abc/current

I’ve looked at the symlink and its path is wrong… Its adding two full paths instead of chroot_path_prefix + deploy_path

find . -type l -ls
20211665    0 lrwxrwxrwx   1 hrwebsam psacln        109 Jul  4 12:43 ./current -> /var/www/vhosts/xxx.abc/var/www/vhosts/xxx.abc/httpdocs/deploy/releases/6

If you have any hits regarding the symlink much appreciated!

Strange. Isn’t / and ~/ the same in your chrooted environment?

That’s correct. You won’t want to upload your potentially modified local development files to a production or staging server. For a reproducable result, you deploy a certain branch of your repo.

You should run Composer automatically during each deployment process.

Yes, you need to create a .env file (see the Bedrock docs for that). You can also check out my Bedrock Deployer recipes, which automatically create a .env file on initial deployment and copy that file on subsequent deployments.

That is where you would ideally use a build/CI service. Which may be an overkill for small projects. That’s why my Bedrock Deployer recipes build the Sage dist files on the local machine and uploads them to the server for each deployment.

Ideally all your plugins are installed through Composer. You’ll find most WordPress plugins on Wpackagist as Composer packages. You may also store commercial plugins in your repo or in a private package. This has been discussed in several threads on this site.

I can’t help you remotely concerning your Symlink problem. I suggest you look at the code of the symlink task in my recipe and try to understand what happens. It may also help to run some single tasks instead of running the whole deploy recipe. Also replacing the run() function with a writeln() function in some tasks will help you to understand what happens (or what doesn’t happen). Also consider using the verbose flag when running dep gives you further insights.

1 Like

@flomo amazing thanks. I’ve actually removed the chroot scripts and the symlink is correct. Now server problems in terms of seeing a Wordpress site but i’ll get there eventually.

Just to clarify on thing - after running dep deploy locally composer should be ran on the server during that process?

Many thanks for your patience and guidance!

Glad to help.

Make sure, you set the website root to the symlink your/path/current/web in Plesk (= /web of your most recent release).

It should, but it doesn’t do that automatically. You will have to write a task for that (or use my bedrock:vendors task). You will also have to repeat this step to run Composer in your Sage theme.

Good luck!

Another one simple config for Deployer and Bedrock: https://github.com/zorca/wp-deploy