Laravel deploy script
This page provides a ready-to-use bash deploy script for Laravel applications that reports deployment status to LogReader. Drop it in your project root, configure three environment variables, and every deploy will appear in your LogReader timeline — with the commit message, branch, and a clear success or failure status.
The script handles the full deploy sequence: enabling maintenance mode, pulling from git, running Composer, running migrations, clearing and rebuilding the cache, and bringing the app back online. If any step fails, the script exits immediately and reports the error.
Requirements
The script requires the following tools to be available on your server:
- bash — the script uses
set -euo pipefailfor safe error handling - curl — for sending deployment status to LogReader
- git — for pulling the latest code
- PHP — adjust the
PHPvariable to point to your binary (e.g./usr/local/php84/bin/php) - Composer — the script tries
composer.pharfirst, then falls back to the systemcomposerbinary
Deployment statuses
Each deployment entry in LogReader has one of three statuses:
| Status | Colour | When |
|---|---|---|
info |
Blue | Informational message — deploy started, intermediate step, debug output |
success |
Green | Deploy completed without errors |
failed |
Red | A step failed — the error message is included |
The script sends an info entry at the start, then either a success entry (with the commit message) or a failed entry (with the error) at the end. You can add extra info entries anywhere in the script for debugging.
Environment variables
Add these three variables to your .env file on the server you deploy from. The script reads them automatically — no hardcoded credentials in the script itself.
LOGREADER_URL=https://logreader.dev
LOGREADER_APP_ID=1
LOGREADER_TOKEN=your-app-token
| Variable | Description |
|---|---|
LOGREADER_URL |
Base URL of your LogReader instance, without a trailing slash |
LOGREADER_APP_ID |
The numeric ID of your app — visible in the Apps overview in your dashboard |
LOGREADER_TOKEN |
The app's API token — also found in the Apps overview |
LogReader notifications are silently skipped if any of these variables are empty, so the script is safe to use without them configured.
The deploy script
Create a file called deploy.sh in your project root and add it to .gitignore so credentials loaded from .env are never committed. Adjust the PHP, COMPOSER, and GIT_BRANCH variables at the top to match your server.
#!/bin/bash
# ── Configuration ─────────────────────────────────────────────────────────────
PHP=/usr/local/php84/bin/php # path to PHP binary (use: which php)
COMPOSER=/usr/local/bin/composer # path to Composer binary
GIT_BRANCH=main # branch to deploy
LOGREADER_URL=
LOGREADER_APP_ID=
LOGREADER_TOKEN=
APP_DIR=$(cd "$(dirname "$0")" && pwd)
if [ -f "$APP_DIR/.env" ]; then
LOGREADER_URL=$(grep -m1 '^LOGREADER_URL=' "$APP_DIR/.env" | cut -d= -f2-)
LOGREADER_APP_ID=$(grep -m1 '^LOGREADER_APP_ID=' "$APP_DIR/.env" | cut -d= -f2-)
LOGREADER_TOKEN=$(grep -m1 '^LOGREADER_TOKEN=' "$APP_DIR/.env" | cut -d= -f2-)
fi
# ──────────────────────────────────────────────────────────────────────────────
set -euo pipefail
notify_logreader() {
local status="$1"
local message="${2:-}"
if [ -n "$LOGREADER_URL" ] && [ -n "$LOGREADER_APP_ID" ] && [ -n "$LOGREADER_TOKEN" ]; then
local logreader_status="$status"
[ "$status" = "fail" ] && logreader_status="failed"
curl -s -X POST "${LOGREADER_URL}/api/apps/${LOGREADER_APP_ID}/deployments" \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer ${LOGREADER_TOKEN}" \
-d "{\"status\":\"${logreader_status}\",\"message\":\"${message}\",\"branch\":\"${GIT_BRANCH}\"}" \
> /dev/null || true
fi
}
maintenance_up() {
"$PHP" artisan up 2>/dev/null || true
}
fail() {
echo "ERROR: $1" >&2
maintenance_up
notify_logreader fail "$1"
exit 1
}
cd "$APP_DIR"
notify_logreader info "Deploy started."
echo "==> Enabling maintenance mode"
"$PHP" artisan down || fail "artisan down failed"
echo "==> Git pull (branch: $GIT_BRANCH)"
git pull origin "$GIT_BRANCH" || fail "git pull failed"
COMMIT_MSG=$(git log -1 --pretty=%s 2>/dev/null | sed 's/\\/\\\\/g; s/"/\\"/g' || true)
echo "==> Composer install"
"$PHP" composer.phar install --no-interaction --prefer-dist --optimize-autoloader 2>/dev/null \
|| "$PHP" "$COMPOSER" install --no-interaction --prefer-dist --optimize-autoloader \
|| fail "composer install failed"
echo "==> Migrations"
"$PHP" artisan migrate --force || fail "migrate failed"
echo "==> Clearing cache"
"$PHP" artisan cache:clear || fail "cache:clear failed"
"$PHP" artisan config:clear || fail "config:clear failed"
"$PHP" artisan route:clear || fail "route:clear failed"
"$PHP" artisan view:clear || fail "view:clear failed"
echo "==> Building cache"
"$PHP" artisan config:cache || fail "config:cache failed"
"$PHP" artisan route:cache || fail "route:cache failed"
echo "==> Disabling maintenance mode"
"$PHP" artisan up || fail "artisan up failed"
sleep 5
notify_logreader success "${COMMIT_MSG:-Deploy completed successfully.}"
echo "==> Deploy complete"
File permissions
The script needs to be executable. Run this once on your server (use the full path — relative paths can fail depending on how the script is invoked):
chmod +x /home/youruser/domains/example.com/laravel/deploy.sh
If you're tracking the script in git, mark it executable locally so the permission is preserved:
chmod +x deploy.sh
GitHub Actions
The simplest approach is to SSH into your server and run the script directly. Add your SSH credentials as repository secrets, then create .github/workflows/deploy.yml:
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy via SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: bash ~/domains/example.com/laravel/deploy.sh
The required secrets are SSH_HOST(IP-adres from your server), SSH_USER, and SSH_PRIVATE_KEY. Add them in your repository under Settings → Secrets and variables → Actions.
Bitbucket Pipelines
The approach is the same — SSH into your server and run the script. Add SSH_HOST, SSH_USER, and SSH_PRIVATE_KEY as repository variables under Repository settings → Pipelines → Repository variables, then create bitbucket-pipelines.yml:
image: atlassian/default-image:4
pipelines:
branches:
main:
- step:
name: Deploy
deployment: production
script:
- pipe: atlassian/ssh-run:0.4.1
variables:
SSH_USER: $SSH_USER
SERVER: $SSH_HOST
COMMAND: bash ~/domains/example.com/laravel/deploy.sh
SSH_KEY: $SSH_PRIVATE_KEY
Extra steps
The script covers the most common Laravel deploy steps. Add or remove steps to match your application. Some common additions:
Restart Laravel Horizon:
echo "==> Restarting Horizon"
"$PHP" artisan horizon:terminate || fail "horizon:terminate failed"
Restart Laravel Reverb (WebSockets):
echo "==> Restarting Reverb"
pkill -f "reverb:start" || true
nohup "$PHP" artisan reverb:start --host=127.0.0.1 --port=8080 >> storage/logs/reverb.log 2>&1 &
Run npm build:
echo "==> Building assets"
npm ci && npm run build || fail "npm build failed"
Debug messages
You can send additional info entries at any point in the script to track intermediate steps or add context. Each call creates a new timeline entry in LogReader:
# After migrations
notify_logreader info "Migrations complete."
# After cache rebuild
notify_logreader info "Cache rebuilt."
This is useful for long-running deploys where you want to confirm each step completed, or to narrow down which step failed when debugging a production issue.