Deploying your Laravel application with Deployer

Published by at 22nd January 2018 12:00 pm

Deployment processes have a nasty tendency to be a mish-mash of cobbled-together scripts or utilities in many web shops, with little or no consistency in practice between them. As a result, it's all too easy for even the most experienced developer to mess up a deployment.

I personally have used all kinds of bodged-together solutions. For a while I used Envoy scripts to deploy my Laravel apps, but then there was an issue with the SSH library in PHP 7 that made it impractical to use it. Then I adopted Fabric, which I'd used before for deploying Django apps and will do fine for deploying PHP apps too, but it wasn't much more sophisticated than using shell scripts for deployment purposes. There are third-party services like Deploybot, but these are normally quite expensive for what they are.

A while back I heard of Deployer, but I didn't have the opportunity to try it until recently on a personal project as I was working somewhere that had its own in-house deployment process. It's a PHP-specific deployment tool with recipes for deploying applications built with various frameworks and CMS's, including Laravel, Symfony, CodeIgniter and Drupal.

Installing Deployer

Deployer is installed as a .phar file, much like you would with Composer:

1$ curl -LO https://deployer.org/deployer.phar
2$ mv deployer.phar /usr/local/bin/dep
3$ chmod +x /usr/local/bin/dep

With that done, you should be able to run the following command in your project's directory to create a Deployer script:

$ dep init

In response, you should see a list of project types:

1 Welcome to the Deployer config generator
2
3
4
5 This utility will walk you through creating a deploy.php file.
6 It only covers the most common items, and tries to guess sensible defaults.
7
8 Press ^C at any time to quit.
9
10 Please select your project type [Common]:
11 [0] Common
12 [1] Laravel
13 [2] Symfony
14 [3] Yii
15 [4] Yii2 Basic App
16 [5] Yii2 Advanced App
17 [6] Zend Framework
18 [7] CakePHP
19 [8] CodeIgniter
20 [9] Drupal
21 >

Here I chose Laravel as I was deploying a Laravel project. I was then prompted for the repository URL - this will be filled in with the origin remote if the current folder is already a Git repository:

1Repository [git@gitlab.com:Group/Project.git]:
2 >

You'll also see a message about contributing anonymous usage data. After answering this, the file deploy.php will be generated:

1<?php
2namespace Deployer;
3
4require 'recipe/laravel.php';
5
6// Configuration
7
8set('repository', 'git@gitlab.com:Group/Project.git');
9set('git_tty', true); // [Optional] Allocate tty for git on first deployment
10add('shared_files', []);
11add('shared_dirs', []);
12add('writable_dirs', []);
13
14
15// Hosts
16
17host('project.com')
18 ->stage('production')
19 ->set('deploy_path', '/var/www/project.com');
20
21host('beta.project.com')
22 ->stage('beta')
23 ->set('deploy_path', '/var/www/project.com');
24
25
26// Tasks
27
28desc('Restart PHP-FPM service');
29task('php-fpm:restart', function () {
30 // The user must have rights for restart service
31 // /etc/sudoers: username ALL=NOPASSWD:/bin/systemctl restart php-fpm.service
32 run('sudo systemctl restart php-fpm.service');
33});
34after('deploy:symlink', 'php-fpm:restart');
35
36// [Optional] if deploy fails automatically unlock.
37after('deploy:failed', 'deploy:unlock');
38
39// Migrate database before symlink new release.
40
41before('deploy:symlink', 'artisan:migrate');

By default it has two hosts, beta and production, and you can refer to them by these names. You can also add or remove hosts, and amend the existing ones. Note the deploy path as well - this sets the place where the application gets deployed to.

Note that it's set up to expect the server to be using PHP-FPM and Nginx by default, so if you're using Apache you may need to amend the command to restart the server. Also, note that if like me you're using PHP 7 on a distro like Debian that also has PHP 5 around, you'll probably need to change the references to php-fpm as follows:

1desc('Restart PHP-FPM service');
2task('php-fpm:restart', function () {
3 // The user must have rights for restart service
4 // /etc/sudoers: username ALL=NOPASSWD:/bin/systemctl restart php-fpm.service
5 run('sudo systemctl restart php7.0-fpm.service');
6});
7after('deploy:symlink', 'php-fpm:restart');

You will also need to make sure the acl package is installed - on Debian and Ubuntu you can install it as follows:

$ sudo apt-get install acl

Now, the recipe for deploying a Laravel app will include the following:

  • Pulling from the Git remote
  • Updating any Composer dependencies to match composer.json
  • Running the migrations
  • Optimising the application

In addition, one really great feature Deployer offers is rollbacks. Rather than checking out your application directly into the project root you specify, it numbers each release and deploys it in a separate folder, before symlinking that folder to the project root as current. That way, if a release cannot be deployed successfully, rather than leaving your application in an unfinished state, Deployer will symlink the previous version so that you still have a working version of your application.

If you have configured Deployer for that project, you can deploy using the following command where production is the name of the host you're deploying to:

$ dep deploy production

The output will look something like this:

1✔ Executing task deploy:prepare
2✔ Executing task deploy:lock
3✔ Executing task deploy:release
4➤ Executing task deploy:update_code
5Counting objects: 761, done.
6Compressing objects: 100% (313/313), done.
7Writing objects: 100% (761/761), done.
8Total 761 (delta 384), reused 757 (delta 380)
9Connection to linklater.shellshocked.info closed.
10✔ Ok
11✔ Executing task deploy:shared
12✔ Executing task deploy:vendors
13✔ Executing task deploy:writable
14✔ Executing task artisan:storage:link
15✔ Executing task artisan:view:clear
16✔ Executing task artisan:cache:clear
17✔ Executing task artisan:config:cache
18✔ Executing task artisan:optimize
19✔ Executing task artisan:migrate
20✔ Executing task deploy:symlink
21✔ Executing task php-fpm:restart
22✔ Executing task deploy:unlock
23✔ Executing task cleanup
24✔ Executing task success
25Successfully deployed!

As you can see, we first of all lock the application and pull the latest version from the Git remote. Next we copy the files shared between releases (eg the .env file, the storage/ directory etc), update the dependencies, and make sure the permissions are correct. Next we link the storage, clear all the cached content, optimise our app, and migrate the database, before we set up the symlink. Finally we restart the web server and unlock the application.

In the event you discover a problem after deploy and need to rollback manually, you can do so with the following command:

$ dep rollback production

That makes it easy to ensure that in the event of something going wrong, you can quickly switch back to an earlier version with zero downtime.

Deployer has made deployments a lot less painful for me than any other solution I've tried. The support for rollbacks means that if something goes wrong it's trivial to switch back to an earlier revision.