Skip to content
Mehedi Hasan Sagor

// blog

Zero‑Downtime Deploys and Observability: Production‑Ready Laravel Practices

Mehedi Hasan Sagor Oct 5, 2026 5 min read
Zero‑Downtime Deploys and Observability: Production‑Ready Laravel Practices

Why production matters

Running a Laravel app in development is forgiving – you can clear caches, restart the server, and roll back code at will. In production, every second of downtime costs revenue, user trust, and team morale. The goal of a production‑ready Laravel stack is simple: deliver new features reliably, keep the system observable, and be able to revert instantly if something goes wrong.

Zero‑downtime deploys with Laravel

The most common source of outage is a server restart while new code is being copied. Modern CI/CD platforms (GitHub Actions, GitLab CI, Bitbucket Pipelines) can automate a zero‑downtime flow.

  1. Build the artifact – compile assets, run tests, and create a versioned tarball.
  2. Push to a staging directory – keep the current live symlink untouched.
  3. Run pre‑deploy checks – lint, static analysis, and php artisan config:cache.
  4. Swap the symlink – point the web root to the new release.
  5. Run post‑deploy tasks – migrations, queue workers, cache warm‑up.

A minimal Bash script that follows this pattern on a Ubuntu server looks like:

#!/usr/bin/env bash
set -euo pipefail

RELEASE=$(date +%Y%m%d%H%M%S)
APP_DIR=/var/www/myapp
RELEASES_DIR=$APP_DIR/releases
CURRENT_LINK=$APP_DIR/current

# 1. Build artifact (assume CI already did this and uploaded a tarball)
mkdir -p $RELEASES_DIR/$RELEASE
tar -xzf myapp-$RELEASE.tar.gz -C $RELEASES_DIR/$RELEASE

# 2. Install dependencies
cd $RELEASES_DIR/$RELEASE
composer install --no-dev --optimize-autoloader
npm ci && npm run prod

# 3. Cache configuration & routes
php artisan config:cache
php artisan route:cache
php artisan view:cache

# 4. Atomically switch symlink
ln -sfn $RELEASES_DIR/$RELEASE $CURRENT_LINK

# 5. Run migrations safely (see next section)
php artisan migrate --force

# 6. Restart queue workers
php artisan queue:restart

# 7. Warm‑up cache (optional)
php artisan cache:clear && php artisan cache:forget routes

echo "Deploy $RELEASE completed successfully"

The key is the ln -sfn command – it swaps the current symlink instantly, so the web server starts serving the new code without a reload. Your web server (NGINX or Apache) should be configured to follow symlinks and keep existing PHP‑FPM workers alive until they finish the current request.

Safe database migrations without outages

Even with a flawless code swap, a poorly written migration can lock tables and bring the app to a halt. Follow the add‑column‑first, back‑fill, then drop pattern:

// 2024_10_05_000001_add_is_active_to_users.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration {
    public function up(): void
    {
        Schema::table('users', function (Blueprint $table) {
            $table->boolean('is_active')->default(true)->nullable(); // step 1: add nullable column
        });

        // step 2: back‑fill in a separate Artisan command or queued job
        // php artisan users:backfill-is-active

        Schema::table('users', function (Blueprint $table) {
            $table->boolean('is_active')->default(true)->change(); // make NOT NULL
        });
    }

    public function down(): void
    {
        Schema::table('users', function (Blueprint $table) {
            $table->dropColumn('is_active');
        });
    }
};

Tips

  • Run migrations with --step so Laravel creates a transaction for each batch, allowing partial rollbacks.
  • For large tables, prefer chunked updates in a background job rather than a single UPDATE statement.
  • Use the --lock option (php artisan migrate --lock=0) only when you are sure no other deployment is running.

Observability: logs, metrics, and tracing

A silent failure is more dangerous than a visible error page. Build three layers of insight:

Centralised logging

Laravel ships with Monolog. Replace the default single channel with a JSON‑formatted syslog that ships to Elastic, Loki, or CloudWatch.

