Tools: The Anatomy of a Zero-Downtime Deploy: What Happens in Those 15 Seconds (2026)

Tools: The Anatomy of a Zero-Downtime Deploy: What Happens in Those 15 Seconds (2026)

The Release Directory Structure

Second 0-1: Deployment Triggered

Second 1-2: Create Release Directory

Second 2-5: Clone the Repository

Second 6-7: Sync Environment Variables

Second 7-9: Composer Install

Second 9-10: Run Database Migrations

Second 10-11: Build Assets

Second 11-12: Cache Optimization and Custom Deploy Script

Second 12: The Atomic Switchover

Second 12-13: PHP-FPM Reload

Second 13-15: Post-Deployment Tasks

What About Rollback?

Multi-Server Deployments

Why This Matters

Conclusion You push to your Git repository, Deploynix picks up the change, and 15 seconds later your users are on the new version of your application. No maintenance page. No dropped requests. No error responses during the transition. Zero downtime. But how? How does Deploynix replace a running application with a new version without any user ever seeing an error? The answer is a carefully orchestrated sequence of operations, each designed to ensure that at every single moment during the deployment, a working version of your application is serving requests. This post walks through that sequence second by second. Understanding it will help you write better deploy scripts, debug deployment issues, and appreciate why certain conventions (like not storing uploads in your project directory) exist. Before we step through the deployment, you need to understand the directory structure that makes zero-downtime deployment possible. It is based on a simple but powerful concept: symlinks. On your Deploynix server, your site's directory structure looks something like this: The key is the current symlink. Nginx is configured to serve your application from the current directory. But current is not a real directory. It is a symbolic link that points to a specific release directory. Your application code lives in timestamped release directories under releases/. The shared directory contains files that persist across deployments: your storage directory (with logs, cache, uploaded files, and framework cache) and your .env file. These are symlinked into each release directory so every release uses the same storage and configuration. This structure is what makes atomic switchover possible. Changing a symlink is an atomic operation on Linux. One moment current points to the old release, the next moment it points to the new release. There is no intermediate state where current points to nothing or to a partially deployed release. The deployment begins. This can happen in several ways: a webhook from your Git provider (GitHub, GitLab, Bitbucket, or a custom repository) after a push, a manual trigger from the Deploynix dashboard, an API call using your Deploynix API token, or a scheduled deployment that fires at a pre-configured time. Deploynix records the deployment start time, the commit hash being deployed, and the user who triggered it. This metadata is used for the deployment log, rollback reference, and audit trail. A real-time event is broadcast to the Deploynix dashboard so anyone watching can see the deployment in progress. If you have team members with Owner, Admin, Manager, Developer, or Viewer roles in your organization, they can see the deployment happening live. Deploynix creates a new release directory with a timestamp name: This directory will contain the new version of your application. It is completely separate from the current release directory that is still serving traffic. Nothing about the running application changes at this point. Deploynix clones your repository into the new release directory. This is a shallow clone of the specific commit being deployed, which is faster than a full clone because it only downloads the files at that commit, not the entire Git history. The clone connects to your Git provider using the SSH key or token configured on your Deploynix site. GitHub, GitLab, Bitbucket, and custom Git repositories are all supported. During this step, your application continues serving requests from the previous release. Users are completely unaware that a deployment is happening. Before running any build steps, Deploynix creates symbolic links from the new release directory to the shared resources: The .env file is symlinked from shared/.env into the new release directory. This ensures the new release uses the same environment configuration as the current release. The storage directory is symlinked from shared/storage into the new release directory. This means logs, cached views, uploaded files, and framework cache are shared across all releases. An uploaded file from the current release is immediately accessible to the new release. This is why you should never store user uploads or persistent data inside your project's storage directory in version control. The shared storage directory persists across deployments, and the symlink ensures every release sees the same files. Deploynix syncs your environment variables to the new release directory before running any build steps. The .env file in the shared directory is already symlinked, and any pending environment variable changes are written at this point. This ensures that the deploy script and all subsequent commands use the correct configuration. Deploynix runs composer install --optimize-autoloader --no-dev --no-interaction in the new release directory. This installs your PHP dependencies based on the composer.lock file. The --optimize-autoloader flag generates a classmap for faster class loading in production. The --no-dev flag excludes development dependencies like testing frameworks and debugging tools. The --no-interaction flag ensures the command never prompts for input. This step can take longer than other steps depending on the number of dependencies and whether they are cached from a previous deployment. Composer's cache helps here: packages downloaded for previous deployments are reused if the version matches. Throughout this step, the old release is still serving traffic. A user browsing your site has no idea that Composer is installing packages for the next version just a few directories away. If your deployment includes database migrations, they run now with php artisan migrate --force. The --force flag is required to run migrations in a production environment. This is the one step that can potentially affect the running application, because migrations modify the shared database that both the old and new releases connect to. This is why migration backward compatibility matters. A well-written migration that adds a new column, creates a new table, or adds an index will not affect the running application. The old code simply does not reference the new column or table, so it continues working. A migration that renames a column, drops a column, or changes a column type can break the old release that is still serving requests. This is why deployment best practices recommend making migrations backward compatible: add the new column first, deploy the code that uses it, then remove the old column in a later migration. Deploynix logs all migration output so you can see exactly what ran during the deployment. If your application includes frontend assets that need building, the deploy script runs the build commands. A typical Laravel application with Vite: This compiles your JavaScript, CSS, and other frontend assets in the new release directory. The built assets are part of the new release and will be served after the switchover. The old release continues serving its own built assets to current users. There is no flash of unstyled content or JavaScript errors during the build process. The deploy script runs Laravel's cache optimization commands: These commands generate cached versions of your configuration, routes, views, and events inside the new release directory. The queue:restart command tells all queue workers to finish their current job and then restart, picking up the new code. If you have a custom deploy script configured on your site, it runs after the default deploy steps. This is where you can add any application-specific commands. All of this happens inside the new release directory while the old release is still serving traffic. When the new release becomes active, it will use these cached files for optimal performance. This is the critical moment. Deploynix atomically switches the current symlink to point to the new release directory: The ln -snf command is atomic on Linux. The symlink changes from pointing to the old release to pointing to the new release in a single filesystem operation. There is no intermediate state. At this exact instant, any new request that Nginx routes to the current directory will be served by the new release. Requests that are currently in-flight on the old release will complete normally because their PHP-FPM worker already has the old files loaded in memory. After the symlink swap, Deploynix sends a reload signal to PHP-FPM: A reload (not restart) is crucial. A restart would kill all running PHP-FPM worker processes, dropping any in-flight requests. A reload tells PHP-FPM to finish processing current requests with the existing workers, then spawn new workers that will pick up the new code from the updated current symlink. This graceful reload means that requests that started before the switchover complete using the old code, while new requests are handled by fresh workers running the new code. No request is ever interrupted. If you are using Laravel Octane with FrankenPHP, Swoole, or RoadRunner, the Octane server is restarted instead of PHP-FPM. Octane handles the restart gracefully, finishing in-flight requests before shutting down old workers. With the new release active and PHP-FPM reloaded, Deploynix runs the final tasks: OPcache reset. The PHP-FPM reload in the previous step clears OPcache automatically. Since OPcache has validate_timestamps=0 in production, the reload forces new workers to compile and cache the new files. Release cleanup. Deploynix removes old release directories beyond the configured retention count (releases_to_keep), freeing disk space while preserving enough previous releases for rollback. Deployment notification. A real-time event is broadcast to the Deploynix dashboard marking the deployment as complete. The deployment log records the duration, the commit hash, and the success status. If something goes wrong after deployment, Deploynix's rollback feature reverses the process instantly. Because previous release directories are preserved on the server, rollback is simply changing the current symlink to point to the previous release: Followed by a PHP-FPM reload and queue worker restart. The rollback takes seconds because there is no code to download, no dependencies to install, and no assets to build. The previous release is already on disk, ready to serve. This is why Deploynix keeps a configurable number of previous releases. Each one is a complete, ready-to-serve version of your application that can become active with a single symlink change. When your Laravel application runs on multiple Web servers behind a Deploynix Load Balancer, the deployment process runs on each server sequentially. Server one completes its deployment before server two begins. This ensures that at no point are all servers simultaneously in a deployment state. The Load Balancer continues sending traffic to all servers throughout the deployment. Servers running the old release handle requests until their deployment completes and the symlink swaps. The brief period where some servers run the old version and some run the new version is handled by API versioning and backward-compatible database migrations. For Deploynix's Round Robin, Least Connections, and IP Hash load balancing methods, this rolling deployment is seamless. Users might hit the old version on one request and the new version on the next, but both versions are fully functional. Zero-downtime deployment is not a luxury feature. It is a fundamental requirement for any application that serves real users. A maintenance page during deployment tells your users that your infrastructure is not mature enough to handle updates without interruption. It trains them to expect outages. And for applications that process payments, serve real-time data, or operate in different timezones, there is no "safe" deployment window. Deploynix's zero-downtime deployment gives you the confidence to deploy frequently. Daily deploys. Multiple deploys per day. Hotfixes in the middle of peak traffic. Each deployment is the same reliable, atomic switchover that takes 15 seconds and interrupts exactly zero requests. Those 15 seconds contain a remarkable amount of coordinated work: creating a pristine release directory, cloning your code, installing dependencies, running migrations, building assets, optimizing caches, and atomically switching the serving directory. Every step is designed so that at every moment, a fully working version of your application is serving your users. The symlink-based deployment strategy is simple in concept but powerful in practice. It turns deployment from a high-stress event into a routine operation. And with Deploynix's rollback feature keeping previous releases on disk, recovery from a bad deployment is just as fast as the deployment itself. Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Command

