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 pipefail for safe error handling
  • curl — for sending deployment status to LogReader
  • git — for pulling the latest code
  • PHP — adjust the PHP variable to point to your binary (e.g. /usr/local/php84/bin/php)
  • Composer — the script tries composer.phar first, then falls back to the system composer binary

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.