Zero-Downtime Laravel Deployments Without Drama

Zero-downtime is less about a fancy tool and more about never pointing traffic at a half-finished release.

Deploys that require a maintenance window train a team to deploy rarely, and rare deploys are big, risky deploys. Zero-downtime deployment breaks that loop. The mechanics are not complicated; the discipline is in never letting live traffic see a release that is not fully assembled.

Atomic releases via symlink swap

Build each release into its own timestamped directory, fully — code, dependencies, compiled assets — while the current release keeps serving. When it is complete, flip a current symlink to the new directory. The web server always points at current, so the switch is atomic: one instant it is the old release, the next it is the new one, with nothing in between.

releases/
  2026-07-10-101500/   # new build, assembled in full
  2026-07-09-183000/   # previous, kept for rollback
current -> releases/2026-07-10-101500   # atomic flip
shared/                # .env, storage, persist across releases

Rollback is then just flipping the symlink back to the previous directory — seconds, not a rebuild. Keep .env, storage and other stateful paths in a shared directory symlinked into each release so nothing important lives inside a release you might delete.

Migrations are where downtime hides

An atomic code swap does not save you if a migration locks a large table or if new code hits an old schema. The fix is expand-then-contract: make schema changes backward-compatible, deploy code that works with both shapes, then remove the old shape in a later step.

  • Add columns as nullable or with a default; never rename in place while old code runs
  • Backfill data in batched jobs, not in the migration that blocks the deploy
  • Split a rename into add-new, dual-write, migrate-reads, drop-old across deploys
  • Watch for implicit locks on large tables; on big data, add indexes concurrently
A migration that takes a table lock is a maintenance window in disguise, no matter how atomic your symlink swap is.

Order of operations for caches and workers

Laravel caches config, routes and views for speed, which means a deploy has to rebuild them at the right moment — against the new release, after the swap, not before. And queue workers hold old code in memory until restarted, so a deploy that does not restart them keeps running yesterday’s job code against today’s schema.

  • Rebuild config/route/view caches for the new release as part of the build step
  • Restart or gracefully signal queue workers so they pick up the new code
  • Let in-flight jobs finish rather than killing them mid-run

Tools are the easy part

Deployer, Envoy, or a CI pipeline all implement this pattern; the tool matters less than getting the sequence right. Whatever runs it, the steps are the same: build the release in isolation, run non-blocking migrations, warm caches, flip the symlink, restart workers, and keep the last few releases for instant rollback.

The payoff

When a deploy is atomic and rollback is one command, shipping stops being an event. You deploy small changes often, you deploy them in daylight, and when something is wrong you flip back in seconds instead of debugging a broken production. That change in cadence is worth more than any single performance tweak.

From the same work

Still deploying with your fingers crossed?