There used to be a time when PHP developers had to use deployment tools that were aimed at general web applications. You can see this in Johannes Schickling’s tutorial on deploying Laravel applications with Capistrano, for example. It’s a Ruby tool, and you need to write Ruby code. These tools get the job done but have limited integration with PHP applications. This can result in hacky solutions for certain scenarios.
But nowadays, we’re blessed with a few deployment tools written in our language that enable deeper integration. One of these tools is Rocketeer, a tool that takes inspiration from Capistrano and the Laravel framework.
Rocketeer is a modern tool that brings a great approach for your deployment needs. That is to run tasks and manage your application across different environments and servers. On top of that, it also has some magic in it, like installing Composer dependencies if it detects a composer.json
file. You get sane defaults and automation of common tasks for a modern PHP application. And you have the ability to customise and extend everything.
You could describe it as an SSH task runner that runs client-side. The commands execute on servers through an SSH connection. If you use a shared hosting provider with only FTP access, you’re out of luck, unfortunately. What you also need is a remote repository where the tool can fetch your code from. There is support for Git and SVN by default. Need support for another version control system? Write your own implementation with the provided interfaces.
Installation
You can install Rocketeer in two different ways. Either you download the phar file and make it executable or you install it through Composer. I’m a supporter of the latter. Having it as a dependency allows for easy installation when cloning the repository. This can benefit anyone cloning the repository to get them up and running.
Installing with Composer:
$ composer require anahkiasen/rocketeer --dev
I don’t recommend that you install it globally. Keeping it on the repository level will ensure that each person deploying is running the same version. What I do recommend is that you add vendor/bin
to your PATH. Then you can use the binary by typing rocketeer
in your project root.
Ignition
Let’s get started! First you bootstrap the directories and files for configuration. You do this by running rocketeer ignite
in the root of your project.
When your application ignites, the tool creates a .rocketeer
folder in your project. The contents of the directory will look like this:
| .rocketeer | -- config.php | -- hooks.php | -- paths.php | -- remote.php | -- scm.php | -- stages.php | -- strategies.php
These are all the configuration files you need to start setting up your deployments. Whenever I refer to a configuration file from here on out, it exists in the .rocketeer/
directory.
Remote Folder Structure
It’s important to understand how Rocketeer manages its folder structure on the server side, since it’s a bit different to a regular setup. It uses a few directories for managing certain aspects of the deployment, so it can be effective at what it does. You specify a path to where you want to deploy your application on your server, and the tool will take care of the rest. That folder will look like this, if you have /var/www/app
as your application directory.
| /var/www/app | | -- current => /var/www/app/releases/20160103183754 | | -- releases | | | -- 20160101161243 | | | |-- uploads => /var/www/app/shared/uploads | | | -- 20160103183754 | | | |-- uploads => /var/www/app/shared/uploads | | -- shared | | | -- uploads
The most important folder is current
, which points to your latest release. That is where your web server’s document root should be set to. So what happens when you deploy?
- The tool creates a time-stamped folder in the
releases
directory. - Completes all tasks to make a ready release.
- Updates the symbolic link
current
to the new release.
This process makes your deployment transparent to the user. The switch between releases is almost instant, usually referred to as atomic deployments.
Some data should be persistent between your deployments. This can be file uploads, user sessions and logs, for example. Those files or folders go into the shared
directory. The tool creates symbolic links inside each release for the ones you’ve configured.
Events
Events drive the tool, and all strategies and tasks fire a before and after event when they run. They also provide a special halt event when a task fails. This could be for example dependencies.halt
, or deploy.halt
for general failure. This allows us to hook into the process where we need to.
The default events that happen during a deployment are:
-
deploy.before
: before anything happens. -
create-release.before
: before it creates a new release directory. -
create-release.after
: after creating a new release directory. -
dependencies.before
: before installing or updating dependencies. -
dependencies.after
: after installing or updating dependencies. Perhaps make sure that binaries in your vendor folder are executable. -
test.before
: before running tests. -
test.after
: after running tests. -
migrate.before
: before running database migrations. Maybe you want to do a backup of your database? -
migrate.after
: after running database migrations. -
deploy.before-symlink
: before symlinking the release as our current release. -
deploy.after
: completed. You could notify people that everything was smooth sailing or otherwise.
We can also create our own events that we can fire and listen to. For now, we’ll stick with these events provided for us. They will be enough for us right now.
Tasks
At the heart of Rocketeer, we find a concept called tasks. Most of what is happening under the hood are core tasks. The definition of a task could be a set of instructions to perform as a step in a deployment. If we look at some of the classes that the tool provides, we can get a general feel of what tasks are: classes such as Deploy
, Setup
, Migrate
, Rollback
, and Dependencies
. When you deploy, the deploy command itself is a task with sub-tasks.
Types of Tasks
This is where you’ll start to see how integrated the tool is with PHP, since you’ll write tasks in the language. You can create your own tasks in three different ways:
Arbitrary terminal commands. These are one-liners that you want to run on your server. Can be useful for a lot of things, like running gulp build ---production
for example.
Closures. If you need a bit more flexibility or complexity, you can write a task as a closure (anonymous function). Say you’d like to generate documentation for an API during your deployment.
function($task) { return $task->runForCurrentRelease('apigen generate source src destination api'); }
Classes. For more complex tasks, you should utilise the option to create classes for tasks. You create a class and extend Rocketeer\Abstracts\AbstractTask
. Then you must provide at least a description and an execute()
method. Here’s a completely useless example just to show the structure of a task class:
namespace MyDeployableApp\Deploy\Tasks; class HelloWorld extends \Rocketeer\Abstracts\AbstractTask { /** * Description of the Task * * @var string */ protected $description = 'Says hello to the world'; /** * Executes the Task * * @return void */ public function execute() { $this->explainer->line('Hello world!'); return true; } }
Note that you have to register task classes yourself. Either you do this through the hooks.php
file and add it to the custom
array…
'custom' => array('MyDeployableApp\Deploy\Tasks\HelloWorld',),
… or you can do this through the facade:
Rocketeer::add('MyDeployableApp\Deploy\Tasks\HelloWorld');
Once you register it, you can execute it:
$ rocketeer hello:world staging/0 | HelloWorld (Says hello to the world) staging/0 |=> Hello world! Execution time: 0.004s
Defining Tasks
We discussed events first because we hook tasks in where we need them in the process. You can do this in a few ways. Go with the one you like and that meets requirements for your level of complexity.
The easiest way of defining your tasks is in the hooks.php
file. It provides two arrays for this, specifying task execution before or after certain events.
'before' => [ 'setup' => [], 'deploy' => ['hello:world'], 'cleanup' => [], ],
Strategies
You might be able to tell already that the tasks provided are quite generic. Take Dependencies
, for example. What kind of dependencies are we talking about and which package manager?
This is where strategies come into play. A strategy is a specific implementation of a task, such as running tests with Behat or using Gulp for building your front-end. Tasks have a default strategy with the option of running the other strategies through the CLI. We can list the strategies available like this:
$ rocketeer strategies +--------------+----------------+-----------------------------------------------------------------------+ | Strategy | Implementation | Description | +--------------+----------------+-----------------------------------------------------------------------+ | check | Php | Checks if the server is ready to receive a PHP application | | check | Ruby | Checks if the server is ready to receive a Ruby application | | check | Node | Checks if the server is ready to receive a Node application | | deploy | Clone | Clones a fresh instance of the repository by SCM | | deploy | Copy | Copies the previously cloned instance of the repository and update it | | deploy | Sync | Uses rsync to create or update a release from the local files | | test | Phpunit | Run the tests with PHPUnit | | migrate | Artisan | Migrates your database with Laravel's Artisan CLI | | dependencies | Composer | Installs dependencies with Composer | | dependencies | Bundler | Installs dependencies with Bundler | | dependencies | Npm | Installs dependencies with NPM | | dependencies | Bower | Installs dependencies with Bower | | dependencies | Polyglot | Runs all of the above package managers if necessary | +--------------+----------------+-----------------------------------------------------------------------+
Creating Your Own Strategies
Say you’re doing BDD With Behat for your application instead of TDD. Then you want to run your tests with Behat instead of PHPUnit. Since it is a test runner, there is already a strategy namespace for that, but no implementation. Create the directory .rocketeer/strategies/
and place your new BehatStrategy.php
in there.
namespace MyDeployableApp\Deploy\Strategies; use Rocketeer\Abstracts\Strategies\AbstractStrategy;use Rocketeer\Interfaces\Strategies\TestStrategyInterface; class BehatStrategy extends AbstractStrategy implements TestStrategyInterface { public function test() { return $this->binary('vendor/bin/behat')->runForCurrentRelease(); } }
You can now switch out your test strategy to the new implementation in strategies.php
.
'test' => 'Behat',
Connections & Stages
It doesn’t matter if you have an infrastructure in place or have one in mind. It doesn’t matter if your application deploys to many environments across many servers. Rocketeer will be there for you. You can even have many varying locations on the same server. This is where the terms connections and stages enter.
A connection is a server where you deploy your application to. This is often called an environment, and production and staging are examples of this. Configuring these connections is a breeze in the tool. Either you do it through a nested array or by keeping separate files for each connection. Each connection can also have multiple servers in it.
Stages are like connections inside connections, a kind of “connectionception”. You could set up a staging and a production environment on a single server with the use of stages. So instead of having two separate connections, you have one connection with two stages in it.
Plugins
A great feature is that we can extend our process using plugins. There are a few official ones for integration with Laravel, Slack, HipChat and Campfire. Then there are a few, but not that many, on Packagist. Installing plugins is an easy task through the CLI:
$ rocketeer plugin:install rocketeers/rocketeer-slack
Even though there’s a limited number of plugins, it leaves room for developing plugins in the future. It tells of a good philosophy. And why not develop one of your own?
Setting Up Your Deployment
To get your application off of the ground, you need some basic configuration. You need to tell Rocketeer where to find your application and where it should deploy it to. Let’s start by setting an application name and configuring a production server in config.php
.
'application_name' => 'my-deployable-app', // [...] 'connections' => [ 'staging' => [ 'host' => 'staging.my-deployable-app.com', 'username' => '', 'password' => '', 'key' => '/Users/niklas/.ssh/id_rsa', 'keyphrase' => '', 'agent' => '', 'db_role' => true, ], 'production' => [ 'host' => 'www.my-deployable-app.com', 'username' => '', 'password' => '', 'key' => '/Users/niklas/.ssh/id_rsa', 'keyphrase' => '', 'agent' => '', 'db_role' => true, ], ],
You now have an application name and a server to deploy your application to. This setup uses SSH key authentication, but you can connect with a username and password also. To get prompted for username and password, set 'key' => ''
. The tool will store the credentials on your local machine and use them each time later on. I don’t recommend setting a username and a password in the config file, because you never want credentials committed to your repository.
What you should now do is change the default connection that you deploy to. Having the default set to production is not ideal. You don’t want to deploy to production by accident. So in the same file, look for the default
key and change the value to staging
instead.
'default' => ['staging'],
The application name itself isn’t that important. But if you don’t specify a folder to deploy to, it will use this as the folder name in the root directory. By default, the root is set to /home/www
. With this application name, it will deploy it to /home/www/my-deployable-app
. If you want to change your root directory, you can change this in remote.php
.
// Deploys to /var/www/my-deployable-app/ 'root_directory' => '/var/www/',
In the same file, you have the ability to override the application name and specify a directory for your application.
// Deploys to /var/www/tutsplus-tutorial 'app_directory' => 'tutsplus-tutorial',
Now you have a receiving end of the deployment, but you also need to set up the location of your code so it can be fetched. You do this by setting up your remote repository in scm.php
. By default it uses Git, but it has support for SVN also. You tell it the address of our repository and supply credentials if needed. I suggest you use SSH key authentication here as well, and leave the username and password empty.
'repository' => '[email protected]:owner/name.git', 'username' => '', 'password' => '', 'branch' => 'master',
Since you added a connection to the production server, you want to deploy the master branch.
Connection & Stages Specific Configuration
In most cases, you don’t want the same configuration option for all your connections or stages. Say, for example, you want to deploy another branch to the staging environment. Rocketeer allows you to override configuration values for connections and stages using config.php
. To deploy a branch called staging on your staging connection, you do this:
'on' => [ 'connections' => [ 'staging' => [ 'scm' => [ 'branch' => 'staging', ] ] ], ],
It uses a nested array to override configuration values. Under the staging
key, find the corresponding key in the file you want to change. In this case it’s branch
in scm.php
.
Deploy, Lift Off!
Now you have everything set up to make a successful deployment. You haven’t met your requirements for a complete deployment, but it’s enough to get your application cloned to your server and served to the end users. First you can execute the check strategy to see if your server meets the requirements.
$ rocketeer check
If everything is okay, you can deploy by running:
$ rocketeer deploy
Since this was your first deployment, Rocketeer will make sure everything is up to par. The tool creates the directories it needs and that our application will live in. If everything is smooth sailing, you should have a complete build of your application on the server.
If you changed the default connection to staging in the previous section, it will always deploy to staging. That is, of course, unless you tell it to deploy to somewhere else. When you want to deploy on a different connection or more than one, you use the --on
switch.
# Deploy to production $ rocketeer deploy --on="production" # Deploy to staging and production $ rocketeer deploy --on="staging,production"
Want to have a look at what will happen on your server once you hit the button? Use the --pretend
flag to let the tool tell you what it will execute on the server.
$ rocketeer deploy --pretend
Houston, We Got a Problem. We Need a Rollback.
Unfortunately we need to take care of deployments that break functionality or wreak havoc in the infrastructure. Then you need to make a quick rollback to your latest release. Luckily it’s a simple operation—just run the command:
$ rocketeer rollback
Since it stores a number of builds, performing a rollback is fast. It changes the symbolic link of current
to the previous release.
Shared Directories
Setting up shared directories is simple—just add them to the shared
array found in remote.php
. Rocketeer will create and link these folders for you in the deployments after. The specified paths should be relative to your root folder.
'shared' => [ 'storage/logs', 'storage/sessions', 'storage/uploads', '.env', ],
Writable Directories
Most shared directories will also need the web server to be able to write to them. Writing logs, sessions or file uploads is often a task performed by any application. These you add to the permissions.files
array in remote.php
.
'permissions' => [ 'files' => [ 'storage/sessions', 'storage/logs', 'storage/uploads', ], // [...] ],
Install or Update Dependencies
Installing or updating dependencies is something you need if the application relies on any kind of dependencies. The tool comes with support for the most popular package managers. Configuring anything is not necessary if you have the default setup for them. It will detect and install or update dependencies for Composer, Npm, Bower and Bundler. The default strategy for dependencies
is set to Polyglot
. This is the tool’s way of detecting and installing dependencies for the different package managers.
But let’s say that you want to install all dependencies on staging, and the tool uses the --no-dev
flag by default. Perhaps you want to install PHPUnit for running tests, which is a development dependency. In strategies.php
, you can find the composer
key, which tells the tool how to execute Composer. You can then override this in config.php
:
use Rocketeer\Binaries\PackageManagers\Composer; // [...] 'on' => [ // [...] 'connections' => [ 'staging' => [ 'strategies' => [ 'composer' => [ 'install' => function (Composer $composer, $task) { return $composer->install([], ['--no-interaction' => null, '--prefer-dist' => null]); } ], ], ] ], ],
Database Migrations
Migrating databases is often something you want to do when you have a complete release, just before it symlinks to current. Whatever tool you use, you can tell it to run before the deploy.before-symlink
. This hook is not a regular one, but an internal hook. You then need to register it someplace else than hooks.php
. You can this do this in events.php
, which you can create if it doesn’t exist already.
use Rocketeer\Facades\Rocketeer; // Laravel Rocketeer::addTaskListeners('deploy', 'before-symlink', function ($task) { $task->runForCurrentRelease('php artisan migrate'); }); // Symfony2 Rocketeer::addTaskListeners('deploy', 'before-symlink', function ($task) { $task->runForCurrentRelease('php app/console doctrine:migrations:migrate'); }); // Stand-alone Doctrine Rocketeer::addTaskListeners('deploy', 'before-symlink', function ($task) { $task->runForCurrentRelease('doctrine migrations:migrate --no-interaction'); });
Running Tests
Running tests in a deployment process is a great way of ensuring that no broken code or tests slip through the cracks. By default, the tool uses PHPUnit, and you can hook the test runner to run after dependencies are installed or updated.
'after' => [ 'setup' => [], 'deploy' => [], 'dependencies' => ['test'], 'cleanup' => [], ],
We should now see it’s executing PHPUnit on each deployment, and in case of any failing tests it will abort. Make sure you see output from it, otherwise it might have a problem with finding a PHPUnit binary or your test suite.
staging/0 |---- Test (Run the tests on the server and displays the output) fired by dependencies.after staging/0 |------ Test/Phpunit (Run the tests with PHPUnit) $ cd /var/www/my-deployable-app/releases/20160129220251$ /var/www/my-deployable-app/releases/20160129220251/vendor/bin/phpunit --stop-on-failure [[email protected]] (staging) PHPUnit 4.8.21 by Sebastian Bergmann and contributors. [[email protected]] (staging) [[email protected]] (staging) . [[email protected]] (staging) Time: 4.79 seconds, Memory: 6.00Mb [[email protected]] (staging) OK (1 test, 1 assertion) [[email protected]] (staging)staging/0 |=====> Tests passed successfully
Front-End Build Tools
Often our applications are not only a back end, unless they are a REST API for example. Running build tools for our front end is a common task with tools such as Grunt, Gulp or Webpack. Making this part of our deployment process is nothing fancier than using a hook to run the commands such as:
'after' => [ 'setup' => [], 'deploy' => [], 'dependencies' => ['gulp build'], 'cleanup' => [], ],
A front end often relies on dependencies as well, so run tools after installing or updating them.
Tips & Tricks
Running Updates
If you do not want to create a new release when you deploy, you have the option of running an update. Be cautious when doing this since you won’t be able to roll back to the previous version, only the previous release. But it is a quick and simple way of updating your application with the latest changes with:
$ rocketeer update
Local Tasks
Sometimes it can be nice to run tasks in your local environment. Say you want to run a PHPCS check or build your static assets and upload them to the server, removing the need of certain binaries on the server. If you create a task class, you can set the protected variable $local
to true
.
class MyTask extends Rocketeer\Abstracts\AbstractTask { protected $local = true; // [...] }
Conclusion
The deployment process is an important part of an application’s lifecycle. Tools like Rocketeer allow you with ease to make this an uncomplicated matter. This is especially true when using it for a PHP application since it integrates so well with it.
Writing an introductory tutorial to Rocketeer turned out to be a hard task. The tool is so flexible that drawing the lines on where to stop isn’t easy. I hope I got across the possibilities in using this tool and how it can benefit you and your application. If you want to dig deeper, I suggest reading the full documentation. There’s much more to the tool than what I could cover in this article.