Deploying Laravel Apps with Zero Downtime
Deploying a Laravel application without downtime is achievable with the right process. The naive approach — SSH in, git pull, php artisan migrate, restart — causes 5–30 seconds of errors for live users during every deployment. Here's how to eliminate that.
Why Naive Deployments Cause Downtime
During a standard deployment, three things happen that break the running application: the codebase is in a mixed state while files are being updated (half old, half new), migrations run against a database that the old code is still querying, and caches contain stale compiled views and config from the old release.
The solution is to separate these concerns and make the transition atomic.
The Atomic Deployment Pattern
The standard approach is the "releases" directory pattern. Instead of updating files in place, each deployment goes into a timestamped directory alongside previous releases. A symlink points to the current release. Switching versions is a single atomic symlink swap.
Your directory structure looks like this: a releases/ directory containing timestamped release folders, a current symlink pointing to the active release, and a shared/ directory for files that persist across deployments (storage, .env, uploads).
The deployment steps are: create a new release directory, clone or rsync new code into it, symlink shared directories (storage, .env) into the new release, run composer install, run npm build, run migrations (more on this below), warm the cache, then atomically swap the current symlink to the new release. Nginx never stops serving — it just starts serving from the new directory.
Handling Migrations Safely
Migrations are the hardest part. Running php artisan migrate mid-deployment can break the old code that's still handling requests. The solution is backward-compatible migrations: never drop a column or table in the same release that removes the code using it. Instead, deploy in two steps — first deploy the code that tolerates both the old and new schema, then deploy the migration that cleans up.
For most projects, a simpler approach works: run migrations before the symlink swap, not after. Your new code handles requests immediately after the migration, and the old code (which ran against the pre-migration schema) is no longer serving requests. This works as long as your migrations don't break the old schema.
Laravel Envoyer and Forge
Laravel Envoyer implements the atomic deployment pattern for you. You configure your server, connect your Git repository, and Envoyer handles releases, health checks, and rollbacks. It's $10/month and worth it for production applications where uptime matters.
Laravel Forge provisions servers and can deploy with zero downtime out of the box. If you're on Forge, enable the "zero downtime deployment" option in your site settings — it uses the symlink pattern internally.
Our Deployment Setup
For our production systems, we use a combination: a deploy.sh script that handles the git pull, composer install, and cache clearing, with Nginx configured to serve from the current symlink. For larger projects, Envoyer handles the release management. Queue workers are restarted gracefully using php artisan queue:restart which waits for in-progress jobs to complete before stopping.
The result: deployments that take 30–60 seconds and cause zero visible downtime for users.
Need software built?
Tell us what you need. We respond within 24 hours with a realistic quote.