Laravel zero-downtime deployment means shipping new code without a single failed request. The standard approach is to build each release in its own directory, prepare everything there (Composer dependencies, assets, caches, and migrations), and then switch a current symlink to the new release in one atomic step. Add backward-compatible migrations, graceful queue restarts, and a fast rollback process, and your users should never notice a deployment.

Here's a classic failure scenario. A developer runs git pull && composer install on a live server at 4 p.m. For about 40 seconds, the application serves a mix of old and new files. Part of the autoloader is missing, a few checkout requests return 500 errors, and support tickets arrive before the deployment finishes.

Every strategy below exists to prevent that window. You'll learn the core release pattern, see a working script, understand the migration rules most teams learn the hard way, and discover how Forge, Laravel Cloud, and Octane handle deployments.

Key Takeaways

  • Never update code in place. Build each release in a fresh directory and activate it with an atomic symlink swap.

  • Migrations are the real risk. Old code runs against the new schema temporarily, so every migration must be backward-compatible. Use the expand-and-contract pattern.

  • Restart long-running processes. Queue workers, Horizon, and Octane keep old code in memory until they're restarted gracefully.

  • Forge and Laravel Cloud handle much of the work for you. Forge enables zero-downtime deployments by default on new sites, while Cloud switches deployments without downtime.

  • Separate deployment from release. Feature flags let you deploy code without activating it immediately.

The developers in this article's examples are illustrative composites based on common scenarios.

Why Laravel Deployments Cause Downtime

A deployment can break requests in four predictable ways:

  1. Partially updated files. git pull and composer install change files one by one while requests are still running.

  2. Schema mismatches. A migration renames or drops a column that the running code still uses.

  3. Stale processes. Queue workers and Octane workers load your code once and continue running the old version.

  4. Cache confusion. Cached configuration or routes from the old release may reference resources that no longer exist.

A good Laravel deployment strategy handles all four problems. Maintenance mode (php artisan down) avoids application errors by showing everyone a "back soon" page, but that isn't zero downtime. Reserve it for rare, risky changes.

Strategy 1: Atomic Releases with a Symlink Swap

This is the foundation of zero-downtime Laravel deployments and the approach used by Forge, Envoyer, and Deployer. The server layout looks like this:

/var/www/app/
├── current -> releases/20261015093000   # web server points here
├── releases/
│   ├── 20261014170000
│   └── 20261015093000
└── shared/
    ├── .env
    └── storage/

Your web server's document root is /var/www/app/current/public. Each deployment creates a new folder in releases/, links the shared .env and storage directories, builds everything, and only then repoints current. Requests already in progress can finish on the old release, while new requests use the new one.

Here's a minimal script you can adapt:

#!/usr/bin/env bash
set -euo pipefail  # stop on the first error, so a failed step never goes live

APP=/var/www/app
RELEASE="$APP/releases/$(date +%Y%m%d%H%M%S)"

# If any step fails before the switch, delete the half-built release
trap 'rm -rf "$RELEASE"' ERR

# 1. Fresh copy of the code (use a read-only deploy key)
git clone --depth 1 --branch main [email protected]:your-org/your-app.git "$RELEASE"
cd "$RELEASE"

# 2. Shared state lives outside releases
ln -s "$APP/shared/.env" .env
rm -rf storage && ln -s "$APP/shared/storage" storage

# 3. Build inside the new release, while the old one keeps serving traffic
composer install --no-dev --optimize-autoloader --no-interaction
npm ci && npm run build
php artisan optimize          # caches config, routes, views and events
php artisan storage:link      # public/storage is not in Git, so recreate it
php artisan migrate --force   # must be backward compatible (see Strategy 2)

# 4. Atomic switch: create a temp link, then rename it over "current"
ln -sfn "$RELEASE" "$APP/current_tmp"
mv -Tf "$APP/current_tmp" "$APP/current"
trap - ERR   # the new release is live now, so never auto-delete it

# 5. Restart long-running workers so they load the new code
php artisan queue:restart

# 6. Keep the five newest releases (by name), never deleting the active one
ACTIVE="$(readlink -f "$APP/current")"
find "$APP/releases" -mindepth 1 -maxdepth 1 -type d | sort -r | tail -n +6 \
  | { grep -vxF "$ACTIVE" || true; } | xargs -r rm -rf

A few production notes:

  • mv -T is the atomic part. ln -sfn on its own removes and recreates the link, leaving a tiny gap. Renaming a temporary link over the existing one avoids that gap.

  • PHP-FPM and symlinks. OPcache and PHP's realpath cache can continue resolving current to the old folder. In Nginx, set fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; and fastcgi_param DOCUMENT_ROOT $realpath_root; after include fastcgi_params; so those values aren't overridden. Alternatively, reload PHP-FPM after the switch.

  • Prepare shared storage once. Create the complete shared/storage directory tree (app, framework/cache, framework/sessions, framework/views, and logs). Ensure storage and bootstrap/cache are writable by the PHP-FPM user.

  • Keep secrets out of Git. Store .env only in shared/, with permissions restricted to the deployment user.

  • Use a non-root deployment user. Grant it only the permissions it needs, such as permission to reload PHP-FPM.

