DEV Community

Cover image for What Is Zero-Downtime Deployment and Why Does Your Laravel App Need It?
Deploynix
Deploynix

Posted on • Originally published at deploynix.io

What Is Zero-Downtime Deployment and Why Does Your Laravel App Need It?

Picture this: you have a bug fix ready. Your users are active — it is mid-afternoon, peak traffic. You push the code, your deployment script runs, and for 15-30 seconds, your application shows a maintenance page. Or worse, it shows an error page because the new code is partially deployed and the old code is partially removed.

Those 15-30 seconds might not seem like much. But they happen every time you deploy. If you deploy three times a day — which healthy teams do — that is 45-90 seconds of daily downtime. Users hitting your site during those windows see errors, lose their form progress, get logged out, or simply assume your service is unreliable.

Zero-downtime deployment eliminates this entirely. Your users never see a maintenance page, never lose a request, and never know a deployment happened. The transition from old code to new code is seamless and instantaneous from their perspective.

This guide explains what zero-downtime deployment is, how it works technically, what alternatives exist, and how Deploynix implements it for Laravel applications.

The Traditional Deployment Problem

To understand why zero-downtime deployment matters, let us look at how a traditional (naive) deployment works:

  1. You push code to your server (via git pull, rsync, or FTP)
  2. Dependencies are installed (composer install)
  3. Assets are built (npm run build)
  4. Cache is cleared (php artisan config:clear, php artisan cache:clear)
  5. Migrations run (php artisan migrate)
  6. The application restarts

During steps 1-5, your application is in an inconsistent state. The code on disk is changing while PHP-FPM processes are still serving requests. Some processes might load old code, others might load partially new code. Composer's autoloader might reference files that do not exist yet. If a migration runs that changes a database column, the old code (still handling requests) might fail because it expects the old schema.

This is the "deployment window" — a period where anything can go wrong and usually does, at least intermittently.

The Maintenance Mode Band-Aid

Laravel provides php artisan down to put the application in maintenance mode, displaying a friendly "We'll be right back" page. This prevents the inconsistency problem by stopping all user requests during deployment:

php artisan down
git pull
composer install --no-dev
php artisan migrate
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan up
Enter fullscreen mode Exit fullscreen mode

This approach is honest — it acknowledges that deployment creates a service interruption and handles it gracefully. But it is still downtime. Every request during the deployment window receives a 503 response. API consumers get errors. Webhooks from third-party services fail. Users submitting forms lose their work.

For a side project, this is fine. For a business application with paying customers, it is not.

What Zero-Downtime Deployment Actually Means

Zero-downtime deployment means that at no point during the deployment process is your application unavailable to users. Requests continue to be served successfully throughout the entire deployment. The transition from old code to new code happens atomically — one moment the old code is serving requests, the next moment the new code is, with no gap in between.

This is achieved through a technique called atomic symlink switching, combined with a release-based directory structure.

How It Works: The Release Directory Pattern

Instead of deploying code into a single directory and modifying files in place, zero-downtime deployment uses multiple release directories and a symlink:

/home/deploynix/
├── current -> /home/deploynix/releases/20260318_143022
├── releases/
│   ├── 20260318_120510/   (previous release)
│   ├── 20260318_143022/   (current release)
│   └── 20260318_160845/   (new release, being prepared)
└── shared/
    ├── storage/           (shared across releases)
    └── .env               (shared across releases)
Enter fullscreen mode Exit fullscreen mode

The Deployment Sequence

  1. Create a new release directory. A fresh copy of your code is placed in a new timestamped directory under releases/. This directory is completely separate from the currently active release.
  2. Install dependencies. composer install runs in the new release directory. The current release continues serving requests undisturbed.
  3. Build assets. npm run build runs in the new release directory. Again, the current release is unaffected.
  4. Link shared resources. The storage directory and .env file from the shared/ directory are symlinked into the new release. These are shared across all releases so that uploaded files, logs, and configuration persist between deployments.
  5. Run optimizations. php artisan config:cache, route:cache, view:cache, and event:cache run in the new release directory.
  6. Run migrations. Database migrations run against the database. This step requires care (more on this below).
  7. Switch the symlink. The current symlink is atomically updated to point to the new release directory. On Linux, this is a single filesystem operation that happens instantaneously.
  8. Reload PHP-FPM. PHP-FPM workers are gracefully reloaded. In-flight requests finish with the old code, new requests use the new code.
  9. Clean up old releases. Older releases are removed, keeping only the most recent few for rollback purposes.

The critical step is #7 — the symlink switch. Because it is an atomic filesystem operation, there is no moment where the current directory points to nothing or to a partially prepared release. One instant it points to the old release, the next instant it points to the new one.

The Database Migration Challenge

Database migrations are the trickiest part of zero-downtime deployment. The challenge is that during the transition period, both old and new code may be running simultaneously — old PHP-FPM workers finishing requests while new workers start handling new requests. Both versions need to work with the same database.

Safe Migration Practices

Adding a column: Safe. Old code ignores columns it does not know about. New code uses the new column.

Removing a column: Dangerous if done in one step. Old code may still reference the column. The safe approach is two deployments:

  1. First deployment: Remove all references to the column in code
  2. Second deployment: Drop the column from the database

Renaming a column: Dangerous. Do it in three steps:

  1. Add the new column, populate it from the old column
  2. Update code to use the new column
  3. Drop the old column

