Zero downtime deployments with Envoy
Envoy is Laravel's official task runner. Using a Blade style syntax tasks can be defined that can be run both locally and remotely. At Spatie, we've been using Envoy for quite some time to deploy code on production servers. Summarized our trusty Envoy script did the following things: bringing the application down, pulling new code on the server via the installed git repo, running composer install, running migrations, etc..., and finally bringing the application back up. The script had a big downside: the application would be down for close to a minute. This week I took the time to solve that issue.
In February Chris Fidao, of Servers for Hackers-fame, already created an Envoy script to deploy code with zero downtime. Read his entire post to know how it works. In short it comes down to this:
- a new version of the entire application gets prepared in a directory on the server
- when it's fully ready the script will create a symlink so the webserver will use the sources in the prepared directory
Michael Dyrynda describes the process in more detail on his blog. Some big name deployment tools, like Capistrano, use this strategy too.
I've used Michael's code as a basis for our updated Envoy script. The basic flow is the same but I'll highlight some changes.
Loading the .env file
Envoy has the ability to send messages to Slack. In order to do this a hook must be specified. It's best practice to store all sensitive configuration in the .env file. By default Envoy will not read this file. By adding this code in the setup all values specified in the .env file can be read.
@setup
require __DIR__.'/vendor/autoload.php';
(new \Dotenv())->load(__DIR__, '.env');
...
@endsetup
@after
@slack(env('SLACK_ENDPOINT'), "#deployments", "{$task} done")
@endafter
Enabling fast code deployments
Doing an entire deployment encompasses generating assets, doing a composer install and generating the Laravel's compiled.php file. These tasks can take quite some time. More often than not an entire deployment process lasts more than a minute. When doing a simple bugfix in a php file we shouldn't need to run through all these steps. It would be great if we could just to a git pull on the server. In Michaels script he downloaded a tarball to the server. This is great for preserving disk space, but a simple git pull isn't possible. We swapped out downloading the tarball in favor of doing a git clone:
git clone --depth 1 git@bitbucket.org:{{ $repository }} {{ $newReleaseName }}
The --depth 1
indicates that the history doesn't need to be downloaded.
If you've read Chris' and Michael's posts than you know that the storage directories in our project are removed and symlinked to a persistent storage directory. If we would just perform git pull
on our repo on the server these symlinks would be overwritten by the files in the repo. This can be prevented by using git's sparse checkout feature:
git config core.sparsecheckout true
echo "*" > .git/info/sparse-checkout
echo "!storage" >> .git/info/sparse-checkout
echo "!public/build" >> .git/info/sparse-checkout
git read-tree -mu HEAD
Using sparse checkout prevents some files and directories from being checked out. Our symlinks will remain in place. Now we can use the task below to, in a few seconds, deploy some code without having to jump all hoops of a full deploy:
@task('deployOnlyCode',['on' => 'remote'])
cd {{ $currentDir }}
git pull origin master
@endtask
Refreshing PHP's OPCache
When I was testing the Envoy script I noticed that sometimes new changes weren't visible when browsing to the application. My first thought that there was something wrong with the symlinks or webserver configuration. After at bit of Googling I stumbled on this article by Mattias Geniar. It contained the reason why the latest changes were not visible:
In short: if the "path" to your document root or PHP doesn't actually change, PHP's OPCache will not reload your PHP Code.So to clear the OPCache the PHP process needs to be restarted after every deploy:
sudo service php5-fpm restart
If you have any questions regarding our new deployment process are have suggestions to improve it, let me know in the comments below.
What are your thoughts on "Zero downtime deployments with Envoy"?