For the server setup itself, see our guide to deploying Laravel to a VPS over SSH.

Want deployments handled for you? StackMivo generates your Laravel app and deploys it with one click, handling migrations, Vite builds, queue restarts, and health checks, with zero downtime and automatic rollback on failure.

Strategy 2: Backward-Compatible Migrations (Expand and Contract)

The symlink swap doesn't protect you from database issues. Migrations run before the switch, so for a short period, the old code runs against the new schema. During a rolling deployment across several servers, that window can last minutes.

The rule is simple: every migration must work with both the current and the next version of your code. Use the expand-and-contract pattern. Renaming name to full_name takes three deployments:

  1. Expand. Add a nullable full_name column. Update the code to write to both columns and read from full_name, falling back to name. Backfill existing rows with a queued job.

  2. Migrate reads. Once the backfill is complete, read only from full_name.

  3. Contract. In a later deployment, drop name.

Other safe-migration habits:

  • Add columns as nullable or with defaults. Avoid adding required columns without defaults.

  • Never drop or rename a column in the same deployment that removes its use from the application code.

  • Watch large tables. Adding an index or changing a column type on millions of rows can lock the table. Test against a production-sized copy first, and use your database's online schema-change tools when necessary.

  • Run migrations once. On multiple servers, run php artisan migrate --force --isolated from a single deployment step to prevent two servers from migrating simultaneously. This requires all servers to share a central cache server.

Nadia runs a two-person agency in Nairobi. Her team once renamed a column and deployed the code in the same release. The migration ran, and for 90 seconds, the still-running old release threw errors on every order page. They now ship every schema change using expand-and-contract across two releases and haven't had a deployment-related error since.

Strategy 3: Restart Queue Workers, Horizon, and the Scheduler

Queue workers are long-running processes. They load your application once at startup and continue running the old code until they're restarted. If you're new to workers, see our Laravel queues and workers guide.

  • Queue workers: php artisan queue:restart tells each worker to exit gracefully after its current job. Your process manager, usually Supervisor, starts it again using the new release. The restart signal is stored in the cache, so your cache driver must be shared across servers.

  • Horizon: Run php artisan horizon:terminate. Horizon finishes its current jobs and exits, after which Supervisor brings it back.

  • Scheduler: If you run schedule:run from cron, each run already uses whatever current points to. If you define sub-minute tasks, run php artisan schedule:interrupt after the switch so an in-progress schedule:run stops using the old code.

  • One command on Laravel 13: php artisan reload restarts queue workers, Reverb, and Octane together. See the Laravel documentation.

Remember that queued jobs serialize their payloads. If you change a job's constructor, jobs queued by the old release may fail when processed by the new code. Unserializing a job doesn't call its constructor, so a constructor default won't help. Add new properties with declared default values, or create a new job class and let the old one drain.

Strategy 4: Octane Deployments

Laravel Octane keeps your application in memory between requests, so a symlink swap alone won't reload it. Run php artisan octane:reload after the new code goes live to restart the workers gracefully.

If Octane was started from a specific release folder rather than current, a reload may continue serving that folder. Test your setup and restart the Octane process through Supervisor if needed.

Forge's documentation is explicit here: don't use Forge's zero-downtime deployment option with Octane, because Octane already handles graceful restarts and the two approaches can interfere with each other. See the Forge documentation.

Our Octane performance guide covers the setup.

Strategy 5: Rolling Deploys Across Multiple Servers

Once you run two or more application servers behind a load balancer, a Laravel rolling deployment updates them one at a time:

  1. Run migrations once, from a single server or CI step using migrate --isolated.

  2. Remove server 1 from the load balancer and allow in-flight requests to finish.

  3. Deploy the new release, run a health check, and add the server back to the load balancer.

  4. Repeat for each remaining server.

Because old and new code serve traffic side by side for several minutes, backward-compatible migrations from Strategy 2 are essential. They're the foundation of a safe rolling deployment.

Blue-green deployment takes this approach further: run two complete environments, deploy to the idle environment, test it, and then switch all traffic over. It requires double the infrastructure during the switch, but rollback is as simple as switching back.

Strategy 6: Let the Platform Do It

You don't have to script everything yourself.

Option

How it achieves zero downtime

Laravel Forge

Uses a releases directory and a current symlink, with the $CREATE_RELEASE(), $ACTIVATE_RELEASE(), and $RESTART_QUEUES() macros. Enabled by default on new sites, it keeps the last four releases and shares .env automatically.

Laravel Cloud

Builds an image, runs your build and deployment commands, and then gracefully terminates the old deployment while bringing the new one online without downtime.

Deployer

An open-source PHP deployment tool with a Laravel recipe that supports shared storage and .env, artisan:optimize, migrations, an atomic publish step, and artisan:reload.

Envoyer

Laravel's dedicated zero-downtime deployment service, which also integrates with Forge.

One Forge limitation to keep in mind: zero-downtime deployments must be enabled when you create the site. They can't be added to an existing site later. See the Forge documentation. To switch an older site, create a new site with the option enabled and move traffic over.

On Laravel Cloud, the same backward-compatible migration rules still apply because deployment commands, such as migrations, run before the old deployment is terminated. See our Laravel Cloud pricing guide if you're comparing hosting costs.

Strategy 7: Separate Deployment from Release with Feature Flags

The safest deployment is one where nothing visible changes immediately. Ship new code behind a feature flag, deploy it without activating the feature, and then enable it for 5% of users before rolling it out to everyone. If something breaks, switch the flag off instead of rolling back the entire release.

Laravel Pennant provides a first-party approach to feature flags. Our Laravel Pennant feature flags guide covers percentage rollouts and kill switches.

Rollbacks and Health Checks

Zero downtime isn't just about the happy path. You also need a plan for deployments that go wrong.

  • Rollback means repointing the symlink. When previous releases are kept on disk, rolling back means atomically switching to the previous folder, followed by queue:restart.

  • Don't automatically reverse migrations. Rolling back code is safe when migrations are backward-compatible. Running migrate:rollback in production can destroy data, so fix forward with a new migration instead. Take a database backup before any risky change.

  • Run a health check after every deployment. Check a /up endpoint (Laravel's default health route) or a key application page. Forge can ping your application from several regions after each deployment and notify you if a check fails.

  • Gate deployments in CI. Run your test suite before anything reaches production. Our Laravel CI/CD pipeline guide shows a GitHub Actions setup that deploys only successful builds.

Tom manages a Laravel SaaS for a logistics firm in Rotterdam. A bad deployment once broke invoice PDFs on a Friday afternoon. Because his last five releases were still on disk, he repointed current to the previous release in under ten seconds, restarted the queues, and fixed the bug on Monday without a single customer noticing.

Laravel Zero-Downtime Deployment Checklist

  • Each deployment builds in a new release directory.

  • .env and storage are shared, not copied.

  • Composer, assets, and php artisan optimize run before the switch.

  • Migrations are backward-compatible and run once using --isolated.

  • The current symlink swap is atomic.

  • Nginx uses $realpath_root, or PHP-FPM reloads after the switch.

  • Queue workers, Horizon, and Octane restart gracefully (php artisan reload on Laravel 13).

  • At least three to five previous releases are kept for rollback.

  • A health check runs after every deployment.

  • Risky features are released behind feature flags.

Skip the deployment scripting. Describe your app in plain English, and StackMivo generates the Laravel code and deploys it to Laravel Cloud, Forge, Ploi, or any SSH server with zero downtime. Start free with StackMivo →

Frequently Asked Questions

What is zero-downtime deployment in Laravel?

It's a deployment process where users don't encounter errors or a maintenance page while new code goes live. In Laravel, this usually means building each release in its own directory and then switching a current symlink to it in one atomic step.

Does Laravel Forge support zero-downtime deployments?

Yes. Forge enables zero-downtime deployments by default for new sites, using a releases directory and symlink. The option can't be added to an existing site, and Forge recommends not using it with Octane.

How do I run migrations without downtime in Laravel?

Make every migration backward-compatible with the code currently running. Add columns as nullable, and split renames and drops across separate deployments using the expand-and-contract pattern.

Do I need to restart queue workers after deploying?

Yes. Workers keep old code in memory. Run php artisan queue:restart, or php artisan horizon:terminate if you use Horizon, after the new release is live.

What is a rolling deployment in Laravel?

A rolling deployment updates servers behind a load balancer one at a time, ensuring that some servers continue serving traffic. It requires backward-compatible migrations because old and new code run side by side.

How do I roll back a Laravel deployment?

Point the current symlink back to the previous release and restart your queue workers. Avoid running migration rollbacks in production; fix forward instead.

The Bottom Line

Zero-downtime deployment in Laravel comes down to a few habits: build releases in isolation, switch them atomically, keep migrations backward-compatible, restart long-running workers, and always have a one-step rollback ready.

Forge, Laravel Cloud, and Deployer provide much of this functionality out of the box. The migration discipline is still your responsibility.

Get these practices right, and deployments stop being major events. They become something you can do ten times a day without anyone noticing.

Build and deploy your Laravel app with StackMivo, free to start →