I’ve grown, I’ve learned some stuff, I’ve broken some other stuff. So I think it’s about time I did…

Zero Downtime Laravel Deployments with Envoy—Version 2!

My first attempt at zero downtime deployments was exactly what I needed a few years ago. But now, I have something better.

A Quick Recap

The Compiled Assets Problem

The Missing Piece

The Solution

Making it work

Step 1: Ignoring some stuff

/public/mix-manifest.json
*
!.gitignore

Step 2: Server set up

/opt
|
+- /vendor
|
+- /project
|
+- /releases
git clone --depth 1 -b master git@gitlab.com:project.git initial
curl -sS https://getcomposer.org/installer | php
sudo cp composer.phar /usr/local/bin/composer

Step 3: .env and storage

cp /opt/vendor/project/releases/initial/.env.example /opt/vendor/project/.env
cp -r /opt/vendor/project/releases/initial/storage /opt/vendor/project
/project
|
+- /releases
| |
| +- /initial
|
+- /.env
|
+- /storage
rm -rf /opt/vendor/project/releases/initial/storage
ln -nfs /opt/vendor/project/storage /opt/vendor/project/releases/initial/storageln -nfs /opt/vendor/project/.env /opt/vendor/project/releases/initial/.env
/project
|
+- /releases
| |
| +- /initial
| |
| +- /.env -> /opt/vendor/project/.env
| |
| +- /storage -> /opt/vendor/project/storage
|
+- /.env
|
+- /storage

Step 4: Assets

yarn prod
scp -rq public/css/ user@server.com:/opt/vendor/project/releases/initial/publicscp -rq public/js/ user@server.com:/opt/vendor/project/releases/initial/publicscp -q public/mix-manifest.js user@server.com:/opt/vendor/project/releases/initial/public
cd /opt/vender/project/releases/initialphp ./artisan key:generate
composer install --no-dev --prefer-dist
php ./artisan migrate

Step 6: Live!

ln -nfs /opt/vendor/project/releases/initial/public /opt/vendor/project/live
/project
|
+- /releases
| |
| +- /initial
| |
| +- /.env -> /opt/vendor/project/.env
| |
| +- /storage -> /opt/vendor/project/storage
|
+- /.env
|
+- /storage
|
+- /live -> /opt/vendor/project/releases/initial/public

Step 7: Envoy

composer global require laravel/envoy
@servers(['local' => '127.0.0.1', 'production' => 'user@server.com'])@setup
// any setup will go here
@endsetup
@story('deploy')
build
git
install
live
@endstory
@task('build', ['on' => 'local'])
@endtask
@task('git', ['on' => 'production'])
@endtask
@task('install', ['on' => 'production'])
@endtask
@task('assets', ['on' => 'local'])
@endtask
@task('live', ['on' => 'production'])
@endtask
@setup
$repo = 'git@gitlab.com:project.git';

$branch = 'master';

date_default_timezone_set('Africa/Johannesburg');
$date = date('YmdHis');

$appDir = '/opt/vendor/project';

$buildsDir = $appDir . '/releases';

$deploymentDir = $buildsDir . '/' . $date;

$serve = $appDir . '/live';
$env = $appDir . '/.env';
$storage = $appDir . '/storage';

$productionPort = 22;
$productionHost = 'user@server.com';

@endsetup
@task('build', ['on' => 'local'])
yarn prod
@endtask
@task('deployment', ['on' => 'production'])
git clone --depth 1 -b {{ $branch }} "{{ $repo }}" {{ $deploymentDir }}
@endtask
@task('install', ['on' => 'production'])
cd {{ $deploymentDir }}

rm -rf {{ $deploymentDir }}/storage

ln -nfs {{ $env }} {{ $deploymentDir }}/.env
ln -nfs {{ $storage }} {{ $deploymentDir }}/storage

composer install --prefer-dist --no-dev

php ./artisan migrate --force

php ./artisan storage:link
@endtask
@task('assets', ['on' => 'local'])
scp -P{{ $productionPort }} -rq ./public/js/ {{ $productionHost }}:/opt/vendor/project/public
scp -P{{ $productionPort }} -rq ./public/css/ {{ $productionHost }}:/opt/vendor/project/public
scp -P{{ $productionPort }} -q ./public/mix-manifest.json {{ $productionHost }}:/opt/vendor/project/public
@endtask
@task('live', ['on' => 'production'])
ln -nfs {{ $deploymentDir }}/public {{ $serve }}
@endtask
envoy run deploy
Envoy.blade.php zero-downtime script

But wait… There’s more!

Programmer, musician, cyclist (well... I own a bike), husband and father.