Zero-Downtime Deployment with Laravel Forge
Tags: laravel, forge, larafast, webdev
Published: 2024-04-18 09:48:26


![Laravel Forge](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v4lb83fhm1wm20vqg5d0.png)


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:

```bash
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
```

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:

```bash
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
```

### Environment Setup

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

```bash
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
```

### Running Laravel Commands

```bash
$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
```

### Dependency Installation and Build

The script installs NPM dependencies and generates necessary files:

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

### Linking Deployment Directory

The script links the deployment directory to the current directory:

```bash
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

Lastly, the script performs clean-up tasks:

```bash
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"
```

## Full Script

```bash
# 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"
```
## 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.