
Table of Contents
By Khimananda Oli | Last reviewed: August 2026
Dropped requests during release windows remain a primary source of user-facing errors in PHP applications, even when code quality is high. Implementing zero downtime deployment for Laravel with Deployer solves this by decoupling file uploads from traffic serving through atomic filesystem operations. This guide details the exact configuration required to achieve seamless transitions on standard Linux VPS infrastructure without Kubernetes complexity.
How does zero downtime deployment for Laravel with Deployer actually work?
The mechanism relies entirely on filesystem atomicity rather than application-level reloading. When you execute a deployment, Deployer creates a new timestamped directory under /var/www/project/releases/, uploads code, installs Composer dependencies, and builds assets in isolation. The current live traffic continues serving from the existing release via a stable symlink at /var/www/project/current.
Only after all build steps succeed does Deployer atomically update the current symlink to point to the new release. On Linux ext4/xfs filesystems, renaming a symlink is an atomic kernel operation; there is no intermediate state where the link points nowhere or to a partial directory. Nginx and PHP-FPM resolve paths on each request, so the very next incoming HTTP call hits the new codebase immediately. If any pre-symlink task fails, the old symlink remains intact, and users never see an error page.
This architecture requires that mutable state never lives inside the release directory. User uploads, session files, logs, and the .env file must reside in the shared/ directory and be symlinked into each release during preparation. Understanding this separation is critical before configuring your first Laravel VPS deployment, as misconfigured shared paths are the most common cause of post-deploy data loss.
How do you configure shared resources and writable directories correctly?
Laravel expects specific directories to persist across deployments. Deployer’s built-in Laravel recipe handles most defaults, but production environments frequently require additional customizations for packages like Spatie Media Library, Laravel Reverb, or custom cache drivers. Misconfiguring these causes silent failures where uploads disappear after deploy or queue workers crash due to missing log directories.
// deploy.php
namespace Deployer;
require 'recipe/laravel.php';
host('production')
->set('remote_user', 'deploy')
->set('hostname', 'app.example.com')
->set('deploy_path', '/var/www/example');
// Extend default shared files/dirs for real-world Laravel apps
add('shared_files', ['.env', 'storage/oauth-private.key', 'storage/oauth-public.key']);
add('shared_dirs', [
'storage/app/public',
'storage/logs',
'storage/framework/sessions',
'storage/framework/cache/data',
'storage/reverb', // WebSocket state persistence
]);
// Ensure correct ownership for PHP-FPM pool user
set('writable_dirs', [
'bootstrap/cache',
'storage',
'storage/framework',
'storage/logs',
]);
set('writable_mode', 'acl'); // Use ACLs instead of chmod for security
set('writable_use_sudo', false); Using writable_mode = acl is strongly preferred over chmod 777 approaches. Set POSIX ACLs once on the server so both the deploy user and www-data can write without permission escalation:
# Run once during initial server provisioning
sudo setfacl -Rdm u:deploy:rwx,u:www-data:rwx /var/www/example/shared/storage
sudo setfacl -Rm u:deploy:rwx,u:www-data:rwx /var/www/example/shared/storage This prevents the common issue where cache files created during CLI commands (running as deploy user) become unreadable by PHP-FPM (running as www-data). For teams managing database credentials securely, consider integrating secrets management best practices rather than committing encrypted .env files to version control.
What tasks must run before the atomic symlink switch?
The atomic symlink only guarantees zero downtime if the target release is fully functional before switching. Running migrations or cache clearing after the symlink update creates a window where new code executes against stale caches or incompatible database schemas. Deployer’s task ordering enforces safety through explicit dependencies.
- composer:install — Runs with
--no-dev --prefer-dist --optimize-autoloaderin the new release directory. Never shares vendor/ between releases; dependency versions may differ. - artisan:storage:link — Recreates the public/storage symlink pointing to shared storage. Idempotent and safe to run every deploy.
- artisan:migrate — Executes pending migrations before symlink switch. Migrations must be backward-compatible with the currently running code. See zero-downtime migration patterns for expand-contract strategies.
- artisan:config:cache — Pre-generates config cache in the new release. Avoids cold-start latency spike on first requests.
- artisan:route:cache — Compiles routes. Critical for large applications where route resolution adds measurable overhead.
- artisan:view:cache — Pre-compiles Blade templates. Prevents template compilation race conditions under load.
- deploy:symlink — Atomic switch. Only executes if all prior tasks succeeded.
- artisan:queue:restart — Signals queue workers to gracefully terminate after current job. New workers spawn with updated code via systemd/supervisor.
A frequent mistake is placing artisan:optimize after the symlink. This means the first batch of requests hits uncached routes and config, causing latency spikes that trigger health check failures in load-balanced environments. Always warm caches in the isolated release directory before switching.
How do you handle OPcache invalidation without restarting PHP-FPM?
PHP’s OPcache stores compiled bytecode in shared memory. After an atomic symlink switch, OPcache may still serve cached opcodes from the old release path because it keys cache entries by absolute file path. Simply relying on opcache.validate_timestamps=1 introduces unacceptable latency in production as PHP checks file modification times on every request.
The correct approach is explicit cache invalidation triggered immediately after the symlink update. Deployer provides a built-in task, but you must ensure it targets the correct PHP-FPM socket or TCP endpoint:
// In deploy.php — override default cachetool configuration
set('cachetool_args', '--web --web-url=https://app.example.com/opcache-reset.php');
// OR for FPM socket (preferred, no HTTP overhead)
set('cachetool_args', '--fpm-status-path=/run/php/php8.4-fpm.sock');
after('deploy:symlink', 'cachetool:clear:opcache'); If using FPM socket mode, ensure the deploy user has read/write access to the socket file. For multi-server setups behind a load balancer, OPcache reset must execute on each target host independently — Deployer runs tasks per-host by default, which handles this correctly. Never skip OPcache invalidation in production; stale opcodes cause subtle bugs where new code appears partially applied.
When should you choose Deployer over container-based deployment strategies?
Not every Laravel project benefits from Kubernetes complexity. The choice depends on team size, traffic patterns, compliance requirements, and operational maturity. Both approaches achieve zero downtime, but their trade-offs differ significantly.
| Criteria | Deployer (Traditional VPS) | Kubernetes / Container-Based |
|---|---|---|
| Setup Time | Hours. Single SSH key + recipe file. | Days to weeks. Cluster provisioning, ingress, CI integration. |
| Rollback Speed | <2 seconds (symlink revert). | 30–120 seconds (pod termination + rescheduling). |
| Resource Overhead | Near-zero. No daemon processes beyond PHP-FPM/Nginx. | Significant. etcd, kubelet, CNI, service mesh consume RAM/CPU. |
| Horizontal Scaling | Manual or limited auto-scaling. Requires external tooling. | Native HPA/VPA. Automatic pod scaling based on metrics. |
| Compliance Audits | Simpler evidence collection. Direct filesystem access for SOC 2. | Complex. Must audit cluster config, RBAC, network policies. |
| Best For | SMEs, agencies, Nepal-based businesses, single-region apps. | High-scale SaaS, multi-region, microservices architectures. |
For many Nepal-based businesses and global SMEs I’ve advised, Deployer delivers faster time-to-production and lower operational overhead than premature containerization. Reserve Kubernetes for when horizontal scaling demands genuinely exceed what vertical scaling plus read replicas can provide. Review blue-green and canary patterns on Kubernetes if your traffic profile justifies that leap.
Implementing Reliable Zero Downtime Deployment for Laravel with Deployer
Production-grade deployments require more than correct configuration—they demand observable verification and tested recovery procedures. After implementing the atomic symlink workflow described above, add a post-deploy health check task that validates critical endpoints return expected responses before marking the release successful. Configure monitoring alerts on your four golden signals to detect regressions within seconds of symlink switch.
Test your rollback procedure monthly in staging. Run dep rollback production and verify the application recovers within the documented RTO. Document any deviations. Automation without verified recovery is just automated fragility.
If your deployment pipeline needs review or your team is preparing for compliance audits requiring proven change management controls, reach out to discuss your infrastructure. I help teams build deployment systems that survive peak traffic and pass auditor scrutiny without last-minute panic.