The naive Laravel deploy is git pull on the live directory, and it has a window where the code is half-old, half-new - composer autoloads one version, the templates reference another, and a visitor in that second gets a broken page. Zero-downtime deploys remove the window with one idea: never change the directory the web server is looking at.
Releases and one symlink#
The structure, used by every deploy tool worth using:
/var/www/app/
releases/
20261020-141122/
20261021-093015/
current -> releases/20261021-093015
shared/
.env
storage/
Each deploy is a full checkout into a new timestamped directory. Composer install, artisan config:cache, asset builds - all happen there, while current still points at the previous release and the site keeps serving it. The switch is a single atomic operation: repoint the current symlink. If anything is wrong, point it back - rollback takes one second because the old release directory is still intact.
Two things must live outside releases: .env and storage/. Symlink them in from shared/ so every release uses the same environment and the same uploaded files.
Migrations are the hard part#
Code swaps are easy; databases cannot be symlinked back. The rule that keeps deploys reversible: every migration must work with both the old and the new code. Adding a column is fine. Renaming one is not - do it as three deploys (add new, backfill and dual-write, drop old). Run php artisan migrate --force before the symlink swap, so the schema is ready for the release that is about to go live.
The queue and scheduler#
Laravel queues hold serialized jobs, so after the swap, restart the workers (artisan queue:restart) or they keep running the old code from memory. The scheduler needs no restart - cron resolves through current on each run.
That is the whole shape: build beside, switch atomically, migrate compatibly, restart workers. Whether you script it yourself or adopt a tool, Deploy Laravel With Git Push covers the pipeline around it, and Deploy With Tags, Not Branches decides what each release directory should contain. A deploy your users never notice is not luxury - it is just structure.
Share this post
Keep reading
A First CI Pipeline for a Laravel Project
Install, lint, test, build - four jobs that run on every pull request and turn 'works on my machine' into a check everyone can see.
A Staging Environment Worth Having
Staging only catches what it shares with production. Same deploy path, same services, sanitized data - and a clear rule for what it is for.
Branch Naming Conventions That Scale Past Three People
Branch names are a user interface for your team's work. The conventions that survive growth, the ones that collapse, and why boring always wins.