About | Blog

Deploying a Symfony4 Application with Deployer

November 28, 2018

Lately, I've been looking for a way to replace my dirty bash scripts used to deploy this blog on my VPS. A few years ago, I gave a shot to Capistrano and Fabric. They are great tools but written in languages I don't master (that's especially true for Capistrano). I also remember that I had problems with them when upgrading the Ruby and Python packages of my machine. This time, I wanted to try something else, something that could respect my expectations:

At first glance Deployer seemed to answer my needs.

Deployer, a deployment tool for PHP

Apart from all the features I've already listed above, Deployer is easy to install (either by composer or with a phar package), highly configurable and very modular. It supports natively a lot of PHP frameworks and applications. A deployment is as easy as executing dep deploy and using the option -v lists all the underlying system commands that are actually launched. Last but no least, it's written in PHP. It may seem to be a detail, but it's very useful to dig into the code and to understand exactly what a recipe does.

In short, Deployer is simple, efficient and crystal clear.

Symfony works out of the box

Deployer ships with recipes for Symfony 2, 3 and 4. By default, a Symfony4 deployment performs the following actions on the server:

All those actions work out of the box as long as the recipe is correctly configured. Also, using an inventory file, allows to have a solid deployment script that can be versioned alongside the code.

That's great! But it's not yet what I need :)

Building a Symfony application locally

The first thing I needed was to create a custom task to build my project locally:

desc('Build project');
task(
    'build',
    function () {
        // build frontend
        run('sh bin/build-front');
        // build backend
        run('composer install --no-ansi --no-dev --no-interaction --no-progress --no-scripts --optimize-autoloader');
    }
)->local();

As I don't checkout the sources from the repository, I need to be able to upload them:

desc('Upload project');
task('upload', function () {
    upload(__DIR__ . '/*', '{{release_path}}', ['options' =>
        [
            "--exclude='.env'",
            "--exclude='.git/'",
            "--exclude='.gitignore'",
            "--exclude='.idea/'",
            "--exclude='.*.xml.dist'",
            "--exclude='.*.yml'",
            "--exclude='.*.yml.dist'",
            "--exclude='phpstan.neon'",
            "--exclude='semantic.json'",
            "--exclude='symfony.lock'",
            "--exclude='node_modules/'",
            "--exclude='semantic/'",
            "--exclude='tests/'",
            "--exclude='var/'",
        ]
    ]);
});

It's very important to use the variable {{release_path}} here (and not the variable {{deploy_path}}). For instance, if {{deploy_path}} relates to path/to/my/app/, then {{release_path}} would be path/to/my/app/releases/X/ (where X is the Xth deployment).

Then, the last thing to do is to replace the default Symfony4 deploy task by what I need:

- task('deploy', [
+ task('deploy', [ // override the default Symfony4 deploy task
+    'build',
     'deploy:info',
     'deploy:prepare',
     'deploy:lock',
     'deploy:release',
-    'deploy:update_code',
+    'upload',
     'deploy:shared',
-    'deploy:vendors',
-    'deploy:writable', // I don't need any writable file or directory
     'deploy:cache:clear',
     'deploy:cache:warmup',
     'deploy:symlink',
     'deploy:unlock',
     'cleanup',
 ]);

That's simple and works like a charm. The whole script is available here.

Next step will be to setup continuous delivery on Gitlab with Deployer. Maybe I'll have to change a few things on this deployment script.