PHP
Article
By Younes Rafie

Easy Deployment of PHP Applications with Deployer

By Younes Rafie

This article has updated for the most recent version of Deployer on March 26th, 2017.


Everybody tries to automate their development process, testing, code formatting, system checks, etc. This is also the case for deploying our applications or pushing a new version to the production server. Some of us do this manually by uploading the code using an FTP client, others prefer Phing, and Laravel users will prefer Envoyer for this process. In this article, I’m going to introduce you to Deployer – a deployment tool for PHP.

Deployer LOGO

Demo Application

I will be using an application from a previous article for the demo. The application will be deployed to a DigitalOcean droplet. To follow along, you can clone the demo application’s source code from GitHub.

Installation

Deployer is packaged as a PHAR file that we can download to our local machine. We can also move it to the user’s bin directory to make it global. Check the documentation for more details.

mv deployer.phar /usr/local/bin/dep
chmod +x /usr/local/bin/dep

Defining Servers

After cloning the demo repository, we need to create a new file called deploy.php where we’ll define our deployment steps.

The first step is to define our deployment servers. We can authenticate normally using a username and a password.

// deploy.php

server('digitalocean', '104.131.27.106')
    ->user($_ENV['staging_server_user'])
    ->password($_ENV['staging_server_password']);

We can also define the type of this server (staging, production, etc), which allows us to run tasks (more on tasks later) in a specific server stage.

// deploy.php
use function Deployer\set;
use function Deployer\server;

set('default_stage', 'staging');

server('digitalocean', '104.131.27.106')
    ->user($_ENV['staging_server_user'])
    ->password($_ENV['staging_server_password'])
    ->stage('staging')
    ->env('deploy_path', '/var/www');

We need to add the default_stage attribute when using the stage method. Otherwise, we’ll get the You need to specify at least one server or stage. error.

Note: If you’re using PHP 7, you can group use statements in one line like this (use function Deployer\{set, server}). You can read more about new PHP 7 features here.

SSH Authentication

It’s a common practice to use SSH authentication on production servers. If you’re not familiar with using SSH keys for authentication, check out this guide for a detailed walkthrough.

// deploy.php
use function Deployer\{set, server};

set('default_stage', 'staging');

server('digitalocean', '104.131.27.106')
    ->identityFile()
    ->user($_ENV['staging_server_user'])
    ->password($_ENV['staging_server_password'])
    ->stage('staging');

By default, the identityFile method uses the current user’s ~/.ssh/id_rsa identity file. We can change that if needed.

// deploy.php

// ...
    ->identityFile('path/to/id_rsa', 'path/to/id_rsa.pub', 'pass phrase')
// ...

Deployer supports multiple types of SSH connections, and it defaults to native which uses native system commands to authenticate.

Install SSH2 Extension

Deployer also supports using the PHP SSH2 extension. After installing the extension and enabling it, we need to require the herzult/php-ssh package which provides an OOP wrapper for the PHP extension, and sets the ssh_type config option to ext-ssh2.

// deploy.php

set('ssh_type', 'ext-ssh2');
// ...

The only problem here is that Deployer doesn’t have the php-ssh package shipped with the PHAR archive. We’ll need to clone the Deployer repository, require the package, and finally use the build script to generate a new PHAR archive.

Using Configuration Files

We can also define our servers using a YAML configuration file, and pass the file to the serverList method. You can read more about servers in the documentation.

// servers.yml

digitalocean:
  host: 104.131.27.106
  user: root
  identity_file: ~
  stage: staging
  deploy_path: /var/www/

We can now load the file inside our deploy.php file.

serverList('servers.yml');

Defining Tasks

Tasks are commands that can be run through the dep command.

dep deploy:staging

The deploy:staging argument should be a task inside our deploy.php file. We can upload files, run commands on the server, etc.

// deploy.php

use function Deployer\{server, task, run, set, get, add, before, after, upload};

