Skip to content

Instantly share code, notes, and snippets.

@mrl22
Last active May 22, 2025 10:22
Show Gist options
  • Save mrl22/46bf434bec949c86ae26d06654748010 to your computer and use it in GitHub Desktop.
Save mrl22/46bf434bec949c86ae26d06654748010 to your computer and use it in GitHub Desktop.
Laravel Forge Zero-Downtime Deploy Script with Releases and Persistant Storage

Laravel Forge Zero-Downtime Deploy Script

Features

  • Zero Downtime - Gets everything ready, and then switches the current directory symbolic link
  • NPM Install
  • Composer Install
  • Persistant storage at /storage/
  • 5 most recent releases are stored in /releases/ for quick rollback
  • Latest live release activated at /current/
  • Works on existing and already deployed projects

Structure

This deployment script organises deployment into a set of directories, each with its specific purpose:

  • releases: This directory holds a number of past deployments. Each deployment creates a new subdirectory named after the timestamp of the release.
  • storage: The storage directory is your Laravel storage directory designed for files and directories that must persist between deployments, such as user-uploaded assets, and logs.
  • current: A symbolic link pointing to the latest release in the releases directory. This allows for quick rollbacks and minimal downtime during deployments.

How to setup

  1. Create a site, the Web Directory must be set to /current/public.
  2. Setup up deployment of a laravel project.
  3. Once deployments tab appears, replace the deployment script with the one attached. Make sure to update the first line!
  4. Deploy again.

If the script detects that app directory exists inside BASE_DIR, it will run through a setup process where it creates a releases structure and moves your existing Laravel project into it.

Every time you deploy, this script will copy your old release, git pull/clean and prepare it for going live.

Once it is finished, it will change BASE_DIR/current symbolic link to your latest release resulting in a zero downtime deployment.

We keep a copy of artisan and your .env file in BASE_DIR/ so that Forge GUI can read and run commands.

Rollback

If you have an issue with a deployment, you can quickly roll back by swapping the symbolic link:

ln -sfn /home/forge/site.com/releases/oldrelease /home/forge/site.com/current

Bash is not my primary language, if you have any improvements, please let me know in the comments.

BASE_DIR="/home/forge/site.com"
## APPLICATION WEB DIRECTORY MUST BE SET TO "/current/public"
echo "Starting deployment..."
# Define paths using Forge variables
RELEASES_DIR="$BASE_DIR/releases"
CURRENT_DIR="$BASE_DIR/current"
STORAGE_DIR="$BASE_DIR/storage"
TIMESTAMP=$(date +%Y%m%d%H%M%S)
RELEASE_DIR="$RELEASES_DIR/$TIMESTAMP"
APP_DIR="$BASE_DIR/app"
# Navigate to base directory
cd $BASE_DIR
# Check if the app directory exists
if [ -d "$APP_DIR" ]; then
echo "First Run: Found existing app directory. Moving to new release directory..."
mkdir -p $RELEASE_DIR
shopt -s dotglob nullglob
for item in *; do
if [ "$item" != "releases" ] && [ "$item" != "current" ] && [ "$item" != "storage" ] && [ "$item" != ".env" ]; then
mv "$item" "$RELEASE_DIR/"
fi
done
shopt -u dotglob nullglob
else
LATEST_RELEASE=$(ls -td $RELEASES_DIR/* | head -n 1)
if [ -d "$LATEST_RELEASE" ]; then
echo "Copying previous release directory to new release directory..."
cp -a $LATEST_RELEASE/. $RELEASE_DIR/
fi
fi
cd $RELEASE_DIR/
git reset --hard origin/$FORGE_SITE_BRANCH
git pull origin -f $FORGE_SITE_BRANCH
git clean -f
# Link storage directory
echo "Linking storage directory..."
rm -rf storage
ln -s $STORAGE_DIR storage
# Link .env file
echo "Linking .env file..."
rm -f .env
ln -sf $BASE_DIR/.env .env
echo "Installing Composer dependencies..."
$FORGE_COMPOSER install --no-dev --no-interaction --prefer-dist --optimize-autoloader -d $RELEASE_DIR
# Install Node.js dependencies and build frontend
echo "Installing Node.js dependencies..."
npm install
echo "Building frontend..."
npm run build
rm -rf node_modules
if [ -f artisan ]; then
echo "Running migrations..."
$FORGE_PHP artisan migrate --force
echo "Running storage link..."
$FORGE_PHP artisan storage:link --force
fi
# Switch symlink to new release
echo "Activating new release..."
ln -sfn $RELEASE_DIR/artisan $BASE_DIR/artisan
ln -sfn $RELEASE_DIR $CURRENT_DIR
# Reload PHP-FPM
(
flock -w 10 9 || exit 1
echo "Restarting PHP-FPM..."
sudo -S service $FORGE_PHP_FPM reload
) 9>/tmp/fpmlock
$FORGE_PHP artisan queue:restart
# Clean up old releases, keeping the latest 5
echo "Cleaning up old releases..."
cd $RELEASES_DIR
ls -1tr | head -n -5 | xargs -d '\n' rm -rf --
echo "Deployment completed successfully."
@esyx0
Copy link

esyx0 commented May 1, 2025

Thanks a lot for this, i was going crazy trying to fix my deploy script <3

@mrl22
Copy link
Author

mrl22 commented May 1, 2025

Thanks a lot for this, i was going crazy trying to fix my deploy script <3

You're most welcome!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment