Every deployment is a risk. For a brief moment, your application is in transition — old code is being replaced with new code, caches are being cleared, services are being restarted. In a traditional deployment, that transition window means your users might see errors, experience timeouts, or encounter an application running a mix of old and new code simultaneously.
Zero-downtime deployment eliminates that window entirely. Your users never experience a disruption. One moment they're on the old version, the next they're on the new version, with no gap in between.
On Deploynix, zero-downtime deployment isn't a premium feature or an opt-in configuration. It's how every deployment works, by default, for every site. Here's exactly how it works under the hood.
The Problem with Traditional Deployments
To understand why zero-downtime matters, let's look at what happens during a traditional deployment.
A typical deployment runs something like this:
git pull origin main— Pull the latest code into the live directorycomposer install --no-dev— Install PHP dependenciesnpm run build— Build frontend assetsphp artisan migrate— Run database migrationsphp artisan config:cache— Cache configurationphp artisan route:cache— Cache routesphp artisan view:cache— Cache viewsRestart PHP-FPM or Octane workers
The problem is that this sequence takes time — anywhere from 30 seconds to several minutes depending on your project size, dependency count, and build complexity. During that time, your application is in an inconsistent state.
When git pull runs, the code changes immediately. But Composer hasn't installed new dependencies yet. If your new code references a new package or a new class from an updated package, every request during the composer install window will fail with a fatal error.
When php artisan migrate runs, the database schema changes. But if the old code is still handling requests, those requests might fail because the database no longer matches what the code expects.
When PHP-FPM restarts, existing connections are terminated. Users mid-request get an error. POST requests lose their data. Long-running operations are interrupted.
These aren't hypothetical problems. They happen on every traditional deployment. The only question is whether anyone notices — and the answer depends on your traffic volume and how unlucky your users are.
How Atomic Symlink Deployments Work
Deploynix uses atomic symlink deployments to eliminate these problems entirely. Here's the directory structure on a Deploynix-managed server:
/home/deploynix/your-site/
current -> /home/deploynix/your-site/releases/20260318143022
releases/
20260318143022/ (current release)
20260318120000/ (previous release)
20260317090000/ (two releases ago)
shared/
storage/
.env
The key insight is that current is a symbolic link, not a directory. Your web server (Nginx) is configured to serve from current/public. PHP-FPM runs from the current directory. Every request hits whatever current points to.
The Deployment Sequence
When you trigger a deployment on Deploynix, here's what actually happens:
Step 1: Create a new release directory.
A new directory is created under releases/ with a timestamp-based name. This is a completely fresh directory — it doesn't share any state with the running application.
releases/20260318150000/ (new, empty)
Step 2: Clone and build in the new directory.
Your repository is cloned into the new release directory. Composer installs dependencies. NPM builds assets. All of this happens in the new directory while the old release continues serving requests without interruption.
releases/20260318150000/ (built, ready)
Your application is still running from the old release. Users are still being served. Nothing has changed from their perspective.
Step 3: Link shared resources.
Certain directories and files are shared across releases. The storage directory contains user uploads, logs, and framework cache. The .env file contains your environment configuration. These don't live inside each release — they live in the shared/ directory and are symlinked into each release.
releases/20260318150000/storage -> /home/deploynix/your-site/shared/storage
releases/20260318150000/.env -> /home/deploynix/your-site/shared/.env
This means your uploaded files, session data, and logs persist across deployments. They're not overwritten, moved, or lost.
Step 4: Run deployment hooks.
If you've configured pre-activation hooks (migrations, cache clearing, etc.), they run against the new release directory. Database migrations execute while the old code is still serving requests. This works because well-written migrations are backwards-compatible — they add columns and tables without removing or renaming things the old code depends on.
Step 5: Switch the symlink.
This is the atomic moment. The current symlink is updated to point to the new release:
current -> /home/deploynix/your-site/releases/20260318150000
On Linux, the ln -sfn command makes this switch atomic. There's no moment where current points to nothing. There's no moment where it points to a partially-built directory. One instant it points to the old release, the next instant it points to the new release.
Step 6: Gracefully reload PHP-FPM.
After the symlink switches, PHP-FPM receives a reload signal. This is a graceful reload, not a restart. Existing PHP-FPM workers finish processing their current requests using the old code (from the old release directory, which still exists). New workers start and pick up the new code from the updated current symlink.
No request is dropped. No connection is terminated. Workers finishing old requests complete normally. New requests get the new code.
If you're running Laravel Octane (with FrankenPHP, Swoole, or RoadRunner), the process is similar — a graceful restart signal tells the persistent workers to finish current requests and reinitialize with the new codebase.
Step 7: Run post-activation hooks.
Post-deploy hooks run after the new release is live. These might include cache warming, notification sending, or integration pings.
Step 8: Clean up old releases.
Based on your retention configuration, old releases are cleaned up. By default, Deploynix keeps a configurable number of previous releases to support instant rollback. Releases beyond the retention limit are deleted to reclaim disk space.
Shared Storage: What Persists Across Deployments
Understanding what's shared and what's per-release is critical for zero-downtime deployments.
Shared across deployments:
storage/app— User uploads and application filesstorage/app/public— Publicly accessible uploaded filesstorage/framework/cache— Framework cache (when using file-based caching)storage/framework/sessions— Active user sessionsstorage/framework/views— Compiled Blade viewsstorage/logs— Application logs.env— Environment configurationdatabase/database.sqlite— SQLite database (when applicable)
Unique per deployment:
vendor/— Composer dependencies (ensures each release has its exact dependency versions)node_modules/— NPM packages (when applicable)public/build/— Compiled frontend assetsAll application code
This separation means your users' sessions persist across deployments. Their uploaded files aren't lost. Your logs continue uninterrupted. But each release has its own dependency installation, ensuring there's no cross-contamination between versions.
PHP-FPM Graceful Reload: The Key Detail
The graceful reload is what makes zero-downtime work in practice, not just in theory.
When PHP-FPM receives a SIGUSR2 signal (the reload signal), here's what happens:
The master process stops accepting new workers from the old pool
Existing workers continue processing their current requests
New workers are spawned that read from the updated
currentsymlinkAs old workers finish their requests, they're terminated
Eventually, all workers are running the new code
This process typically completes within a few seconds, but the important point is that no request is ever interrupted. A user who started a form submission milliseconds before the deployment will have their POST request completed by an old worker. The next request they make will be handled by a new worker running the updated code.
For Octane applications, the graceful restart works similarly but at the application level rather than the process level. Octane workers finish their current request cycle, then re-bootstrap the Laravel application from the new release.
Instant Rollback
Because previous releases are retained on disk as complete, self-contained directories, rolling back is trivially fast.
When you click "Rollback" in the Deploynix dashboard, here's what happens:
The
currentsymlink is updated to point to the previous releasePHP-FPM receives a graceful reload signal
That's it. There's no re-downloading code, no reinstalling dependencies, no rebuilding assets. The previous release is already on disk, fully built and ready to serve. The rollback takes seconds.
This is fundamentally different from a "rollback" that re-deploys a previous commit. Re-deploying still takes minutes. It still requires downloading dependencies and building assets. It's a new deployment that happens to use old code. A symlink rollback is instantaneous because the old release already exists.
You can roll back to any retained release, not just the immediately previous one. If your last three deployments all introduced issues, you can roll back to the release from three deployments ago with the same single-click process.
Writing Migration-Safe Code
Zero-downtime deployments work best when your database migrations are backwards-compatible. This means the old code should still function correctly after the migration runs but before the symlink switches.
Here are the guidelines we recommend:
Safe migration operations:
Adding a new column (with a default value or nullable)
Adding a new table
Adding a new index
Operations that need care:
Renaming a column — use a two-phase approach (add new column, deploy code that uses both, remove old column)
Removing a column — first deploy code that doesn't use the column, then remove it
Changing a column type — add a new column with the desired type, migrate data, update code, remove old column
This isn't specific to Deploynix — it's a best practice for any zero-downtime deployment system. The key principle is that your database should be compatible with both the old and new code during the transition period.
Deployment Scripts in the Context of Zero-Downtime
Deploynix runs a default deployment sequence that handles the most common Laravel deployment tasks automatically. After the new release is built, the following steps run before the symlink switches:
Dependency installation (
composer install,npm install)Asset compilation (
npm run build)Cache optimization (
php artisan config:cache,php artisan route:cache,php artisan view:cache)Database migrations (
php artisan migrate --force)Queue worker restart (
php artisan queue:restart)
You can also define a custom deploy script for your site. This script runs after the default deployment steps, giving you a place to add project-specific commands like cache warming, notification dispatching, or custom validation.
If any step in the deployment sequence fails, the deployment is aborted. The symlink stays pointed at the current working release. Your users never see the failed deployment.
Monitoring Deployments in Real Time
Deploynix streams deployment logs in real time via WebSocket connections built on Laravel Reverb. You see each step as it happens — code checkout, dependency installation, asset building, script execution, symlink switch, and service reload.
This real-time visibility means you don't need to refresh a page wondering if your deployment finished. You see the output live, including any errors or warnings. If something fails, you know immediately what happened and at which step.
Common Questions
Does zero-downtime work with Octane?
Yes. Deploynix handles graceful restarts for all three Octane drivers — FrankenPHP, Swoole, and RoadRunner. The restart signal varies by driver, but the effect is the same: existing requests complete, then workers reinitialize with the new code.
What about queue workers?
Queue workers need to be restarted to pick up new code. Deploynix restarts queue workers gracefully after the symlink switch. Workers finish their current job before restarting with the new code.
How much disk space do retained releases use?
Each release contains your application code, vendor directory, and built assets. A typical Laravel application's release is 50-200 MB depending on dependencies and assets. With a retention of five releases, you need 250 MB to 1 GB of disk space for releases. You can configure the retention count based on your disk constraints.
Can I deploy to multiple servers?
If you have multiple servers behind a load balancer, you can deploy to each server individually. Deploynix manages load balancer configuration separately, so traffic continues routing normally while each server is updated.
Conclusion
Zero-downtime deployment isn't magic. It's a well-understood technique based on atomic symlink switching, shared storage, and graceful service reloading. The individual pieces are straightforward. The value of Deploynix is that these pieces are assembled correctly, tested thoroughly, and applied to every deployment by default.
You shouldn't have to think about downtime when you deploy. Your users shouldn't notice when you ship new code. And rolling back should be a single click, not a panic-driven SSH session.
That's what zero-downtime deployment means in practice, and it's how every deployment works on Deploynix.
Get started at deploynix.io.
Top comments (0)