task('deploy:staging', function() {
    writeln('<info>Deploying...</info>');
    $appFiles = [
        'app',
        'bootstrap',
        'public',
        'composer.json',
        'composer.lock',
        'artisan',
        '.env',
    ];
    $deployPath = get('deploy_path');

    foreach ($appFiles as $file)
    {
        upload($file, "{$deployPath}/{$file}");
    }

    cd($deployPath);
    run("composer update --no-dev --prefer-dist --optimize-autoloader");

    run("chown -R www-data:www-data app/storage");
    set('writable_dirs', ['app/storage']);

    writeln('<info>Deployment is done.</info>');
});

The file looks overwhelming at first, but there’s nothing special here. First, we print a message using the writeln method to notify the user that the deployment process has started. Next, we define the application folders that need to be uploaded.

The upload method will send our local application files to the server inside the for loop. Check the documentation for the list of available functions.

We cd to the server directory and update our Composer dependencies using the run method, which lets us run any shell command on the server.

The next and final step is to make the necessary directories writable by the server user, in this case www-data for Apache. The writable_dirs parameter tells Deployer about our writable directories. We can achieve the same thing using a shell command, too.

dep deploy:staging

Deploy 1

task("deploy:staging", function () {
    // ... 
})->desc('Deploy application to staging.');

The desc method will add a help message to our task (command).

Our current file has one big task to handle the whole deployment process. We’ll split it into small reusable tasks and run them one after the other in the deploy:staging task.

task('deploy:started', function() {
    writeln('<info>Deploying...</info>');
});

task('deploy:done', function() {
    writeln('<info>Deployment is done.</info>');
});

The above tasks are just to notify the user of the deployment state. Deployer provides after and before methods (hooks) to run tasks when other tasks are fired.

before('deploy:staging', 'deploy:started');
after('deploy:staging', 'deploy:done');
task('deploy:upload', function() {
    $appFiles = [
        'app',
        'bootstrap',
        'public',
        'composer.json',
        'composer.lock',
        'artisan',
        '.env',
    ];
    $deployPath = get('deploy_path');

    foreach ($appFiles as $file)
    {
        upload($file, "{$deployPath}/{$file}");
    }
});

task('deploy:writable_dirs', function() {
    $deployPath = get('deploy_path');
    cd($deployPath);

    run("chown -R www-data:www-data app/storage");
    set('writable_dirs', ['app/storage']);
});

task('deploy:composer', function() {
    $deployPath = get('deploy_path');
    cd($deployPath);

    run("composer update --no-dev --prefer-dist --optimize-autoloader");
});

All that’s left is to run all the tasks in the deploy:staging task.

task('deploy:staging', [
    'deploy:upload', 
    'deploy:writable_dirs',
    'deploy:composer',
]);

Because we split our tasks into meaningful parts, we can re-use them later for another production server, and even for other projects!

--ADVERTISEMENT--

Zero Downtime Deployment

Currently, our Apache server root is pointing to the /var/www/public directory. When we’re deploying a new version of our application, we need to put the server in maintenance mode for a few minutes to avoid any downtime for users.

A simple solution for this problem is to create a list of releases, and point our server’s root to a current directory, which will link to the latest release.

/current (link pointing to the current release)
/releases
    /realease_1
    /realease_2
    /realease_3

Common Deployment Tasks

Deployer has a list of common tasks used by most PHP apps, something like we did earlier. We’ll re-factor our deploy.php to re-use those common tasks where possible.

// deploy.php

require_once "recipe/common.php";

set('ssh_type', 'ext-ssh2');
set('default_stage', 'staging');
set('deploy_path', '/var/www');
set('copy_dirs', [
    'app/commands',
    'app/config',
    'app/controllers',
    'app/database',
    'app/lang',
    'app/models',
    'app/src',
    'app/start',
    'app/tests',
    'app/views',
    'app/filters.php',
    'app/routes.php',
    'bootstrap',
    'public',
    'composer.json',
    'composer.lock',
    'artisan',
    '.env',
]);
set('shared_dirs', [
    'app/storage/cache',
    'app/storage/logs',
    'app/storage/meta',
    'app/storage/sessions',
    'app/storage/views',
]);
set('writable_dirs', get('shared_dirs'));
set('http_user', 'www-data');

First we set the some variables that we’ll be using inside our tasks. shared_dirs, writable_dirs and http_user are all used by the common tasks..