// config/logging.php
return [
    'default' => env('LOG_CHANNEL', 'stack'),
    'channels' => [
        'stack' => [
            'driver' => 'stack',
            'channels' => ['json'],
            'ignore_exceptions' => false,
        ],
        'json' => [
            'driver' => 'monolog',
            'handler' => Monolog\Handler\SyslogUdpHandler::class,
            'formatter' => Monolog\Formatter\JsonFormatter::class,
            'handler_with' => [
                'host' => env('LOG_HOST', '127.0.0.1'),
                'port' => env('LOG_PORT', 514),
            ],
        ],
    ],
];

All exception traces now contain a request ID, making correlation easy.

Metrics collection

Expose Prometheus‑compatible metrics using the spatie/laravel-metrics package or a lightweight custom endpoint.

// routes/web.php
use Illuminate\Support\Facades\Route;
use Spatie\Prometheus\PrometheusFacade as Prometheus;

Route::get('/metrics', function () {
    $metrics = Prometheus::getMetricFamilySamples();
    return response($metrics, 200)
        ->header('Content-Type', Prometheus::TEXT_CONTENT_TYPE);
});

Track request latency, queue job failures, and DB connection pool usage. Grafana dashboards built on these metrics give you a real‑time health picture.

Distributed tracing

When your Laravel app talks to external services (payment gateways, AI APIs), add OpenTelemetry tracing.

// bootstrap/app.php (after creating $app)
use OpenTelemetry\SDK\Trace\TracerProvider;
use OpenTelemetry\SDK\Trace\SpanProcessor\BatchSpanProcessor;
use OpenTelemetry\Exporter\Jaeger\JaegerExporter;

$exporter = new JaegerExporter('my‑laravel‑service', 'http://jaeger:14268/api/traces');
$provider = new TracerProvider();
$provider->addSpanProcessor(new BatchSpanProcessor($exporter));
$tracer = $provider->getTracer('io.opentelemetry.contrib.php');

$app->instance('tracer', $tracer);

Wrap external HTTP calls with a span, and you’ll see end‑to‑end latency breakdowns in Jaeger or Zipkin.

Automated health checks and alerting

Expose a lightweight endpoint that returns 200 OK only when critical subsystems are healthy.

// routes/api.php
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\DB;

Route::get('/health', function () {
    try {
        DB::connection()->getPdo();
    } catch (Throwable $e) {
        return response()->json(['db' => 'down'], 503);
    }
    return response()->json(['status' => 'ok'], 200);
});

Configure UptimeRobot, Pingdom, or your cloud provider’s monitor to poll /health every minute. Hook the alert into Slack or PagerDuty so the on‑call engineer can act before users notice.

Rollback and incident response

Even with safeguards, things go wrong. A fast rollback reduces MTTR (Mean Time To Recovery).

  • Code rollback – keep the previous release directory and use the same symlink swap to revert:
    ln -sfn $RELEASES_DIR/20241004_150300 $CURRENT_LINK
    
  • Database rollback – Laravel’s migrations are reversible. Run php artisan migrate:rollback --step=1 --force to undo the last batch.
  • Queue workers – php artisan queue:restart forces workers to pick up the previous code version.
  • Post‑mortem checklist – capture request ID, error logs, metric spikes, and trace IDs in a shared document. Over time the data reveals patterns that guide future improvements.

Takeaway checklist

  • Build releases as immutable tarballs and switch via an atomic symlink.
  • Write migrations that are additive, back‑filled, then destructive.
  • Ship JSON logs to a central store; tag every line with a request ID.
  • Export Prometheus metrics and enable OpenTelemetry tracing for critical paths.
  • Add a /health endpoint and wire it to an external monitor.
  • Keep the previous release on disk and practice one‑click rollbacks.

By treating deployment, migration, and observability as first‑class concerns, you turn a Laravel codebase into a resilient service that can keep pace with a fast‑moving startup.

  • Laravel
  • Devops
  • Deployment
  • Monitoring
  • Observability