The Anxiety of the 'Deploy' Button
We have all been there. It is 3 PM on a Tuesday, your codebase is ready, and you need to push an update to your live production server. If your deployment process involves pulling code directly into your live web directory, running composer install, and executing migrations while users are actively navigating your app, you are playing with fire.
During those 30 to 60 seconds of deployment, users might hit broken routes, encounter missing dependencies, or face database lockups. Professional server administration demands a better way. Enter Zero-Downtime Deployments.
How Zero-Downtime Deployments Work
The core concept behind zero-downtime deployment is that you never alter the directory your web server (Nginx or Apache) is actively serving. Instead, you use a symlink (symbolic link) strategy.
Here is the architectural breakdown of the server directory structure:
/var/www/my-app/releases/- This directory holds multiple folders, one for every deployment you make (named by timestamp, e.g.,20260307101500)./var/www/my-app/shared/- This holds files that must persist across deployments, specifically your.envfile and yourstorage/directory (where user uploads live)./var/www/my-app/current- This is not a real folder. It is a symlink pointing to the latest directory inside thereleases/folder.
Your Nginx configuration is set to serve from /var/www/my-app/current/public.
The Deployment Flow
When you trigger a deployment (via a CI/CD pipeline like GitHub Actions or a tool like Laravel Envoyer), the server executes the following sequence in the background:
- Clone the Code: A brand new folder is created in
releases/(e.g.,release_B). The new code is cloned here. - Install Dependencies:
composer installandnpm run buildare executed entirely insiderelease_B. The live app (pointing torelease_A) is completely unaffected. - Link Shared Storage: The script creates symlinks from
release_B/storageto theshared/storagefolder, and links the shared.envfile. - Optimize: Laravel optimization commands (
route:cache,config:cache,view:cache) are run on the new release. - The Switch: This is the magic moment. In a fraction of a millisecond, the symlink at
/var/www/my-app/currentis updated to point fromrelease_Atorelease_B.
Traffic is instantly routed to the fully prepared, cached, and compiled new version of your application. The user experiences absolutely zero downtime.
Handling Database Migrations safely
While the code deployment is now seamless, database migrations remain tricky. If a migration drops a column that the old code (still running for a few active requests) relies on, the app will crash.
The golden rule for zero-downtime database management is to make your migrations additive-only during a deployment. If you need to drop a column or drastically change a schema, it must be done in multiple separate deployments:
- Deploy 1: Add the new column. Update the code to write to both the old and new columns, but read from the old.
- Deploy 2: Update the code to read from the new column.
- Deploy 3: Drop the old column safely.
Conclusion
Setting up a symlink-based deployment pipeline is a rite of passage for full-stack developers. It removes the stress from shipping code, allows you to deploy multiple times a day with confidence, and most importantly, ensures your users never see a maintenance page.