How to deploy a Laravel SaaS to production with zero downtime (2026 guide)

A practical guide to deploying Laravel SaaS applications with zero downtime, covering migration strategies, queue workers, health checks, and multi-tenant considerations.

R
Raşit Apalak
·
·
Updated
·
8 min read

Quick Answer

Quick answer: How to deploy a Laravel SaaS to production with zero downtime (2026 guide)

A practical guide to deploying Laravel SaaS applications with zero downtime, covering migration strategies, queue workers, health checks, and multi-tenant considerations.

See supporting documentation

How to deploy a Laravel SaaS to production with zero downtime (2026 guide)

Shipping a Laravel SaaS product is only half the challenge. The other half is deploying it without dropping active sessions, breaking billing webhooks, or corrupting tenant data mid-migration.

This guide covers a practical zero-downtime deployment strategy for Laravel SaaS applications, from migration safety to queue worker management to multi-tenant considerations.

Why zero-downtime matters more for SaaS

Traditional web apps can survive brief maintenance windows. SaaS products often cannot:

  • Stripe webhooks arrive on their own schedule and cannot be paused.
  • Tenants operate across time zones, so there is no universal quiet period.
  • Downtime during checkout or billing cycles directly costs revenue.
  • Team collaboration features mean multiple users are active simultaneously.

If your deployment process takes the app offline for even 30 seconds, you risk failed payments, lost form submissions, and broken trust.

The deployment pipeline at a glance

StageWhat happensRisk if skipped
BuildInstall dependencies, compile assets, run static analysisBroken frontend or missing packages in production
Pre-deploy checksRun automated tests, verify environment configDeploying broken code or missing secrets
Release swapSymlink or atomic switch to new release directoryPartial deployments serving mixed old/new code
MigrateRun database migrations against live databaseSchema drift or failed migrations under traffic
Queue restartGracefully restart queue workersWorkers processing jobs with stale code
Health checkVerify application responds correctly post-deploySilent failures that only surface through user reports
Rollback planKeep previous release available for instant revertExtended downtime while debugging a bad deploy

Step 1: Structure releases for atomic swaps

Zero-downtime deployment starts with how you organize releases on the server.

Release directory pattern

/var/www/app/
├── releases/
│   ├── 20260401_120000/
│   ├── 20260401_140000/
│   └── 20260401_160000/  ← newest
├── current -> releases/20260401_160000/
├── storage/               ← shared across releases
└── .env                   ← shared across releases

The current symlink points to the active release. Deploying means creating a new release directory, preparing it fully, then flipping the symlink. This is atomic at the filesystem level.

Shared resources

Storage directories (storage/app, storage/logs, storage/framework) and the .env file must be shared across releases. Symlink them into each release directory during setup.

ln -nfs /var/www/app/storage /var/www/app/releases/20260401_160000/storage
ln -nfs /var/www/app/.env /var/www/app/releases/20260401_160000/.env

Step 2: Make migrations safe for concurrent traffic

Database migrations are the highest-risk step in any deployment. In a zero-downtime context, the old code is still serving requests while migrations run.

Rules for safe migrations

  1. Never drop a column in the same deploy that stops using it. First deploy code that no longer reads the column, then drop it in a subsequent release.
  2. Never rename a column directly. Add the new column, backfill data, update code to use both, then remove the old column later.
  3. Add columns as nullable or with defaults. This avoids table-level locks on large tables in MySQL/PostgreSQL.
  4. Use short, focused migrations. Long-running migrations block subsequent migrations and can cause connection timeouts.

Multi-tenant migration considerations

If you run database-per-tenant (multi_db mode), migrations must apply to every tenant database, not just the central one.

php artisan tenants:artisan "migrate --force" --tenant=all

This adds deployment time proportional to tenant count. For large tenant bases, consider:

  • Running tenant migrations asynchronously via queued jobs.
  • Batching tenants to avoid overwhelming the database server.
  • Tracking migration status per tenant so partial failures can be retried.

Step 3: Manage queue workers gracefully

Queue workers run as long-lived processes. After a deploy, they still hold the old codebase in memory.

Graceful restart

php artisan queue:restart

This signals workers to finish their current job and then restart with fresh code. It does not kill jobs mid-execution.

Supervisor configuration

Use Supervisor (or systemd) to manage queue workers so they automatically restart:

[program:saas-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/app/current/artisan queue:work --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
numprocs=2
redirect_stderr=true
stdout_logfile=/var/www/app/storage/logs/worker.log

Note the path uses /current/ so it always resolves to the active release through the symlink.

Deployment-critical queues

If your app processes Stripe webhooks or sends transactional emails through queues, verify workers are healthy after every deploy. A silently dead worker can mean missed subscription renewals or lost invite emails.

Step 4: Environment configuration safety

Misconfigured environment variables are a leading cause of post-deploy incidents.

Pre-deploy config validation

Before flipping the release symlink, verify the critical variables exist:

php artisan config:cache
php artisan env:check APP_KEY DB_HOST STRIPE_SECRET MAIL_HOST

If env:check is not available in your Laravel version, write a simple Artisan command that fails if required variables are empty.

Secrets rotation

When rotating API keys or database credentials:

  1. Update .env on the server before deploying.
  2. Run php artisan config:clear && php artisan config:cache after the symlink swap.
  3. Restart queue workers so they pick up new cached config.

Step 5: Health checks and smoke tests

A successful deploy means the symlink flipped without error. It does not mean the application is working.

Application health endpoint

Add a health check route that verifies core dependencies:

Route::get('/health', function () {
    try {
        DB::connection()->getPdo();
        Cache::store()->get('health-check-ping');
        return response()->json(['status' => 'ok'], 200);
    } catch (\Throwable $e) {
        return response()->json(['status' => 'error'], 500);
    }
});

Post-deploy verification

After the symlink swap, hit the health endpoint from your deploy script:

HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://app.example.com/health)
if [ "$HTTP_STATUS" != "200" ]; then
  echo "Health check failed. Rolling back."
  ln -nfs /var/www/app/releases/PREVIOUS_RELEASE /var/www/app/current
  exit 1
fi

Multi-tenant smoke test

For SaaS apps, also verify that at least one tenant context loads correctly. A central database connection succeeding does not guarantee tenant databases are reachable.

Step 6: Rollback strategy

Every deployment should have a one-command rollback path.

Since the previous release directory still exists, rollback is a symlink swap:

ln -nfs /var/www/app/releases/PREVIOUS_RELEASE /var/www/app/current
php artisan config:cache
php artisan queue:restart

Migration rollback limitations

Database rollbacks are harder. If the new release ran migrations that the old code does not expect, reverting the symlink alone is not enough. This is why forward-compatible migrations (Step 2) are critical. Design every migration so the previous release can still function against the migrated schema.

Keep N releases

Retain the last 3-5 releases on disk for quick rollback. Clean up older ones to save disk space:

ls -dt /var/www/app/releases/*/ | tail -n +6 | xargs rm -rf

Step 7: CI/CD pipeline integration

Automate the full pipeline so deploys are repeatable and not dependent on manual SSH sessions.

  1. Push to main triggers the pipeline.
  2. Test stage: run PHPUnit, static analysis (PHPStan/Larastan), and frontend build.
  3. Build stage: composer install --no-dev, npm run build, cache config and routes.
  4. Deploy stage: upload to server, create release directory, run migrations, swap symlink.
  5. Verify stage: health check, smoke test, notify team.

Deployment tools for Laravel

  • Laravel Envoy: task runner built for Laravel deployments. Define deploy scripts in a Blade-like syntax.
  • Deployer (deployer.org): PHP-based deployment tool with Laravel recipe.
  • GitHub Actions + SSH: direct approach for smaller teams using CI to SSH and run deploy scripts.

All of these support the atomic symlink pattern described above.

Common deployment mistakes in SaaS

  • Running php artisan down during deploys. This defeats zero-downtime. The maintenance mode approach is for planned maintenance, not routine releases.
  • Forgetting to restart queue workers. Workers silently run old code until manually restarted.
  • Deploying destructive migrations without a compatibility window. Always separate schema removal from code removal by at least one release cycle.
  • No health check automation. Manual verification after every deploy does not scale.
  • Ignoring tenant database migrations. Central database migrates successfully but tenant databases fall behind.

Deployment checklist for Laravel SaaS

  • Release directory structure with shared storage and .env
  • Forward-compatible migration strategy
  • Tenant database migration plan (if multi_db)
  • Supervisor-managed queue workers with graceful restart
  • Environment variable validation before symlink swap
  • Health check endpoint with database and cache verification
  • Automated rollback on health check failure
  • CI/CD pipeline with test, build, deploy, and verify stages
  • Release retention policy (keep last N releases)
  • Post-deploy notification to team

Ship with confidence

Deployments should not be stressful events. With atomic releases, safe migrations, graceful worker restarts, and automated health checks, you can deploy your Laravel SaaS multiple times per day without impacting users.

If you want a production-ready foundation that is already structured for clean deployments, SaasForgeKit provides the baseline architecture so you can focus on your deployment pipeline instead of rebuilding infrastructure.

Share this post:

Ready to ship faster?

Build your SaaS with a production-ready foundation

Launch with authentication, billing, tenancy, and team workflows already in place, then focus on the features that make your product unique.

Related posts