Support the ongoing development of Laravel.io →

Zero downtime deployment for Laravel apps

20 Feb, 2022 5 min read

While I was building laravelremote.com (a Laravel remote job aggregator), I was worried that each time I wanted to push some changes to the production server, it might take the website down for a couple of seconds. Which probably isn't a big deal, but I still wanted to avoid it.

Laravel offers a first-party paid product to avoid this, Envoyer it's only $10 bucks a month. But laravelremote.com doesn't generate any revenue right now, and I'm the type of person that likes to do things in-house to learn how it works, and I also like the freedom that it provides.

So with that in mind, I figured out a way to do this for free, and I wanted to share this in case someone else finds it useful.

The theory

I searched around the internet to see how Envoyer works under the hood, I don't exactly remember where I saw this, but basically, it works like this:

We have 3 directories that relate to our project:

  • releases/
  • current/
  • storage/

A new directory will be created in the releases/ directory each time we need to deploy a change to the website, then we run some "set up" commands (ex. composer install, move the cache routes, views, etc.), and we add a symbolic link from the releases' storage directory to the "permanent" storage/ directory, since we don't want to forget sessions and stuff like that on each deployment. After all of this, we add a symbolic link to the current/directory that points to the new release we just created.

This article is not a tutorial on how to deploy a Laravel app, so I'm not going to explain my NGINX configuration. The only thing you need to know is that my NGINX server is configured to read my website from the /var/www/laravelremote.com/ directory. This directory has another symbolic that points to the current/ directory.

More simply, every new release will be deployed then linked to the current directory, which will make the server show the updates.

The practice

Let's put that into practice, first I created a base directory that will contain everything we need for the project. I like to add this directory under the home folder and give it the name of the app

mkdir laravelremote/

since we are going to be working in this directory, we should cd into it

cd laravelremote

Now we need to create the storage directory and the other subdirectories that Laravel uses.

mkdir storage/
mkdir -p storage/cache/data/
mkdir storage/sessions/
mkdir storage/views/

These are the default Laravel directories that exist within the storage directory.

After that, we need to create the releases folder.

mkdir releases/

Also, we should create and global .env file that we should link to on every release.

touch .env

Add to this file the production environment variables.

The current directory will be created once we deploy the project for the first time, so there is no need to create it right now.

Deploying

We could create a new directory in the releases' directory, pull the main branch from GitHub, run every other needed command, and link everything at the end to the current directory. But that seems like a lot of steps for simple deployment, so I created a bash script to do that instead:

#!/bin/sh

UNIX_TIME=$(date +%s)

DEPLOYMENT_DIRECTORY=$UNIX_TIME

mkdir -p $DEPLOYMENT_DIRECTORY
git clone --depth 1 --branch master [email protected]:username/reponame.git $DEPLOYMENT_DIRECTORY

echo removing storage
rm -Rf $DEPLOYMENT_DIRECTORY/storage

cd $DEPLOYMENT_DIRECTORY

cp ~/laravelremote/.env .env

echo link master storage
ln -s -n -f -T ~/laravelremote/storage ~/laravelremote/releases/$DEPLOYMENT_DIRECTORY/storage

composer install --no-dev
npm install
npm run prod

echo view:cache command
php artisan view:cache

echo route:cache command
php artisan route:cache

echo config:cache command
php artisan config:cache

echo migrations
php artisan migrate --force


cd ..

#link to the latest deployment from Live
ln -s -n -f -T ~/laravelremote/releases/$DEPLOYMENT_DIRECTORY ~/laravelremote/current

echo Done!

This script lives under the releases/ folder. It creates a new directory for the new release with the current UNIX timestamp, then it pulls the repo from GitHub into that directory. After that, I remove the storage folder from that directory, copy the .env file that contains the production keys (you might want to create a symbolic link instead), and then link the "permanent" storage file to the current release.

After that, I run some necessary commands for the project to work.

  • Install the composer dependencies
  • Install the NPM dependencies and build the resources. (I don't commit my build resources, other people might do it differently)
  • Cache the views, routes, and config for optimization
  • Run the database migrations

Then finally, we link the current directory to this release, which will publish the changes.

And that's it, whenever I push something to the master branch, I ssh into the server, and then I run this script, and my changes are deployed with 0 downtime.

Possible improvements

I still have some things that I don't like about this approach. One of them is that I would like to make it, so it automatically deploys the changes when I push them to GitHub without having to ssh into the server. Another one is that the old versions are not removed automatically, I currently remove them manually, but I would like to automate this too. And since we are talking about that, I noticed that Envoyer has a way to roll back the changes if something goes wrong, so you can link one of the previous versions if you need to. That would probably need a more complicated setup on my part, but It would be interesting to do.

Regardless, I'm pretty happy with how this turned up, even though I still want to improve it. If you would like to hear about those future improvements follow me on Twitter, I'll probably post any developments there.

Last updated 2 months ago.
2
Like this article? Let the author know and give them a clap!
cosmeoes (Cosme Escobedo) Writing at https://cosme.dev

Other articles you might like

June 22nd 2022

Adding Social Logins to Your Laravel Apps: Twitter and GitHub

Introduction In your Laravel applications, you would typically provide the functionality for your us...

Read article
February 24th 2022

Setting up Laravel with Inertia.js + Vue.js + Tailwind CSS

NOTICE: This guide was written using Laravel 8, but it 100% works on Laravel 9. Laravel is by far...

Read article
February 10th 2022

Filter Eloquent models with multiple optional filters

Often we need to filter eloquent models when displaying to a view. If we have a small number of filt...

Read article

We'd like to thank these amazing companies for supporting us

Your logo here?

The Laravel portal for problem solving, knowledge sharing and community building.

© 2022 Laravel.io - All rights reserved.