
Zero-downtime deployment comes down to one idea: never modify the directory Nginx is serving. Instead, build the new version of your app in a fresh directory next to the live one, and switch traffic over with a single atomic symlink swap that takes microseconds.
The naive alternative — SSH in, run git pull, then composer install in the live directory — leaves your app broken for the entire duration of the deploy. While Composer swaps out the vendor directory, autoloaded classes go missing mid-request. While new migrations run against old code, requests hit mismatched schemas. On a slow deploy that window is thirty seconds to several minutes of 500 errors, and it happens on every single release.
Running php artisan down first turns the error page into a prettier 503, but it is still downtime. The structure below removes the window entirely, and once scripted it is no more work per release than the pull-based deploy was.
The layout has three parts: a releases directory holding one timestamped copy of the app per deploy, a shared directory holding everything that must survive between deploys, and a current symlink that always points at the live release. Nginx serves current/public and never knows a deploy happened.
The swap itself must be atomic. A plain ln -sfn appears atomic but actually deletes and recreates the link, leaving a tiny gap. The standard trick is to create the new link under a temporary name and rename it over the old one — rename() is atomic at the filesystem level, so a request either sees the old release or the new one, never nothing.
Here is the whole flow as a single script. It clones the release, installs dependencies, links shared resources, warms Laravel's caches, runs migrations, and only then swaps the symlink:
Tools like Deployer implement exactly this recipe with hooks and rollback built in, so you do not have to maintain the script yourself. If you host on CloudStick, its Git deployment feature covers the trigger side: connect the repository to the web application and a push to your production branch starts the deploy on the server — combined with this release structure, shipping becomes git push and nothing else. CloudStick's Web Application Clone is also the easy way to keep a staging copy of the site where you can rehearse a risky release before it touches production.
The symlink swap is not the last step — two caches still point at the old release. OPcache resolves symlinks when it caches a file, so PHP-FPM keeps executing the previous release's code until it is reloaded. And queue workers are long-running processes that booted the old code and will keep running it forever.
Use reload, not restart — reload spawns fresh workers with an empty OPcache while letting in-flight requests finish on the old ones, which is what keeps this step downtime-free. queue:restart sets a flag in the cache that tells each worker to exit cleanly after its current job; Supervisor then boots replacements that load the new release. If you skip either step, the deploy looks fine in the browser while background jobs quietly run week-old code.
The rollback story is the quiet payoff of this whole structure: because the previous release still exists on disk, reverting a bad deploy is pointing current back at it and reloading PHP-FPM — a few seconds, no git archaeology under pressure. Keep the last four or five releases and prune older ones at the end of each deploy (ls -dt releases/* | tail -n +6 | xargs rm -rf) so the disk does not fill with dead copies.
To adopt this on an existing site, do it in one maintenance window: create the releases and shared directories, move the live app in as the first release, move .env and storage into shared/, create the symlinks, and repoint the web server's document root at current/public. Every deploy after that one is invisible to your users.
Then script it — the script above, Deployer, or a Git-push trigger — because a zero-downtime process only works when it runs identically every time. The releases structure, the shared symlinks, the atomic swap, and the FPM reload: four pieces, and none of them are optional.