Copy

$ /home/deploynix/yoursite.com/ current -> /home/deploynix/yoursite.com/releases/20260318143022 releases/ 20260318120000/ (previous release) 20260318143022/ (current release) shared/ storage/ .env /home/deploynix/yoursite.com/ current -> /home/deploynix/yoursite.com/releases/20260318143022 releases/ 20260318120000/ (previous release) 20260318143022/ (current release) shared/ storage/ .env /home/deploynix/yoursite.com/ current -> /home/deploynix/yoursite.com/releases/20260318143022 releases/ 20260318120000/ (previous release) 20260318143022/ (current release) shared/ storage/ .env /home/deploynix/yoursite.com/releases/20260318143522/ /home/deploynix/yoursite.com/releases/20260318143522/ /home/deploynix/yoursite.com/releases/20260318143522/ -weight: 500;">npm ci -weight: 500;">npm run build -weight: 500;">npm ci -weight: 500;">npm run build -weight: 500;">npm ci -weight: 500;">npm run build php artisan config:cache php artisan route:cache php artisan view:cache php artisan event:cache php artisan queue:-weight: 500;">restart php artisan config:cache php artisan route:cache php artisan view:cache php artisan event:cache php artisan queue:-weight: 500;">restart php artisan config:cache php artisan route:cache php artisan view:cache php artisan event:cache php artisan queue:-weight: 500;">restart ln -snf /home/deploynix/yoursite.com/releases/20260318143522 /home/deploynix/yoursite.com/current ln -snf /home/deploynix/yoursite.com/releases/20260318143522 /home/deploynix/yoursite.com/current ln -snf /home/deploynix/yoursite.com/releases/20260318143522 /home/deploynix/yoursite.com/current -weight: 600;">sudo -weight: 500;">systemctl reload php8.4-fpm -weight: 600;">sudo -weight: 500;">systemctl reload php8.4-fpm -weight: 600;">sudo -weight: 500;">systemctl reload php8.4-fpm ln -snf /home/deploynix/yoursite.com/releases/20260318143022 /home/deploynix/yoursite.com/current ln -snf /home/deploynix/yoursite.com/releases/20260318143022 /home/deploynix/yoursite.com/current ln -snf /home/deploynix/yoursite.com/releases/20260318143022 /home/deploynix/yoursite.com/current