Guides

Laravel Zero-Downtime Deployment: Ship Updates Without Breaking Your Site

Muhammad SaadMay 10, 20266 min read
Laravel Zero-Downtime Deployment: Ship Updates Without Breaking Your Site

Following laravel deployment best practices means your users should never see a broken page during an update. Yet most tutorials show you running git pull, composer install, and php artisan migrate directly on production — a process that can leave your site broken for 30-60 seconds on every deploy. This guide covers zero-downtime deployment techniques that keep your Laravel app running smoothly while you ship changes.

Why Standard Deployments Cause Downtime

A typical deployment script runs these steps sequentially on your live server:

git pull origin main
composer install --no-dev
php artisan migrate --force
php artisan config:cache
npm run build
php artisan queue:restart

During this process, several things can go wrong:

  • composer install temporarily removes autoload files — requests fail with class-not-found errors
  • npm run build takes 10-30 seconds — old assets are gone, new ones aren't ready
  • Migrations might lock database tables — queries queue up or timeout
  • Config cache is cleared before the new one is built — your app reads raw .env files briefly

Each of these creates a window where your site is partially or fully broken.

The Symlink Strategy: How Zero-Downtime Works

The core idea behind zero-downtime deployment is simple: build everything in a new directory, then switch to it atomically.

/var/www/
├── releases/
│   ├── 20260315_001/    ← previous release
│   ├── 20260315_042/    ← current live release
│   └── 20260316_011/    ← new release (being built)
├── shared/
│   ├── .env
│   ├── storage/
│   └── node_modules/
└── current → releases/20260315_042/   ← symlink (atomic switch)

Your Nginx points to /var/www/current/public. When the new release is fully built and tested, you swap the symlink — a single atomic filesystem operation that takes microseconds.

Setting Up Zero-Downtime Laravel Deployment

Step 1: Create the Directory Structure

sudo mkdir -p /var/www/{releases,shared/storage}
sudo chown -R www-data:www-data /var/www/

Move your .env file and storage directory to the shared location:

mv /var/www/current/.env /var/www/shared/.env
mv /var/www/current/storage/* /var/www/shared/storage/

Step 2: The Zero-Downtime Deploy Script

#!/bin/bash
set -e

APP_DIR="/var/www"
REPO="git@github.com:youruser/yourapp.git"
RELEASE="$(date +%Y%m%d_%H%M%S)"
RELEASE_DIR="$APP_DIR/releases/$RELEASE"
SHARED_DIR="$APP_DIR/shared"

echo "📦 Creating release: $RELEASE"
git clone --depth 1 $REPO "$RELEASE_DIR"

echo "🔗 Linking shared resources..."
ln -sf "$SHARED_DIR/.env" "$RELEASE_DIR/.env"
rm -rf "$RELEASE_DIR/storage"
ln -sf "$SHARED_DIR/storage" "$RELEASE_DIR/storage"

echo "📥 Installing PHP dependencies..."
cd "$RELEASE_DIR"
composer install --optimize-autoloader --no-dev --no-interaction

echo "🏗️ Building frontend assets..."
npm ci --prefer-offline
npm run build

echo "🗄️ Running migrations..."
php artisan migrate --force

echo "⚡ Caching configuration..."
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache

echo "🔄 Switching symlink (zero-downtime moment)..."
ln -sfn "$RELEASE_DIR" "$APP_DIR/current"

echo "♻️ Reloading PHP-FPM..."
sudo systemctl reload php8.2-fpm

echo "🔄 Restarting queue workers..."
php artisan queue:restart

echo "🧹 Cleaning old releases (keeping last 5)..."
cd "$APP_DIR/releases"
ls -dt */ | tail -n +6 | xargs rm -rf

echo "✅ Deployed $RELEASE successfully!"

The critical line is ln -sfn — it creates a new symlink atomically, replacing the old one in a single operation. Your site never serves a partially-built release.

Step 3: Update Nginx Configuration

Point Nginx to the symlink, not a specific release:

