How to set up your Laravel application for zero-downtime (Envoyer/Capistrano) deploys

Posted on March 29, 2017 | By Matt Stauffer

If you've ever worked with Capistrano or Envoyer, you've probably seen a directory structure in your webroot that looks something like this:

root root   4096 Mar 29 18:44 .
root root   4096 Mar 28 14:15 ..
root root   47   Mar 29 14:54 current -> ./releases/1490824249
root root   4096 Mar 29 14:50 releases

Where you're expecting to see your webroot containing your Git repository, instead it's this weird structure. What gives?

A brief introduction to Capistrano-style zero-downtime deploys

The reason you're getting zero-downtime deploy from these tools is because the entire deploy process—clone, composer install, etc.—doesn't happen in the directory that is currently serving your site. Instead, each new release gets its own separate "release" directory, all while your site is still being served from its current "release" directory.

- current -> ./releases/1490802721 * apache/nginx serves from this directory
- releases
    - 1490802133 (the new release you're building right now)
    - 1490802721 (latest complete release)
    - 1490803081 (a little bit older release)
    - 1490824249 (an even older release)

All of these release directories are just subdirectories of releases. Each directory here represents one of your deploys, and each directory individually has everything needed to serve your site. Your web server points to yourproject/current/public and therefore the "currently served" release is just that which has a symlink pointed at it from yourproject/current.

So, once the build process is complete for each new release, your deploy tool will delete the current symlink and create a new current symlink that points to your latest release. Boom. Now that release is live.

Caveats

In general, Laravel is no different from any other project in that this style of deployment works great. In fact, a tool provided by Taylor Otwell, Envoyer, is predicated around this release style.

However, every tool has a different set of caveats around how to handle them well in zero-downtime settings. Here's why:

There are always some things that you want to persist between releases. Most of it lives in databases and caches, which is fine—nothing's wiping your database on every deploy. But some isn't. Take the storage folder; do you want to wipe that every time you push a new release? Naw. What about the .env file? Definitely naw. So there are a few quick tricks.

How to set up your deploy for Laravel

Remember: If you use Envoyer, this is all handled for you. But if you don't, here's what to do.

  1. Clone your release into a new release folder. This should be handled by your deploy tool.
  2. composer install -o --no-interaction
  3. php artisan migrate --no-interaction --force
  4. (optional, if you don't commit your built scripts) npm install (or yarn) and either gulp --production (Elixir) or npm run production (Mix)
  5. rm -rf storage && ln -s ../../storage ./
    (Delete the storage directory and symlink it to a storage folder in the parent)
  6. ln -s ../../.env ./
    (Symlink the .env file to a .env file in the parent)
  7. php artisan route:cache
  8. php artisan config:cache
  9. Update the current symlink to your new release. This should be handled by your deploy tool.
  10. php artisan queue:restart
    (Restart the queue.)

This is all you need to do on every deploy. As you can tell, you'll end up with a root directory that looks a bit more like this:

root root   4096 Mar 29 18:44 .
root root   4096 Mar 28 14:15 ..
root root   1033 Mar 29 18:44 .env
root root   47   Mar 29 14:54 current -> ./releases/1490824249
root root   4096 Mar 29 14:50 releases
root root   4096 Mar 29 14:51 storage

And, of course, you'll need to create the .env file and the storage directory and subdirectories before you run your build script for the first time.

But that's it! You're now ready to go! Get deploying!

Postscript

Looking for a list of steps to take on every deploy, regardless of whether or not it's Capistrano?

composer install -o --no-interactions
php artisan migrate --no-interaction --force
yarn && npm run production
php artisan route:cache
php artisan config:cache
php artisan queue:restart

Comments? I'm @stauffermatt on Twitter


Tags: laravel  •  envoyer  •  capistrano  •  deploy