Zero Downtime Deployments With Laravel Forge


Laravel Forge is a fantastic tool for rapidly spinning up servers with all the best and latest software. It also provides for easy PHP project deployment and setup. However, the default deploy script is rather lacking in a sense that it results in downtime.

Envoyer is a tool that solves this issue, but if you just want zero downtime deployments and don’t want any of the other features that it provides, it might be hard to justify shelling out for another tool.

Here’s a way to handle zero downtime deployments from Forge, in a very similar manner to Envoyer.

Note: the default Forge project is used as an example throughout this post. Remember to change it to whatever your project domain name is set to in Forge.

Deploying

The below script should go into the Deploy Script section of your pre-existing Forge website.

cd /home/forge

PROJECT_NAME="default"
PROJECT_REPO="git@github.com:laravel/laravel.git"
RELEASES_KEPT=3
RELEASE=$(date +"%Y-%m-%d-%H-%M-%S")

# Copy the old .env file
cp $PROJECT_NAME"/.env" $PROJECT_NAME".env"

# Create a default-releases directory if it does not exist.
if [ ! -d $PROJECT_NAME"-releases" ]
then
    mkdir $PROJECT_NAME"-releases"
fi

# Create our new release.
cd $PROJECT_NAME"-releases"
git clone $PROJECT_REPO $RELEASE

cd $RELEASE

# Install dependencies
composer install --no-interaction --prefer-dist --optimize-autoloader

# Move in the old .env file and clean up.
cp -af "../../"$PROJECT_NAME".env" ".env"
rm "../../"$PROJECT_NAME".env"

if [ -f artisan ]
then
    php artisan migrate --force
fi

# Clean up old releases.
cd ..
rm -rf `ls -1 | sort -r | tail -n +$((RELEASES_KEPT+1))`

cd /home/forge

# Remove the project directory if exists.
if [ -d $PROJECT_NAME ]
then
    rm -rf $PROJECT_NAME
fi

# Relink our new release.
ln -sfn $PROJECT_NAME"-releases/"$RELEASE $PROJECT_NAME

# Reload PHP-FPM
echo "" | sudo -S service php7.1-fpm reload

Configuring

There are 4 variables defined at the beginning of the script: PROJECT_NAME, PROJECT_REPO, RELEASES_KEPT and RELEASE.

PROJECT_NAME - should be set to the domain name that you have given your website in Forge.

PROJECT_REPO - is the git repository with your project files. Should be the same repo that you provided when setting up your website for the first time. This must bet an SSH link and not HTTPS.

RELEASES_KEPT - the number of releases that you want to keep present. These will be used for rolling back, should your freshly deployed code not work as intended.

RELEASE - the naming pattern for naming your releases. By default, it’s the deployment date separated by hyphens.

First Deploy

The very first deployment will introduce a bit of downtime as it has to remove the directory of your project and replace it with a symlink.

Which leads into another point…

Changing the storage path

Since we’re essentially using a fresh copy of code every time we deploy, the storage/ folder will not have any of the files that the app has added during its release lifetime.

storage/ usually holds some of the frameworks cache stuff, logs and other tidbits. In most cases, you’ll want to keep this folder untouched whenever you do a release.

This issue can be solved by changing your storage/ path to be outside of your PROJECT_NAME directory. So something like /home/forge/default-storage would work.

To do this, SSH into your Forge server and run the following to create the new storage folder:

cp -rp /home/forge/default/storage /home/forge/default-storage

Now open up your bootstrap/app.php and just before return $app; add this:

$app->bind('path.storage', function() {
    return env('APP_STORAGE');
});

The last step is to add APP_STORAGE=/home/forge/default-storage to your environment variables.

Rolling Back

Rolling back is just symlinking to an older release and then rolling back your database changes. You can do this by SSHing into your server and running the following:

ln -sfn default-releases/ROLLBACK_RELEASE_FOLDER default

And then roll back any migrations from the previous release:

default/artisan migrate:rollback

Replace the ROLLBACK_RELEASE_FOLDER with whatever version of your application you want to go back to.

Ending Notes

I don’t want to take away anything from Envoyer with this. It is a fantastic tool and if you need more control over your deployments, you should probably use it.

If you find any issues with this post, or would like to add anything to it, please get in touch via the socials on this site and let me know.

Related Posts

Testing Laravel Applications without Vagrant or Docker