Zero-Downtime Deployments with Laravel Forge


Every deployment carries risk. Your application might be unavailable for seconds or minutes while code updates, dependencies install, or caches rebuild. Here’s how to minimize that downtime with Laravel Forge.

Understanding the Problem

During a typical deployment:

  1. Git pulls new code
  2. Composer installs dependencies
  3. Migrations run
  4. Caches rebuild
  5. Queue workers restart

Users hitting your site during this process might see errors or stale content.

The Default Forge Deployment Script

Forge’s default deployment script is:

cd /home/forge/example.com
git pull origin main
composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader
php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan queue:restart

This works but has a problem: users can access your site while it’s being updated.

Enabling Maintenance Mode

The simplest approach is to enable maintenance mode during deployment:

cd /home/forge/example.com

# Put the application into maintenance mode
php artisan down

git pull origin main
composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader
php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan queue:restart

# Bring the application back up
php artisan up

This shows users a maintenance page instead of errors, but your application is still briefly unavailable.

Better: Render a Custom Maintenance Page

You can customize the maintenance page to match your brand:

php artisan down --render="errors::503"

Or specify a custom view:

php artisan down --render="maintenance"

Zero-Downtime with Envoyer

For true zero-downtime deployments, consider Laravel Envoyer. It:

  1. Deploys to a new directory
  2. Updates dependencies in the new directory
  3. Switches a symlink when ready
  4. Rolls back instantly if needed

However, you can achieve similar results with Forge using custom scripts.

Optimizing Your Deployment Script

Here are strategies to minimize downtime:

1. Parallel Processing

Run independent tasks in parallel:

cd /home/forge/example.com

git pull origin main
composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader &
php artisan migrate --force &
wait

php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan queue:restart

2. Cache Warming

Build caches before switching:

cd /home/forge/example.com

php artisan down --render="errors::503" --retry=60

git pull origin main
composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader
php artisan migrate --force

# Warm caches while in maintenance mode
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache

php artisan queue:restart
php artisan up

3. Health Checks

Verify the deployment succeeded before bringing the site back up:

cd /home/forge/example.com

php artisan down

git pull origin main
composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader
php artisan migrate --force
php artisan optimize

# Health check
if php artisan health:check; then
    php artisan queue:restart
    php artisan up
else
    echo "Health check failed! Application still in maintenance mode."
    exit 1
fi

Database Migrations

Migrations can be tricky. Follow these practices:

Never destructive during deployment: Don’t drop columns or tables in the same deployment as the code that stops using them.

Two-phase migrations:

  1. First deployment: Make column nullable or add new column
  2. Second deployment: Remove old column

Use transactions where possible:

public function up()
{
    DB::transaction(function () {
        // Your migration logic
    });
}

Queue Workers

Queue workers need special handling:

# Gracefully restart workers
php artisan queue:restart

This signals workers to finish their current job before restarting. For critical jobs, consider:

# Stop processing new jobs
php artisan horizon:pause

# Deploy your code

# Resume processing
php artisan horizon:continue

Deployment Notifications

Get notified when deployments complete:

cd /home/forge/example.com

# ... your deployment steps ...

# Notify on success
curl -X POST https://your-webhook-url.com/deploy-success

# Or use Laravel's notification system
php artisan notify:deploy-success

Monitoring Deployments

Track deployment health:

  1. Use application monitoring (Sentry, Bugsnag)
  2. Monitor error rates spike after deployment
  3. Track response times
  4. Set up alerts for deployment failures

Rollback Strategy

Always have a rollback plan:

# Quick rollback
cd /home/forge/example.com
git reset --hard HEAD~1
composer install --no-dev
php artisan migrate:rollback
php artisan optimize

Better yet, use Forge’s deployment history or Envoyer’s instant rollback.

Best Practices

  1. Test locally first: Always test migrations and updates locally
  2. Deploy during low traffic: Schedule deployments for off-peak hours
  3. Monitor after deployment: Watch logs and metrics for 15-30 minutes
  4. Keep deployments small: Smaller, frequent deployments reduce risk
  5. Use feature flags: Deploy code without activating features immediately
  6. Database backups: Always backup before running migrations

The Production Deployment Checklist

Before every production deployment:

  • Test migration on staging
  • Review deployment script
  • Check server disk space
  • Verify queue workers are running
  • Ensure database backup is recent
  • Have rollback plan ready
  • Monitor logs during deployment
  • Verify critical features after deployment

Deployments don’t have to be stressful. With proper planning and the right deployment strategy, you can deploy confidently and frequently.