Zero-Downtime Deployment with Laravel Forge

karakhanyans

Serg

Posted on April 18, 2024

Zero-Downtime Deployment with Laravel Forge

Laravel Forge

Deploying web applications can be a repetitive and error-prone task. In this blog post, we'll introduce a Bash script designed to automate the deployment process for a web application hosted on a server. Let's dive into the details of the script and how it streamlines the deployment workflow.

Setup

Before diving into the deployment process, let's set up some initial configurations:

DOMAIN=example.com
PROJECT_REPO="your_github_repo_name"
AMOUNT_KEEP_RELEASES=5

RELEASE_NAME=$(date +%s--%Y_%m_%d--%H_%M_%S)
RELEASES_DIRECTORY=~/$DOMAIN/releases
DEPLOYMENT_DIRECTORY=$RELEASES_DIRECTORY/$RELEASE_NAME
Enter fullscreen mode Exit fullscreen mode

These variables define crucial aspects such as the domain name, project repository, and the number of releases to keep.

Deployment Process

Cloning Repository and Setup

The script starts by creating a unique release directory and clones the project repository into it:

cd /home/forge/$DOMAIN

mkdir -p "$RELEASES_DIRECTORY" && cd "$RELEASES_DIRECTORY"

git clone "$PROJECT_REPO" "$RELEASE_NAME"
cd "$RELEASE_NAME"
git checkout "$FORGE_SITE_BRANCH"
git fetch origin "$FORGE_SITE_BRANCH"
git reset --hard FETCH_HEAD
Enter fullscreen mode Exit fullscreen mode

Environment Setup

Next, the script copies the .env file from the project directory:

printf '\nℹ️ Copy ./.env file\n'
ENV_FILE=~/"$DOMAIN"/.env
if [ -f "$ENV_FILE" ]; then
  cp $ENV_FILE ./.env
else
  printf '\nError: .env file is missing at %s.' "$ENV_FILE" && exit 1
fi
Enter fullscreen mode Exit fullscreen mode

Running Laravel Commands

$FORGE_COMPOSER install --no-dev --no-interaction --prefer-dist --optimize-autoloader

( flock -w 10 9 || exit 1
    echo 'Restarting FPM...'; sudo -S service $FORGE_PHP_FPM reload ) 9>/tmp/fpmlock

if [ -f artisan ]; then
    $FORGE_PHP artisan migrate --force
fi
Enter fullscreen mode Exit fullscreen mode

Dependency Installation and Build

The script installs NPM dependencies and generates necessary files:

printf '\nℹ️ Installing NPM dependencies based on \"./package-lock.json\"\n'
npm install
printf '\nℹ️ Generating JS App files\n'
npm run build
Enter fullscreen mode Exit fullscreen mode

Linking Deployment Directory

The script links the deployment directory to the current directory:

printf '\nℹ️ !!! Link Deployment Directory !!!\n'
echo "$RELEASE_NAME" >> $RELEASES_DIRECTORY/.successes
if [ -d ~/$DOMAIN/current ] && [ ! -L ~/$DOMAIN/current ]; then
  rm -rf ~/$DOMAIN/current
fi
ln -s -n -f -T "$DEPLOYMENT_DIRECTORY/public" ~/$DOMAIN/current
Enter fullscreen mode Exit fullscreen mode

Clean Up

Lastly, the script performs clean-up tasks:

printf '\nℹ️ Delete failed releases:\n'
# Code for deleting failed releases

printf '\nℹ️ Delete old successful releases:\n'
# Code for deleting old successful releases

printf '\nℹ️ Status - stored releases:\n'
# Code for displaying stored releases

printf '\n✅ Deployment DONE: %s\n' "$DEPLOYMENT_DIRECTORY"
Enter fullscreen mode Exit fullscreen mode

Full Script

# SETUP #
DOMAIN=yourdomain.com
PROJECT_REPO="git@github.com:your-team/repo.git"
AMOUNT_KEEP_RELEASES=5

RELEASE_NAME=$(date +%s--%Y_%m_%d--%H_%M_%S)
RELEASES_DIRECTORY=~/$DOMAIN/releases
DEPLOYMENT_DIRECTORY=$RELEASES_DIRECTORY/$RELEASE_NAME

# stop script on error signal (-e) and undefined variables (-u)
set -eu

printf '\nℹ️ Starting deployment %s\n' "$RELEASE_NAME"

cd /home/forge/$DOMAIN

mkdir -p "$RELEASES_DIRECTORY" && cd "$RELEASES_DIRECTORY"