Changing a column type: Depends on the change. Widening (int to bigint, varchar(50) to varchar(255)) is usually safe. Narrowing or changing types requires a multi-step approach.

The General Rule

Make database changes additive. Add columns, add tables, add indexes. Destructive changes (drops, renames, type changes) should be done in separate deployments after the code that depends on the old schema has been fully replaced.

Alternatives to Zero-Downtime Deployment

Blue-Green Deployment

Blue-green deployment maintains two identical production environments — "blue" and "green." At any time, one is live and the other is idle. You deploy to the idle environment, test it, and then switch traffic from the live environment to the newly deployed one.

Advantages: Complete environment isolation. If the new deployment has issues, you switch back instantly.

Disadvantages: Requires double the infrastructure. Database state must be shared or synchronized between environments.

For most Laravel applications, blue-green deployment is overkill. The release directory pattern achieves the same result without doubling your server costs.

Rolling Deployment

In a multi-server setup with a load balancer, rolling deployment updates servers one at a time. The load balancer routes traffic away from the server being updated, the update completes, and the server is added back to the pool.

Deploynix supports this through its load balancer integration. When you deploy to a site with multiple web servers, the deployment happens across all servers, and the load balancer ensures traffic continues flowing to servers that have not yet been updated.

Canary Deployment

Canary deployment routes a small percentage of traffic to the new version while the majority still hits the old version. If the canary shows no issues, traffic is gradually shifted to the new version.

This is more complex to implement and is typically used by large-scale applications where even a brief period of errors across all traffic is unacceptable.

How Deploynix Implements Zero-Downtime Deployment

Deploynix uses the release directory pattern described above as its default deployment strategy. Here is what happens when you trigger a deployment:

1. Code Checkout

Your latest code is fetched from your git provider (GitHub, GitLab, Bitbucket, or a custom provider) and placed in a new release directory.

2. Custom Deploy Script

Deploynix supports a custom deploy script that runs after the default deployment steps (dependency installation, asset compilation, optimizations) and before the release is activated. This lets you run additional commands — seeding data, clearing specific caches, notifying external services, or any custom build steps your application requires. You configure the script through the Deploy Script tab in the site dashboard.

3. Dependency Installation

Composer dependencies are installed with --no-dev (production mode). If your application uses Node.js for asset compilation, those dependencies are installed too.

4. Asset Compilation

If configured, npm run build compiles your frontend assets (Vite, Tailwind CSS, Alpine.js).

5. Optimization

Laravel's optimization commands run to cache configuration, routes, views, and events. This eliminates filesystem reads on every request.

6. Symlink Switch

The current symlink is atomically updated to point to the new release.

7. Process Reload

PHP-FPM (or Octane, if configured) is gracefully reloaded. Existing workers finish their in-flight requests, and new workers start using the new code.

Rollback: When Things Go Wrong

Even with zero-downtime deployment, new code can introduce bugs that only appear in production. This is where rollback becomes essential.

Because Deploynix keeps previous releases on the server, rolling back is as simple as switching the current symlink back to the previous release directory. This is another atomic operation — rollback is just as instantaneous as deployment.

Deploynix provides one-click rollback through the dashboard. Select the previous release, click rollback, and the symlink switches back. Your application is running the old code within seconds, and you can debug the issue without users being affected.

Important: Rollback only reverts the application code. Database migrations are not reversed automatically because doing so could cause data loss. If your deployment included a migration, you need to assess whether the migration can be safely reversed or whether the old code is compatible with the new database schema (which it should be if you followed the safe migration practices described above).

Scheduled Deployments

Sometimes you want to deploy at a specific time — during a maintenance window, after business hours, or at a time when traffic is lowest. Deploynix supports scheduled deployments, allowing you to prepare a deployment and set it to execute at a future time.

Scheduled deployments use the same zero-downtime process. The only difference is timing — instead of executing immediately when you click deploy, the deployment is queued and executed at the scheduled time. You can also cancel a scheduled deployment if plans change.

The Business Case for Zero-Downtime Deployment

The technical benefits are clear, but the business case is equally compelling:

Deploy more frequently. When deployments carry no risk of user-facing downtime, you can deploy small changes more often. Smaller deployments are easier to debug when something goes wrong.

Deploy during business hours. No more deploying at midnight to avoid impacting users. Deploy when your team is alert and available to monitor the result.

Maintain trust with users. Users who encounter maintenance pages or errors during deployment lose confidence in your service. Zero-downtime deployment is invisible to users, which is exactly how deployment should be.

Meet SLA commitments. If you have uptime SLAs with customers, every deployment that causes downtime counts against your availability metric. Zero-downtime deployment removes deployment as a source of downtime.

Conclusion

Zero-downtime deployment is not a luxury feature — it is a fundamental practice for any application that serves real users. The technique is well-established (atomic symlink switching has existed for decades), and the implementation on modern deployment platforms like Deploynix is automated and reliable.

The core concept is simple: prepare the new release completely before switching to it, switch atomically so there is no gap, and keep previous releases available for instant rollback. Combined with safe database migration practices and deployment hooks for custom logic, this gives you a deployment process that is fast, safe, and invisible to your users.

If you are still running git pull on your production server and hoping for the best, the switch to zero-downtime deployment is one of the highest-impact improvements you can make to your operations. Your users will not notice the change — and that is exactly the point.

Top comments (0)