
August 14, 2026
11 min read
Table of Contents
By Kokil Thapa | Last reviewed: August 2026
Dropping active user sessions during a production release is unacceptable for any serious application, yet many teams still rely on manual file uploads or basic Git pulls that cause visible outages. Implementing zero downtime deployment for Laravel with Deployer solves this by using atomic symlinks to swap code instantly while preserving shared state like uploads and environment files. This approach ensures your users never see an error page, even when you are pushing critical fixes or major feature updates to your Laravel development projects.
How does zero downtime deployment for Laravel with Deployer actually work?
The core mechanism behind zero downtime deployment for Laravel with Deployer is the atomic symlink swap. Instead of overwriting files in place—which causes partial reads, missing classes, and fatal errors—Deployer creates a completely new timestamped release directory for every deployment. All dependencies are installed, assets are built, and optimizations run inside this isolated folder before it ever touches live traffic.
Only after all preparation tasks succeed does Deployer execute a single ln -sfn command to point the /current symlink to the new release. This operation is atomic at the filesystem level: there is no intermediate state where the symlink points nowhere or partially resolves. Nginx or Apache continues serving requests from the old path until the exact microsecond the link updates, then seamlessly serves the new code. The previous release remains on disk untouched, making instant rollback possible if post-deploy monitoring reveals issues.
This architecture also separates volatile code from persistent state. User uploads, session files, logs, and the .env configuration live outside the release directory entirely in a shared/ folder. Deployer symlinks these into each new release during setup, so your application retains its database credentials and media library across deployments without copying gigabytes of data repeatedly. For legal-tech portals I maintain, this separation is non-negotiable because client documents and case files must persist independently of code releases.
How do you configure deploy.php for Laravel 12 and PHP 8.4?
A correct deploy.php configuration is the foundation of reliable zero downtime deployment for Laravel with Deployer. With Laravel 12 requiring minimum PHP 8.2 (and commonly running on 8.4 in 2026), your recipe must match both framework expectations and server reality. Start by installing Deployer globally via Composer 2.7+ and initializing the Laravel recipe:
composer require deployer/deployer --dev
vendor/bin/dep init Select the Laravel preset when prompted. The generated configuration needs specific adjustments for production reliability. Here is a battle-tested base configuration I use across multiple client projects:
<?php
namespace Deployer;
require 'recipe/laravel.php';
set('application', 'my-laravel-app');
set('repository', 'git@gitlab.com:myorg/my-laravel-app.git');
set('php_version', '8.4');
set('keep_releases', 5);
// Critical shared files and directories
add('shared_files', ['.env']);
add('shared_dirs', [
'storage/app/public',
'storage/logs',
'storage/framework/sessions',
'storage/framework/cache',
'public/uploads'
]);
// Writable directories for Laravel
add('writable_dirs', [
'bootstrap/cache',
'storage',
'public/uploads'
]);
host('production')
->set('hostname', '192.0.2.10')
->set('remote_user', 'deploy')
->set('deploy_path', '/var/www/{{application}}')
->set('http_user', 'www-data')
->set('bin/php', '/usr/bin/php8.4'); Several details here prevent common production failures. Explicitly setting bin/php avoids the disaster scenario where Deployer picks up PHP 8.1 from the system PATH while your application requires 8.4 features. The http_user directive ensures Deployer sets correct ownership after writing files, preventing permission denied errors when Laravel tries to write logs or cache. On Ubuntu 22.04/24.04 servers running Apache or Nginx with PHP-FPM, this user is typically www-data.
The shared_dirs list deserves careful attention. Many tutorials omit storage/framework/sessions and storage/framework/cache, causing users to be logged out after every deploy or losing queued job state. If your application handles file uploads through a custom controller rather than Spatie Media Library, include your upload directory explicitly. For projects using Filament admin panels with resource managers, verify that any custom storage paths are included in this list.
Configuring writable permissions correctly
Permission issues are the most frequent cause of failed deployments on Linux servers. Deployer’s default ACL strategy works well when your deploy user belongs to the same group as the web server user. Add this to your host configuration:
->set('writable_mode', 'acl')
->set('writable_use_sudo', false) If ACL support is unavailable on your filesystem (common on some VPS providers in Nepal using older ext3 partitions), fall back to chmod mode with writable_chmod_mode => '0775'. Never use 0777 in production. Test writability before going live by running dep deploy:writable production in isolation to catch permission problems without triggering a full release cycle.
How do you handle database migrations and OPcache during deployment?
Database migrations and opcode caching are the two most dangerous phases in zero downtime deployment for Laravel with Deployer. Getting either wrong causes data corruption or stale code execution despite successful symlink swaps.
Running migrations safely before the symlink swap
Laravel’s official Deployer recipe runs artisan:migrate after the symlink swap by default. In practice, this is risky for zero-downtime scenarios because the new code might reference columns or tables that don’t exist yet during the brief window between swap and migration completion. Override this behavior to run migrations against the new release directory before making it live:
task('deploy:migrate:before', function () {
cd('{{release_path}}');
run('{{bin/php}} {{release_path}}/artisan migrate --force --no-interaction');
})->desc('Run migrations before symlink swap');
before('deploy:symlink', 'deploy:migrate:before');
after('deploy:symlink', 'deploy:migrate:after'); This demands discipline in migration design. Every migration must be backward-compatible: add columns as nullable first, deploy, then backfill and add constraints in a subsequent release. Never drop columns or rename tables in a single deploy when practicing zero downtime deployment for Laravel with Deployer. If you need destructive schema changes, use the expand-contract pattern across multiple releases.
Clearing OPcache reliably after deployment
PHP-FPM caches compiled bytecode aggressively. Without explicit invalidation, your server may continue executing old PHP files even though the symlink points to new code. The Laravel recipe includes deploy:opcache, but it often fails silently because CLI PHP and FPM PHP use separate OPcache instances. Configure a dedicated cache-clearing endpoint or use the CLI reset command:
task('deploy:opcache:reset', function () {
run('{{bin/php}} -r "opcache_reset();" 2>/dev/null || true');
run('sudo systemctl reload php8.4-fpm');
})->desc('Reset OPcache and reload PHP-FPM');
after('deploy:symlink', 'deploy:opcache:reset'); Reloading PHP-FPM is more reliable than opcache_reset() alone because it forces worker processes to restart with fresh bytecode. On high-traffic sites, use systemctl reload instead of restart to avoid dropping active connections. Ensure your deploy user has passwordless sudo access for this specific command via /etc/sudoers.d/deployer. For projects where FPM reload isn’t feasible, expose a secured /opcache-reset endpoint protected by IP whitelist or bearer token and call it via curl in your deploy task.
How do you integrate Deployer with GitLab CI for automated releases?
Manual deploys from developer laptops introduce inconsistency and security risk. Integrating zero downtime deployment for Laravel with Deployer into GitLab CI ensures every release follows identical steps with auditable logs. For the sister sites I maintain on shared EC2 infrastructure, this pipeline has eliminated configuration drift entirely.
deploy_production:
stage: deploy
image: deployer/deployer:latest
script:
- mkdir -p ~/.ssh
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' > ~/.ssh/id_ed25519
- chmod 600 ~/.ssh/id_ed25519
- ssh-keyscan -H $PRODUCTION_HOST >> ~/.ssh/known_hosts
- vendor/bin/dep deploy production --tag=$CI_COMMIT_TAG
environment:
name: production
url: https://example.com
rules:
- if: '$CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/'
when: manual Store your SSH private key in GitLab CI/CD variables as SSH_PRIVATE_KEY with file type enabled. Never commit keys to the repository. The manual trigger for tagged commits prevents accidental production deploys from merge requests while maintaining full automation for intentional releases. For teams working on CI/CD pipelines in Nepal with intermittent connectivity, consider adding retry logic and artifact caching to reduce bandwidth usage.
A critical detail for Laravel 12 with Vite 6.x: production servers should not have Node.js installed. Build frontend assets in CI and commit the compiled output to the release artifact or include them in the deploy payload. This eliminates Node version mismatches and reduces server attack surface. Your deploy.php should skip npm:build entirely when deploying from CI, relying instead on pre-built assets transferred during the upload phase.
How do you roll back safely when a deployment fails?
Even with perfect zero downtime deployment for Laravel with Deployer configuration, bugs reach production. The ability to revert instantly is what makes this approach truly safe. Deployer maintains the last five releases by default (keep_releases => 5). Rolling back is a single command:
vendor/bin/dep rollback production This re-points the /current symlink to the previous release and reloads PHP-FPM. However, rollback has a critical caveat: database migrations are not automatically reversed. If your failed deployment included irreversible schema changes, rolling back code alone will cause fatal errors. This reinforces why backward-compatible migrations are mandatory.
Implement a post-deploy health check task that runs automatically after every release. If the check fails, trigger rollback without human intervention:
task('deploy:health_check', function () {
$url = get('health_check_url', 'https://example.com/health');
$response = run("curl -sf --max-time 10 $url");
if (empty($response)) {
throw new \RuntimeException('Health check failed');
}
})->desc('Verify application health after deploy');
after('deploy:success', 'deploy:health_check');
fail('deploy:health_check', 'deploy:rollback'); Your /health endpoint should verify database connectivity, cache availability, and critical service dependencies—not just return HTTP 200. For legal-tech platforms processing sensitive documents, I extend this to verify that document storage paths are writable and encryption keys are accessible. Automated rollback based on real health signals catches problems faster than waiting for user reports.
| Deployment Approach | Downtime Risk | Rollback Speed | Migration Safety | Best For |
|---|---|---|---|---|
| Manual FTP/Git Pull | High (minutes) | Slow (manual restore) | Uncontrolled | Hobby projects only |
| Basic rsync Script | Medium (seconds) | Moderate (re-run sync) | Manual coordination | Low-traffic internal tools |
| Deployer Atomic Symlink | None (atomic swap) | Instant (symlink revert) | Pre-swap execution | Production Laravel apps |
| Kubernetes/Docker Swarm | None (rolling update) | Fast (container revert) | Requires orchestration | Large-scale microservices |
For most Laravel applications serving Nepali businesses or international clients, Deployer’s atomic symlink approach provides the best balance of safety, simplicity, and operational overhead. Container orchestration adds significant complexity that rarely justifies itself for monolithic Laravel applications under moderate load. Stick with proven tooling unless your scale genuinely demands distributed infrastructure.
Implementing Reliable Zero Downtime Deployment for Laravel with Deployer
Zero downtime deployment for Laravel with Deployer transforms production releases from stressful events into routine operations. The combination of atomic symlinks, shared persistent storage, pre-swap migrations, and automated OPcache clearing eliminates the outage windows that plague simpler deployment methods. Start with the configuration patterns outlined here, test thoroughly on a staging environment that mirrors your production server’s PHP version and filesystem layout, and implement health checks before trusting automated rollbacks. If you need help configuring DevOps automation for your Laravel infrastructure or troubleshooting deployment issues on existing projects, get in touch to discuss your specific requirements.