printf '\nℹ️ Clone GIT project from %s and checkout branch %s\n' "$PROJECT_REPO" "$FORGE_SITE_BRANCH"
git clone "$PROJECT_REPO" "$RELEASE_NAME"
cd "$RELEASE_NAME"
git checkout "$FORGE_SITE_BRANCH"
git fetch origin "$FORGE_SITE_BRANCH"
git reset --hard FETCH_HEAD

printf '\nℹ️ Copy ./.env file\n'
ENV_FILE=~/"$DOMAIN"/.env
if [ -f "$ENV_FILE" ]; then
  cp $ENV_FILE ./.env
else
  printf '\nError: .env file is missing at %s.' "$ENV_FILE" && exit 1
fi

$FORGE_COMPOSER install --no-dev --no-interaction --prefer-dist --optimize-autoloader

( flock -w 10 9 || exit 1
    echo 'Restarting FPM...'; sudo -S service $FORGE_PHP_FPM reload ) 9>/tmp/fpmlock

if [ -f artisan ]; then
    $FORGE_PHP artisan migrate --force
fi

printf '\nℹ️ Installing NPM dependencies based on \"./package-lock.json\"\n'
npm install
printf '\nℹ️ Generating JS App files\n'
npm run build

printf '\nℹ️ !!! Link Deployment Directory !!!\n'
echo "$RELEASE_NAME" >> $RELEASES_DIRECTORY/.successes
if [ -d ~/$DOMAIN/current ] && [ ! -L ~/$DOMAIN/current ]; then
  rm -rf ~/$DOMAIN/current
fi
ln -s -n -f -T "$DEPLOYMENT_DIRECTORY/public" ~/$DOMAIN/current

# Clean Up
cd $RELEASES_DIRECTORY

printf '\nℹ️ Delete failed releases:\n'
if grep -qvf .successes <(ls -1)
then
  grep -vf .successes <(ls -1)
  grep -vf .successes <(ls -1) | xargs rm -rf
else
  echo "No failed releases found."
fi

printf '\nℹ️ Delete old successful releases:\n'
AMOUNT_KEEP_RELEASES=$((AMOUNT_KEEP_RELEASES-1))
LINES_STORED_RELEASES_TO_DELETE=$(find . -maxdepth 1 -mindepth 1 -type d ! -name "$RELEASE_NAME" -printf '%T@\t%f\n' | head -n -"$AMOUNT_KEEP_RELEASES" | wc -l)
if [ "$LINES_STORED_RELEASES_TO_DELETE" != 0 ]; then
  find . -maxdepth 1 -mindepth 1 -type d ! -name "$RELEASE_NAME" -printf '%T@\t%f\n' | sort -t $'\t' -g | head -n -"$AMOUNT_KEEP_RELEASES" | cut -d $'\t' -f 2-
  find . -maxdepth 1 -mindepth 1 -type d ! -name "$RELEASE_NAME" -printf '%T@\t%f\n' | sort -t $'\t' -g | head -n -"$AMOUNT_KEEP_RELEASES" | cut -d $'\t' -f 2- | xargs -I {} sed -i -e '/{}/d' .successes
  find . -maxdepth 1 -mindepth 1 -type d ! -name "$RELEASE_NAME" -printf '%T@\t%f\n' | sort -t $'\t' -g | head -n -"$AMOUNT_KEEP_RELEASES" | cut -d $'\t' -f 2- | xargs rm -rf
else
  AMOUNT_KEEP_RELEASES=$((AMOUNT_KEEP_RELEASES+1))
  LINES_STORED_RELEASES_TOTAL=$(find . -maxdepth 1 -mindepth 1 -type d -printf '%T@\t%f\n' | wc -l)
  printf 'There are only %s successfully stored releases, which is less than or equal to your\ndefined %s releases to keep, so none of them got deleted.' "$LINES_STORED_RELEASES_TOTAL" "$AMOUNT_KEEP_RELEASES"
fi

printf '\nℹ️ Status - stored releases:\n'
find . -maxdepth 1 -mindepth 1 -type d -printf '%T@\t%f\n' | sort -nr | cut -f 2-

printf '\n✅ Deployment DONE: %s\n' "$DEPLOYMENT_DIRECTORY"
Enter fullscreen mode Exit fullscreen mode

Conclusion

Automation is key to improving deployment efficiency and reducing errors. By using this Bash script, you can streamline the deployment process for your web applications, saving time and ensuring consistency.

💖 💪 🙅 🚩
karakhanyans
Serg

Posted on April 18, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related