Laravel Zero Downtime Deployment Script
A nice zero downtime deployment script for Laravel apps.
Deploying a Laravel application without downtime is essential for maintaining a seamless user experience. This deployment script achieves exactly that, ensuring a smooth transition between releases. It clones the latest version of your project, sets up environment files, installs dependencies, runs migrations, and links the new release—all while keeping your application live. With built-in clean-up for old releases and error handling, this script is a robust solution for modern Laravel deployments.
1# SETUP #
2DOMAIN=example.com
3PROJECT_REPO="[email protected]:example.com/app.git"
4AMOUNT_KEEP_RELEASES=5
5
6RELEASE_NAME=$(date +%s--%Y_%m_%d--%H_%M_%S)
7RELEASES_DIRECTORY=~/$DOMAIN/releases
8DEPLOYMENT_DIRECTORY=$RELEASES_DIRECTORY/$RELEASE_NAME
9
10# stop script on error signal (-e) and undefined variables (-u)
11set -eu
12
13printf '\nℹ️ Starting deployment %s\n' "$RELEASE_NAME"
14mkdir -p "$RELEASES_DIRECTORY" && cd "$RELEASES_DIRECTORY"
15
16printf '\nℹ️ Clone GIT project from %s and checkout branch %s\n' "$PROJECT_REPO" "$FORGE_SITE_BRANCH"
17git clone "$PROJECT_REPO" "$RELEASE_NAME"
18cd "$RELEASE_NAME"
19git checkout "$FORGE_SITE_BRANCH"
20git fetch origin "$FORGE_SITE_BRANCH"
21git reset --hard FETCH_HEAD
22
23printf '\nℹ️ Link env file\n'
24ENV_FILE=~/"$DOMAIN"/shared/.env
25if [ -f "$ENV_FILE" ]; then
26 rm -rf ./env
27 ln -s -n -f -T $ENV_FILE ./.env
28else
29 printf '\nError: env file is missing at %s.' "$ENV_FILE" && exit 1
30fi
31
32printf '\nℹ️ Link storage folder\n'
33STORAGE_DIR=~/"$DOMAIN"/shared/storage
34if [ -d "$STORAGE_DIR" ]; then
35 rm -rf ./storage
36 ln -s -n -f -T $STORAGE_DIR ./storage
37else
38 printf '\nError: storage dir is missing at %s.' "$STORAGE_DIR" && exit 1
39fi
40
41printf '\nℹ️ Install Composer Dependency Updates\n'
42$FORGE_COMPOSER install --no-interaction --prefer-dist --optimize-autoloader --no-dev
43
44printf '\nℹ️ Installing NPM dependencies based on \"./package-lock.json\"\n'
45npm ci
46printf '\nℹ️ Generating JS App files\n'
47npm run build
48
49printf '\nℹ️ Laravel Artisan commands\n'
50if [ -f artisan ]; then
51 printf '\nℹ️ Link ./public/storage\n'
52 $FORGE_PHP artisan storage:link
53
54 printf '\nℹ️ Clear and cache routes, config, views, events\n'
55 $FORGE_PHP artisan config:cache
56 $FORGE_PHP artisan route:cache
57 $FORGE_PHP artisan view:cache
58 $FORGE_PHP artisan event:cache
59
60 printf '\nℹ️ Database Migrations\n'
61 $FORGE_PHP artisan migrate --force
62fi
63
64printf '\nℹ️ !!! Link Deployment Directory !!!\n'
65echo "$RELEASE_NAME" >> $RELEASES_DIRECTORY/.successes
66if [ -d ~/$DOMAIN/current ] && [ ! -L ~/$DOMAIN/current ]; then
67 rm -rf ~/$DOMAIN/current
68fi
69ln -s -n -f -T "$DEPLOYMENT_DIRECTORY" ~/$DOMAIN/current
70
71# if your "fastcgi_param SCRIPT_FILENAME" is set to "$realpath_root$fastcgi_script_name" you do not need to restart fpm,
72# which can lead to droped or failed requests.
73printf '\nℹ️ Restart PHP FPM\n'
74( flock -w 10 9 || exit 1
75 echo 'Restarting FPM...'; sudo -S service $FORGE_PHP_FPM reload ) 9>/tmp/fpmlock
76
77printf '\nℹ️ Restart Horizon Queue Workers\n'
78 $FORGE_PHP artisan horizon:terminate
79
80# Clean Up
81cd $RELEASES_DIRECTORY
82
83printf '\nℹ️ Delete failed releases:\n'
84if grep -qvf .successes <(ls -1)
85then
86 grep -vf .successes <(ls -1)
87 grep -vf .successes <(ls -1) | xargs rm -rf
88else
89 echo "No failed releases found."
90fi
91
92printf '\nℹ️ Delete old successful releases:\n'
93AMOUNT_KEEP_RELEASES=$((AMOUNT_KEEP_RELEASES-1))
94LINES_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)
95if [ "$LINES_STORED_RELEASES_TO_DELETE" != 0 ]; then
96 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-
97 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
98 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
99else
100 AMOUNT_KEEP_RELEASES=$((AMOUNT_KEEP_RELEASES+1))
101 LINES_STORED_RELEASES_TOTAL=$(find . -maxdepth 1 -mindepth 1 -type d -printf '%T@\t%f\n' | wc -l)
102 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"
103fi
104
105printf '\nℹ️ Status - stored releases:\n'
106find . -maxdepth 1 -mindepth 1 -type d -printf '%T@\t%f\n' | sort -nr | cut -f 2-
107
108printf '\n�
109 Deployment DONE: %s\n' "$DEPLOYMENT_DIRECTORY"