task('deploy:upload', function() {
    $files = get('copy_dirs');
    $releasePath = get('release_path');

    foreach ($files as $file)
    {
        upload($file, "{$releasePath}/{$file}");
    }
});

We kept the deploy:upload task in this case, but you can also use the deploy:update_code task to pull your application from remote Git hosts. Don’t forget to set the necessary attributes when using it.

set('repository', 'http://github.com/whyounes/500pxAPI_Test.git');
set('branch', 'master');
// deploy.php

task('deploy:staging', [
    'deploy:prepare',
    'deploy:release',
    'deploy:upload',
    'deploy:shared',
    'deploy:writable',
    'deploy:symlink',
    'deploy:vendors',
    'current',
])->desc('Deploy application to staging.');

after('deploy:staging', 'success');
  • deploy:prepare: Test the connection, shared folder, releases folder, etc.
  • deploy:release: Create the release directory.
  • deploy:upload: Our task for uploading files.
  • deploy:shared: Create shared folders if they do not exist. Uses the shared_dirs attributes we set earlier.
  • deploy:writable: Set writable directories.
  • deploy:symlink: Create a symbolic link from the release to current.
  • deploy:vendors: Run Composer installation.
  • current: Print the current release name.
  • success: Print a success message.

Deploy 2

Deploy dirs

Deployment Recipes

Because most of us are using frameworks for our projects, Deployer has some pre-configured recipes for well-known frameworks like Laravel, Symfony, Yii, Zend, etc. Check the recipes section in the documentation for more details.

Conclusion

Deployer has a really nice API to make the deployment process easy and configurable. If you’ve never used a deployment tool to automate your deployment process, we really encourage you to try it!

If you’ve used Deployer before we would love to hear what you think! If you have any questions, you can post them below!

  • adrianmak

    From the guide above, it demostrated deploy php files only.
    For a php application, a database connection is always present.
    How to deploy local mysql database to staging server?

    • I’d like to know the database deployment piece as well.

      • younesrafie

        Deployer has a list of predefined recipes for some well-known frameworks, but you’ll need to create you own if you have a custom migration setup

  • Sam Mousa

    Isn’t it bad practice to run composer update on the production server?
    I think there are several alternatives:
    1. Commit composer.lock and run composer install instead.
    2. Since we can upload any set of files you could even just upload the vendor directory via the upload command instead of running composer remotely.

    • Anton

      1. Default common recipe uses `composer install`.
      2. It’s better to use common recipe instead what was described in article.

    • younesrafie

      Commiting your `composer.lock` file and running `composer install` is better. However, some applications have a custom commands for cleaning the `vendors` folder and pushing it to the server!

  • Does this allow atomic deployments?

    • younesrafie

      Yes, it does

      • Maria Duckworth

        Get 7500 to 14000 dollars a month online.Even newbies can have more than dollars 58 h..jz All you just Need an Internet Connection and a Computer To Make Some Extra cash. Why not try this .

        http://www.coronaville.ME.LY
        jhj…

  • Lars Lernestål

    A question: What are the advantages of using this tool as opposed to using a more complete automation-tool? Like Ansible for example.

    • Anton

      Simplicity.

  • Michal Kleiner

    The way how symlinks are created using the recipe is not atomic (in the true meaning). Problem with Apache and high load websites is that there’s always couple requests either lost or dispatched from the old version of the code. Solution?
    – use true atomic way of creating symlinks (ln -s release14 current_tmp && mv -Tf current_tmp current)
    – use Nginx
    With that, not a single request is lost or dispatched using old code.

    • Anton

      Deployer 4 now have true atomic symlinks with mv -Tf there it is possible.

  • Youssef

    must I have SSH access to use deployer?, cos for my current hosting web application I don’t have it.

    • younesrafie

      No, you can also use FTP, check the documentation

      • Youssef

        ok then, no way because FTP cause me alot of downtime :( .

  • Chistelbrown

    Nice tut, my project is built with codeigniter, and hosted on a shared server, does the same rules apply?

Recommended
Sponsors
Get the latest in PHP, once a week, for free.