Published 8 Jun 2020

Zero-downtime Laravel deploys with this simple bash script

This is a quick and simple bash script which allows you to deploy Laravel projects with zero downtime and even rollbacks.

I made this for a nice and simple way to deploy my own Laravel projects without the need for a 3rd party service. I even use it to deploy this blog.

Assumptions & pre-requisites

  • You have a Laravel project on GitHub
  • You want to deploy it to a Linux server
  • You have SSH access to the server
  • You have a basic understanding of editing files via the command-line with something like Vim

Usage

Once set up, simply SSH into your server and type deploy.

Starting off

We're going to create the deploy script inside a bin folder under your username.

SSH into your server and run the following:

mkdir ~/bin
touch ~/bin/deploy

You'll also need to update your .bash_profile with the path to your bin folder, so you can run the deploy command from anywhere.

Just run the following:

echo 'export PATH=/path/to/your/bin:$PATH' >>~/.bash_profile
source ~/.bash_profile

Making the script executable

We now have an empty script file. We need to make the file executable.

chmod u+x ~/bin/deploy

Before we add the script

Personal Access Token

You'll need a Personal Access Token from GitHub to be able to clone your repository. If you don't have one, you can do this by going into GitHub, clicking your profile picture in the top right > Settings > Developer Settings > Personal Access Tokens > Generate New Token.

.env file

You'll also need a base .env file sat one level above your project directory.

Note: With the script I am assuming your project's document root is /var/www/vhosts/myproject/current/public. Please ensure to change this accordingly.

So make sure to add your production .env file to /var/www/vhosts/myproject.

cd /var/www/vhosts/myproject
vim .env

then paste your contents accordingly.

Your project folder

The script works best with an empty project that hasn't been deployed yet. The reason for this is we use a symbolic link to the document root, rather than putting the files there directly. You can get around this however by running mv current current-backup before the deploy (keep in mind this will bring down your site until you re-deploy).

Adding the script

We're ready to add the script. cd ~/bin and then vim deploy to open the empty script file and add the following:

#!/usr/bin/bash

NUM_RELEASES=4
PERSONAL_ACCESS_TOKEN=
GIT_USERNAME=
REPO_NAME=

echo "Deploying..."
cd /var/www/vhosts/myproject/

if [ -d releases ]; then
    cd releases
else
    mkdir releases && cd releases
fi

echo "Removing old releases..."
INDEX=0
FOLDERS=`ls | wc -l`

if [ $FOLDERS -ge $NUM_RELEASES ]; then
  for d in */ ; do
    if [ $INDEX -le $(( $FOLDERS - $NUM_RELEASES )) ]; then
        rm -rf $d
    fi

     INDEX=$(( INDEX + 1 ))
  done
fi

echo "Cloning repository..."
NEWRELEASE=$(date +%Y%m%d%H%M%S)
git clone https://$PERSONAL_ACCESS_TOKEN@github.com/$GIT_USERNAME/$REPO_NAME.git $NEWRELEASE
cp ../.env $NEWRELEASE/.env
cd $NEWRELEASE

echo "Installing composer dependencies..."
composer install --optimize-autoloader --no-dev --no-interaction --prefer-dist

echo "Installing npm dependencies..."
npm install --no-audit --prefer-offline

echo "Compiling front-end assets..."
npm run prod

echo "Running artisan commands..."
php artisan migrate --force
php artisan storage:link
php artisan optimize
php artisan queue:restart
cd ../..

echo "Activating new release..."
rm current && ln -s releases/$NEWRELEASE current

echo "Deploy complete!"

Feel free to fill in the variables here with the correct values, and tweak the artisan commands to fit your use-case.

And that's it! Simply run the command deploy and you're off and running.

How it works

The zero-downtime is achieved by using a symbolic link from the document root to the most recent release.

So when you deploy, it builds a new release in a separate directory, then once everything is finished, it simply updates the symbolic link to point at the new directory instead. This happens pretty much instantly, so it's a seamless experience for your users.

The script keeps up to 4 releases in your releases folder at any time. You can of course adjust this in the script to keep more or less if you like.

Rolling back

If for any reason you need to roll back a deploy to a previous one - for example if there's a bug in production - you can do so in the following way:

cd /var/www/vhosts/myproject/releases && ls -lah

Find the folder name of the release you want to roll back to (probably the second one down in the list)

cd ../
rm current && ln -s releases/{folderName} current

Of course, you'll need to have deployed at least twice to be able to roll back to a previous version. If there's enough demand for this I'll create a separate rollback script.

There it is! Hope you like it - please drop me an email to tom@tomn.dev or on Twitter @thetomnewton with your thoughts and feedback on how to improve it even more.