server {
    listen 443 ssl;
    server_name yourdomain.com;
    root /var/www/current/public;

    # ... rest of your config
}

Since current is a symlink, Nginx follows it to whatever release is active. When the symlink changes, new requests hit the new release. If you need to learn more about the full Laravel deployment checklist, we have a detailed guide on production setup.

Handling Database Migrations Safely

Migrations are the trickiest part of zero-downtime deploys. A migration that drops a column will break the old release while the new one is being built.

The Expand-Contract Pattern

Split breaking changes into two deployments:

Deploy 1 (Expand): Add new columns/tables without removing old ones

// Migration: Add new_email column alongside email
Schema::table('users', function (Blueprint $table) {
    $table->string('new_email')->nullable()->after('email');
});

Deploy 2 (Contract): Remove old columns after all code uses the new ones

// Only after Deploy 1 is live and working
Schema::table('users', function (Blueprint $table) {
    $table->dropColumn('email');
    $table->renameColumn('new_email', 'email');
});

This pattern ensures both old and new code can run simultaneously during the transition.

Migration Timeout Protection

Long-running migrations can lock tables. Add a timeout:

public function up()
{
    DB::statement('SET lock_wait_timeout = 5');
    
    Schema::table('orders', function (Blueprint $table) {
        $table->index('created_at');
    });
}

Automated Rollback

One major advantage of the symlink approach: instant rollbacks.

#!/bin/bash
# rollback.sh — Switch to previous release
APP_DIR="/var/www"
PREVIOUS=$(ls -dt "$APP_DIR/releases"/*/ | sed -n '2p')

if [ -z "$PREVIOUS" ]; then
    echo "❌ No previous release found"
    exit 1
fi

echo "⏪ Rolling back to: $PREVIOUS"
ln -sfn "$PREVIOUS" "$APP_DIR/current"
sudo systemctl reload php8.2-fpm
php artisan queue:restart
echo "✅ Rollback complete"

One command and your site is back on the previous working version — no git revert, no redeployment. For complex deployments, having a staging environment to test changes before production is equally important.

Tools That Automate This

If you prefer not to maintain custom scripts, several tools implement these laravel deployment best practices automatically:

Laravel Envoyer ($12/month) — Built by the Laravel team. Handles symlink deployments, health checks, and rollbacks with a clean UI.

Deployer (free, open source) — PHP-based deployment tool with built-in Laravel recipes:

# Install
composer global require deployer/deployer

# Initialize Laravel recipe
dep init --template=Laravel

GitHub Actions + Custom Script — Free CI/CD that runs your deploy script on push:

name: Deploy
on:
  push:
    branches: [main]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Deploy via SSH
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: deploy
          key: ${{ secrets.SSH_KEY }}
          script: /var/www/deploy.sh

FAQ

What is zero-downtime deployment in Laravel?

Zero-downtime deployment uses a symlink strategy to build new releases in a separate directory and switch to them atomically. Users never see a broken or partially-deployed application.

Can I do zero-downtime deploys on shared hosting?

No. You need SSH access and the ability to create symlinks, which requires VPS or dedicated hosting. Shared hosting doesn't provide this level of control.

How do I handle storage files across releases?

Use a shared storage directory symlinked into each release. This ensures uploaded files, logs, and cached data persist across deployments without duplication.

What if a migration fails during deployment?

The old symlink remains active, so your site keeps running on the previous release. Fix the migration, test it locally or on staging, and redeploy. The failed release directory can be deleted.

Deploy Laravel with Zero Downtime on DeployBase

Implementing laravel deployment best practices with zero-downtime deploys requires a server you can fully control. At DeployBase, our VPS plans give you root SSH access, NVMe SSD storage for fast builds, and the flexibility to set up symlink-based deployments exactly as described above. Starting at $5/month with 24/7 support.

Get your VPS at DeployBase → — professional hosting for professional deployments.

Share this article

Muhammad Saad

Muhammad Saad

DeployBase Team

Ready to Get Started?

Join thousands of developers who trust DeployBase for their hosting needs.