Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Zero Downtime Deployment for Laravel with Deployer

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.

Release 202608141000composer installnpm buildoptimizeRelease 202608141100NEW CODE READYAtomic TargetOld ReleaseKept as backupSafe Rollback/current SymlinkPoints to NEW ReleaseInstant Atomic SwapShared Storage.env / uploads / logs
Atomic symlink architecture enabling zero downtime deployment for Laravel with Deployer

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.

1. New ReleaseCode Prepared2. MigrateBEFORE Symlink3. Symlink SwapAtomic & Instant4. Clear OPcacheAFTER SymlinkWhy This Order Matters• Migrations run against OLD live code → backward-compatible schema changes only• Symlink swap happens AFTER DB is ready → no missing column errors• OPcache cleared LAST → prevents serving cached bytecode from old release❌ WRONG: Clear Cache Before SwapOld code reloads → race conditionUsers hit broken intermediate state✅ CORRECT: Clear After SwapNew code loads fresh bytecodeZero stale responses served
Correct sequencing for migrations and OPcache in zero downtime deployment for Laravel with Deployer

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.

Git PushTag v1.2.3Lint & TestPint + PestBuild AssetsVite + CommitDeploy ProductionManual TriggerPipeline Safety Checks✓ Tests must pass before deploy stage unlocks✓ Asset build artifacts committed to release branch✓ Production server has NO Node.js installed✓ SSH key injected at runtime, never persisted✓ Rollback available via dep rollback if health check fails
GitLab CI pipeline stages for secure zero downtime deployment for Laravel with Deployer

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 ApproachDowntime RiskRollback SpeedMigration SafetyBest For
Manual FTP/Git PullHigh (minutes)Slow (manual restore)UncontrolledHobby projects only
Basic rsync ScriptMedium (seconds)Moderate (re-run sync)Manual coordinationLow-traffic internal tools
Deployer Atomic SymlinkNone (atomic swap)Instant (symlink revert)Pre-swap executionProduction Laravel apps
Kubernetes/Docker SwarmNone (rolling update)Fast (container revert)Requires orchestrationLarge-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.

Frequently Asked Questions

Zero downtime deployment keeps the current release live while the new version uploads and prepares in a separate directory. Traffic switches instantly via symlink only after all setup tasks complete successfully, preventing user-facing errors or maintenance pages during updates.

Deployer creates timestamped release directories, uploads code, installs dependencies, and builds assets in isolation. It then atomically swaps the current symlink to point to the new release. If any step fails, the symlink remains untouched and the old version continues serving traffic safely.

Yes. Deployer 7.x fully supports Laravel 12 and PHP 8.4. Ensure your deploy.php recipe uses the laravel preset and your server runs PHP 8.2 or higher. I have deployed Laravel 12 applications on PHP 8.4 using this exact stack in production throughout 2026.

No, and I recommend against it. Build frontend assets locally or in CI, then upload the compiled artifacts. This reduces server attack surface, eliminates Node version conflicts, and speeds up deployments significantly. My standard workflow commits built assets as pipeline artifacts specifically to avoid installing Node on production Ubuntu servers.

Store .env in the shared directory configured in deploy.php, never in Git. Deployer symlinks it into each release automatically. Use vaults or encrypted storage for sensitive keys, injecting them during CI if needed. Never commit secrets to the repository or pass them as plain-text deploy arguments.

OPcache holds compiled PHP files from the previous release. Configure Deployer to reload PHP-FPM after the symlink swap using sudo systemctl reload php8.4-fpm. Without this, workers serve cached bytecode from the old release path even though the symlink points correctly to new code.

Run migrations after code upload but before the symlink swap. Use backward-compatible migrations that work with both old and new code. If a migration fails, Deployer aborts before switching traffic. Always test migrations against a staging copy first; I have recovered production databases too many times from untested schema changes.

The deploy user must own the deployment base directory and have write access to shared and releases folders. Common failures occur when www-data owns files but the deploy user cannot modify them. Fix with chown -R deploy:www-data /var/www/site and ensure umask settings allow group writes in deploy.php.

CI tools orchestrate pipelines; Deployer handles server-side release mechanics. I use GitLab CI to test, build assets, and trigger deploys, while Deployer manages atomic symlinks, shared directories, and rollbacks on the server. They complement each other rather than compete. Pure CI SSH scripts lack Deployer's rollback safety and release structure.

Yes. Run dep rollback to repoint the current symlink to the previous release directory. This takes seconds because no code reuploads or reinstalls occur. Keep at least three releases configured. I have used this dozens of times in production when post-deploy issues surface that staging did not catch.

Configure Deployer to restart queue workers after symlink swap using php artisan queue:restart. Update cron paths to reference the current symlink, not hardcoded release directories. Stale cron paths pointing to old releases are among the most common silent failures I encounter on client projects after migration to Deployer.

You need SSH access, sufficient disk space for multiple releases, and permission to reload PHP-FPM. Shared hosting rarely supports this. A VPS or dedicated server running Ubuntu 22.04 or 24.04 with PHP-FPM works reliably. Budget approximately Rs 3,000 to 5,000 monthly (~USD 22-37) for adequate Nepali-hosted infrastructure supporting this workflow.

Webhook endpoints must remain stable across releases. Test payment callbacks against staging before deploying gateway changes. Since Deployer preserves the old release until swap, you can verify webhooks hit the correct endpoint version. On Nepal Gift Card, this approach prevented payment confirmation losses during gateway integration updates.

Indirectly, yes. Eliminating maintenance windows prevents crawl errors and 503 responses during updates. Consistent uptime signals reliability to search engines. More importantly, fast atomic deploys enable frequent content and schema updates without risking indexation drops. I treat deployment reliability as foundational technical SEO infrastructure, not just DevOps.

Initial configuration takes two to four hours for experienced developers: writing deploy.php, configuring shared paths, setting up SSH keys, and testing rollback. First production deploy requires additional validation. Subsequent deploys take under sixty seconds. The upfront investment pays immediately; I have retrofitted Deployer onto legacy Laravel projects that previously required thirty-minute manual FTP updates.

Share this article

Quick Contact Options
Choose how you want to connect me: