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:
- Git pulls new code
- Composer installs dependencies
- Migrations run
- Caches rebuild
- 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:
- Deploys to a new directory
- Updates dependencies in the new directory
- Switches a symlink when ready
- 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:
- First deployment: Make column nullable or add new column
- 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:
- Use application monitoring (Sentry, Bugsnag)
- Monitor error rates spike after deployment
- Track response times
- 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
- Test locally first: Always test migrations and updates locally
- Deploy during low traffic: Schedule deployments for off-peak hours
- Monitor after deployment: Watch logs and metrics for 15-30 minutes
- Keep deployments small: Smaller, frequent deployments reduce risk
- Use feature flags: Deploy code without activating features immediately
- 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.